Search code examples
c++templatesconstructorshared-ptr

Multiple inside-of-class typedef of shared_ptr


I can currently initialize the following class MyTest

template<class T>
class MyTest {

public:
    typedef std::shared_ptr<MyTest> Ptr;

    MyTest( Ptr _nextTest = NULL ) : m_nextTest( _nextTest ) { }
    ~MyTest() { }

private:
    Ptr m_nextTest;

};

like so

MyTest<int>::Ptr my( new MyTest<int>() );

What I would now also like to be capable of is having a MyTest of a different type as an argument for the constructor as in

MyTest<int>::Ptr my( new MyTest<float>() );

But this obviously brakes (*1) as my typedef of Ptr within MyTest limits it to one type only. Is there a way to achieve this without specifying the second type manually for each instance of MyTest?


Edit: (comments suggested to elaborate on the question) My aim is to build some kind of pipeline. The pipeline will consist of several independent pieces. I want to inherently control in which way those pieces can be plugged into another. Meaning, that if a current piece has int input and float output the next piece must have float as input. I got this far but as you can see my knowledge on templates is rather limited:

template<class I, class O>
class AbstractPipelineTask {

public:
    typedef std::shared_ptr<AbstractPipelineTask> Ptr;

    AbstractPipelineTask( Ptr _nextTask ) : m_nextTask ( _nextTask ) { }
    virtual ~AbstractPipelineTask() { }

    void call( I _input ) {

        m_input = _input;
        m_output = executeTask( m_input );

        if ( m_nextTask )
            m_nextTask->call( m_output );
    }

    virtual O executeTask( I _input ) = 0;

protected:  
    Ptr m_nextTask;

    I m_input;
    O m_output;
};


template<class I, class O>
class ActualPipelineTask : public AbstractPipelineTask< I, O > {

public:
    typedef std::shared_ptr<ActualPipelineTask> Ptr;

    ActualPipelineTask( AbstractPipelineTask<I,O>::Ptr _nextTask ) : AbstractPipelineTask( _nextTask ) { }
    virtual ~ActualPipelineTask() { }

    virtual O executeTask( I _input ) {

        return ( _input * _input );
    }
};

class Str2IntPipelineTask : public AbstractPipelineTask< std::string, int > {

public:
    typedef std::shared_ptr<Str2IntPipelineTask> Ptr;

    Str2IntPipelineTask( AbstractPipelineTask<std::string,int>::Ptr _nextTask = NULL ) : AbstractPipelineTask( _nextTask ) { }
    virtual ~Str2IntPipelineTask() { }

    virtual int executeTask( std::string _input ) {

        return atoi( _input.c_str() );
    }
};

I can plug together as many ActualPipelineTasks as I want but I cant combine any that don't have the exact same I and O types:

// Works
ActualPipelineTask<int,int>::Ptr    pipeTask2( new ActualPipelineTask<int,int>() );
ActualPipelineTask<int,int>::Ptr    pipeTask1( pipeTask2 );

// Doesn't work (*2)
ActualPipelineTask<int,int>::Ptr    pipeTask2( new ActualPipelineTask<int,int>() );
ActualPipelineTask<int,int>::Ptr    pipeTask1( pipeTask2 );
Str2IntPipelineTask::Ptr            pipeTask0( pipeTask1 );

I probably used the wrong tool for what I want to achieve. But that was the only one I could come up with.

*1:

error C2664: 'std::_Ptr_base<_Ty>::_Reset0' : cannot convert parameter 1 from 'MyTest *' to 'MyTest *'

*2

error C2664: 'std::shared_ptr<_Ty>::shared_ptr(std::nullptr_t)' : cannot convert parameter 1 from 'std::shared_ptr<_Ty>' to 'std::nullptr_t'


Solution

  • MyTest<int> and MyTest<float> are totally unrelated types. A pointer (smart or plain) to one of these types cannot point to an object of the other one, regardless of any typedefs involved.

    If you want to be able to have a pointer capable of pointing to both, you'll have to redesign the MyTest class template - perhaps derive it from a non-template base class. But bear in mind that this would indeed be a redesign - you'd probably need a virtual interface in this base class, and so on. Approach the problem as such. There's no easy hack available.


    Regarding your update.

    Always bear in mind templates are a compile-time construct. You can use templates when the way you use them is known at compile time. In your situation, that's not quite the case.

    You've incorporated both the input type and output type into type of the task. Therefore, when building the pipeline, each task in the pipeline must know the input type and the output type of the next task.

    How you can solve this depends on how you intend to use the pipeline. There are two possible situations:

    Scenario A. You know the structure (the type sequence) of the pipeline at compile time, and just want to be able to select the tasks at runtime. You can do that with your current classes simply by separating the "sequencing" functionality from the "execution" functionality (which would be a good idea anyway: a class should have just a single responsibility). The code would then look like this:

    template<class I, class O>
    class AbstractPipelineTask {
    
    public:
        typedef std::shared_ptr<AbstractPipelineTask> Ptr;
    
        virtual ~AbstractPipelineTask() { }
    
        virtual O executeTask( I _input ) = 0;
    
    protected:  
        Ptr m_nextTask;
    };
    
    
    class MyKnownPipeline
    {
      AbstractPipelineTask<int, int>::Ptr task0;
      AbstractPipelineTask<int, std::string>::Ptr task1;
      AbstractPipelineTask<std::string, int>::Ptr task2;
    
    public:
      // Functions to set up the tasks somehow
    
      int call(int input)
      {
        return task2->execute(task1->execute(task0->execute(input)));
      }
    };
    

    To make this more flexible (e.g. if you have multiple pipelines), you could turn MyKnownPipeline into a variadic class template. I consider doing that beyond the scope of this answer, it would be rather complex template code.

    Scenario B. You don't know the structure of the pipeline at compile time. You just know you'll have a pipeline and configure it somehow at runtime (or only with runtime information).

    In that case, templates can't really help you (at least on the interface part). You need the abstract base class to be type-independent. Look into ways of doing type erasure and storing data of unknown type in memory (see e.g. Boost.Variant or Boost.Any).

    You could still use templates on the implementation side. Here's an idea of what could be done; I don't guarantee it's complete, correct or wise to do it that way.

    class AbstractTask
    {
    public:
      virtual bool acceptableNext(const AbstractTask &next) const = 0;
    
      virtual boost::any execute(boost::any input) = 0;
    };
    
    
    template <class I>
    class KnownInputTask : public AbstractTask
    {
    public:
      boost::any execute(boost::any input) override final
      {
        return execute_impl(any_cast<I>(input));
      }
    
    private:
      virtual boost::any execute_impl(I) = 0;
    };
    
    
    template <class I, class O>
    class KnownInOutTask : KnownInputTask<I>
    {
    public:
      bool acceptableNext(const AbstractTask &next) const override
      {
        return dynamic_cast<KnownInputTask<I>*>(&next) != nullptr;
      };
    };
    
    
    class Pipeline
    {
      std::vector<AbstactTask::Ptr> tasks;
    
    public:
      bool addTask(AbstractTask::Ptr task)
      {
        if (!tasks.empty() && !tasks.back()->acceptableNext(*task)) return false;
        tasks.push_back(task);
        return true;
      }
    
      boost::any call(boost::any input) const
      {
        for (auto t : tasks)
          input = t->execute(input);
        return input;
      }
    };