1

I'm currently trying to program a pipeline, which is able to process different kind of data in every pipeline element. Now I want to use unique_ptrs with some kind of template polymorphism and template specialization.

struct Start {}; // Dummy struct

template<typename In, typename Out>
class PipelineElement {
public:
    virtual Out process(In in) = 0;
};

// partial template specialization for the first pipeline element
template<typename Out>
class PipelineElement<Start, Out> {
public:
    virtual Out process() = 0;
};

class Producer : public PipelineElement<Start, int> {
    int process() override { ... }
};

Now a function shall take a unique_ptr of a partial specialized PipelineElement. However, the following won't compile with the error message:

auto Pipeline::setStart<int>(std::unique_ptr<PipelineElement<Start,int>,std::default_delete<PipelineElement<Start,int>>>)': cannot convert argument 1 from 'std::unique_ptr<Producer,std::default_delete<Producer>>' to 'std::unique_ptr<PipelineElement<Start,int>,std::default_delete<PipelineElement<Start,int>>>

class Pipeline {
    template<typename Out>
    static auto setStart(std::unique_ptr<PipelineElement<Start, Out>> element) { ... }
};

int main() {
    Pipeline::setStart(std::make_unique<Producer>());
}

If I use regular pointers instead, it does compile without any errors.

Why does the version with normal pointers compile, and the version with smart pointers doesn't?

class Pipeline {
    template<typename Out>
    auto setStart(PipelineElement<Start, Out>* element) { ... }
};

int main() {
    Pipeline::setStart(new Producer());
}
Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
  • 1
    In my tests ([here](https://ideone.com/Yaf7Sn) and [here](https://onlinegdb.com/HkqS9XCuD)), `Pipeline::setStart(std::make_unique());` works if you specify the template parameter explicitly instead of letting the compiler try to deduce it: `Pipeline::setStart(std::make_unique());` – Remy Lebeau Nov 03 '20 at 00:53
  • But do note that deleting a `Producer` object via a `PipelineElement*` pointer won't work correctly since `PipelineElement` does not have a `virtual` destructor, so the `Producer` destructor won't be called ([proof](https://ideone.com/Yaf7Sn)). – Remy Lebeau Nov 03 '20 at 00:59

1 Answers1

1

The PipelineElement doesn't have a virtual destructor. So deleting Producer by deleting pointer to PipelineElement will cause an error.

Edit: due to @RemyLebeau info. Unfortunately, std::unique_ptr fails to figure this out. The compilation issue is because compiler fails to deduce the template parameters of setStart. Note: please, in the future provide genuine error messages and not fake ones.

The ability to deduce template parameters is limited and doesn't try most forms of conversions except some trivial ones during the template parameters search. If you don't want to specify the template parameters each time it is adviced to accept a more general type of input and impose SFINEA-based restrictions:

Here, I wrote an example based on restricting input unique_ptr<T> to have T inherit from PipelineElementBase.

#include <iostream>
#include <memory>
#include <type_traits>
using namespace std;
 
struct Start {}; // Dummy struct

class PipelineElementBase
{
    public:
    virtual ~PipelineElementBase() = default;
};

template<typename In, typename Out>
class PipelineElement : public PipelineElementBase
{
public:
    ~PipelineElement() { cout << "~PipelineElement<In,Out>" << endl; }
    virtual Out process(In in) = 0;
};
 
// partial template specialization for the first pipeline element
template<typename OutParam>
class PipelineElement<Start, OutParam> : public PipelineElementBase
{
public:
    using Out = OutParam;
    ~PipelineElement() { cout << "~PipelineElement<Start,Out>" << endl; }
    virtual Out process() = 0;
};
 
class Producer : public PipelineElement<Start, int>
{
public:
    ~Producer() { cout << "~Producer" << endl; }
    int process() override { return 1; }
};
 
class Pipeline
{
public:
    template<typename PE, std::enable_if_t<std::is_base_of_v<PipelineElementBase, PE>,int> = 0>
    static auto setStart(std::unique_ptr<PE> element) 
    { 
        using Out = typename PE::Out;
        return 1; 
    }
};
 
int main()
{
    Pipeline::setStart(std::make_unique<Producer>());
    return 0;
}
ALX23z
  • 4,456
  • 1
  • 11
  • 18
  • 1
    "*the two types' default destructors are non convertible between each other*" - I thought so too, but [this reference](https://en.cppreference.com/w/cpp/memory/default_delete) suggests `std::default_delete` should convert to `std::default_delete>`: "*Constructs a `std::default_delete` object from another `std::default_delete` object. This constructor will only participate in overload resolution if `U*` is implicitly convertible to `T*` ... [this] makes possible the implicit conversion from `std::unique_ptr` to `std::unique_ptr`*" – Remy Lebeau Nov 03 '20 at 00:58
  • @RemyLebeau strangr. Originally, I also thought about failing to deduce template parameters but the error message implied that the template parameter was deduced... wait OP's message wasn't error message? – ALX23z Nov 03 '20 at 01:01