Search code examples
c++c++11design-patternscrtpdataflow

C++ CRTP based dataflow output class design simplification



Background information

I am working on a dataflow-like design pattern. The classes presented below are meant to represent the output data dispatch mechanism. level1 is a CRTP base class. getOutput<N> in level1 is the function that can be used for getting the output data from an instance of a derived class. Depending on the template parameter N it calls one of the user defined methods getOutputImpl. These methods are meant to be provided in a (CRTP-style) derived class. Each of the methods getOutputImpl defines an output port associated with the user defined derived class. The input type of the method getOutputImpl is defined by design. The output types of the methods getOutputImpl can vary. However, by design, the output type must have a structure std::unique_ptr<TOutputType>, where TOutputType can be any class. More background information can be found here: previous question.


Question

To allow for automatic recognition of the number of the user defined ports (i.e. methods getOutputImpl) a method getOutputPortsNumber(void) is provided in the base level1 class. This method is based around the idea that the return type of all user defined methods getOutputImpl is std::unique_ptr<TOutputType>. Thus, one can define an additional getOutputImpl method in the base class that does not have this return type (e.g. it has a void return type: void getOutputImpl(...)).

The methodology described above works if the void getOutputImpl(...) is defined within the user-defined derived class (DataflowOutputClass in this example) along with other user-defined std::unique_ptr<TOutputType> getOutputImpl(...) methods. However, when the additional void getOutputImpl(...) method is moved to the base level1 class, I get a compilation error: no matching function for call to 'DataflowOutputClass<int>::getOutputImpl(PortIdxType<2ul>, const PolyIndex&) const.


Code

typedef size_t Index;
typedef unsigned long Natural;
typedef std::vector<Index> PolyIndex;
typedef const PolyIndex& crPolyIndex;
template<Index N> struct PortIdxType{};

template<typename TLeafType>
class level1
{

public:

    TLeafType* asLeaf(void)
        {return static_cast<TLeafType*>(this);}

    TLeafType const* asLeaf(void) const
        {return static_cast<TLeafType const*>(this);}

    template <Index N>
    auto getOutput(crPolyIndex c_Idx) const
        {return asLeaf() -> getOutputImpl(PortIdxType<N>{}, c_Idx);}

    static constexpr Natural getOutputPortsNumber(void)
        {return getOutputPortsNumberImpl<0>();}

    template<Index N>
    static constexpr std::enable_if_t<
        std::is_void<
            decltype(
                std::declval<TLeafType*>() ->
                getOutput<N>(PolyIndex({}))
                )
        >::value,
            Index
            > getOutputPortsNumberImpl(void)
        {return N;}

    template<Index N>
    static constexpr std::enable_if_t<
        !std::is_void<
            decltype(
                std::declval<TLeafType*>() ->
                getOutput<N>(PolyIndex({}))
                )
        >::value,
            Index
            > getOutputPortsNumberImpl(void)
        {return getOutputPortsNumberImpl<N + 1>();}

    template<Index N>
    void getOutputImpl(
        PortIdxType<N>, crPolyIndex c_Idx
        ) const
        {throw std::runtime_error("Wrong template argument.");}


};

template<typename T>
class DataflowOutputClass:
    public level1<DataflowOutputClass<T>>
{
public:

    // if void getOutputImpl(...) const is moved here from level1,
    // then the code compiles and works correctly.

    //overload for when N = 0
    std::unique_ptr<double> getOutputImpl(
        PortIdxType<0>, crPolyIndex c_Idx
        ) const
    {
        std::unique_ptr<double> mydouble(new double(10));
        return mydouble;
    }

    //overload for when N = 1
    std::unique_ptr<int> getOutputImpl(
        PortIdxType<1>, crPolyIndex c_Idx
        ) const
    {
        std::unique_ptr<int> myint(new int(3));
        return myint;
    }

};


int main()
{
    DataflowOutputClass<int> a;
    std::cout << a.getOutputPortsNumber() << std::endl;
}

Solution

  • In the original code, I identified three issues:

    1. std::declval<TLeafType*>() -> getOutput tries to look up a name in an incomplete class.

    2. std::declval<TLeafType*>() -> getOutput<N> does not name a function template getOutput.

    3. The getOutputImpl declarations in the derived class hide any member functions with the same name of the base class.


    1 Looking up a name in an incomplete class

    The expression std::declval<TLeafType*>() -> getOutput is used in the return type of DataflowOutputClass::getOutputPortsNumberImpl.

    Instantiating a class template leads to the instantiation of the declarations of all member functions. When you derive with CRTP via level1<DataflowOutputClass<T>> in the DataflowOutputClass class, the compiler needs to instantiate level1<..> before instantiating the derived class. Therefore, during the instantiation of level1<DataflowOutputClass<T>>, the DataflowOutputClass<T> class is still incomplete.

    A workaround is to postpone the determination of the return type of DataflowOutputClass::getOutputPortsNumberImpl by making it dependent on a template parameter of the function template:

    template<Index N, typename T = TLeafType>
    static constexpr std::enable_if_t<
        std::is_void<
            decltype(
                std::declval<T*>() ->
                getOutput<N>(PolyIndex({}))
                )
        >::value,
            Index
            > getOutputPortsNumberImpl(void)
        {return N;}
    

    Now, the return type is dependent on a template-parameter of the function template. This return type can only be resolved when the function is instantiated. The function is instantiated implicitly via the usage of getOutputPortsNumber in main, where the derived class is already complete.

    Note that it is not necessary to look up the name getOutput in the scope of the derived class, you can as well default T = level1. We would not look up a name in the derived class if we used:

    template<Index N, typename T = TLeafType>
    static constexpr std::enable_if_t<
        std::is_void<
            decltype(
                getOutput<N>(PolyIndex({}))
                )
        >::value,
            Index
            > getOutputPortsNumberImpl(void)
        {return N;}
    

    However, to determine the return type of this getOutputPortsNumberImpl, instantiating the definition of getOutput is necessary, because getOutput uses return type deduction. Its definition will suffer from a similar problem as the original code: it tries to look up a name in an incomplete type.

    Fixing the issue with return type deduction by making the return type of the calling function template dependent on a function template parameter seems like a poor fix to me, but the whole technique can be replaced by something simpler, see below.


    2 declval<TLeafType*> -> getOutput<N> does not name a function template

    In #1, we have already replaced this with std::declval<T*>() -> getOutput<N>(PolyIndex({})), but the issue is the same. Consider:

    bool operator> (bool, PolyIndex);
    
    // class template level1
    
    struct derived
    {
        int getOutput;
    };
    

    With this setup, an expression like declval<T*>() -> getOutput<N>(PolyIndex({})) can be parsed as:

    (
      (declval<T*>()->getOutput)  <  N
    )
    >
    (
      PolyIndex({})
    )
    

    That is, (x < N) > PolyIndex{}.

    To allow the compiler to understand that getOutput is a template, use the template keyword:

    std::declval<T*>() -> template getOutput<N>(PolyIndex{})
    

    (The additional () to initialize PolyIndex are unnecesary.)


    3 Derived class member functions hide base class member functions

    In general, any member in a derived class hides members of the same name of the base classes. To overload a member function in a derived class with member functions in a base class, you can use a using-declaration to "inject" the name of the base member into the derived class:

    template<typename T>
    class DataflowOutputClass:
        public level1<DataflowOutputClass<T>>
    {
    public:
    
        using level1<DataflowOutputClass<T>>::getOutputImpl;
    
        //overload for when N = 0
        std::unique_ptr<double> getOutputImpl(
            PortIdxType<0>, crPolyIndex c_Idx
            ) const;
    
        // ...
    };
    

    4 A solution that does not require a using-declaration

    Currently, a using-declaration is required since the OP's metaprogramming must produce a valid return type for the expression getOutput<N>(PolyIndex({})). The current approach differentiates void from non-void return types. Instead, we can simply detect if the expression getOutput<N>(PolyIndex{}) is well-formed. For this, I'll use Walter E. Brown's void_t technique:

    template<typename T>
    struct voider { using type = T; };
    
    template<typename T>
    using void_if_well_formed = typename voider<T>::type;
    

    We will use this as follows:

    void_if_well_formed< decltype(expression) >
    

    will yield the type void if the expression is well-formed. Otherwise, if the expression is ill-formed due to a substitution failure in the immediate context, the whole void_if_well_formed<..> will produce a substitution failure in the immediate context. These kinds of errors can be used by a technique called SFINAE: Substitution Failure Is Not An Error. It might be more appropriately be named Substitution Failure In The Immediate Context Is Not An Error.

    SFINAE can be exploited e.g. by declaring two function templates. Let expression<T>() stand for any expression that is dependent on T.

    template<typename T, void_if_well_formed<decltype(expression<T>())>* = nullptr>
    std::true_type  test(std::nullptr_t);
    
    template<typename T>
    std::false_type test(void*);
    

    If we now call the test via test<some_type>(nullptr), the first overload is preferred because the argument types matches exactly the function parameter type. However, the second overload is also viable. If the first overload is ill-formed due to SFINAE, it is removed from the overload set and the second overload is chosen instead:

    template<typename T>
    using test_result = decltype( test<T>(nullptr) );
    

    Using these techniques, we can implement level1 as follows:

    template<typename TLeafType>
    class level1
    {
    public:
        template <Index N, typename T = TLeafType>
        using output_t =
            decltype(std::declval<T*>() ->
                     getOutputImpl(PortIdxType<N>{}, std::declval<crPolyIndex>()));
    
        static constexpr Natural getOutputPortsNumber(void)
            {return getOutputPortsNumberImpl<0>(nullptr);}
    
        template<Index N>
        static constexpr Index getOutputPortsNumberImpl(void*)
            {return N;}
    
        template<Index N, typename T = TLeafType,
                 void_if_well_formed<output_t<N, T>>* = nullptr>
        static constexpr Index getOutputPortsNumberImpl(std::nullptr_t)
            {return getOutputPortsNumberImpl<N + 1>(nullptr);}
    
    };
    

    With slightly more work, we can even write this as follows:

    template<Index N>
    struct HasOutputFor
    {
        static auto P() -> PortIdxType<N>;
        static auto cr() -> crPolyIndex;
        
        template<typename T>
        static auto requires_(T&& t) -> decltype(t.getOutputImpl(P(), cr()));
    };
    
    template<Index N, typename T = TLeafType, REQUIRE( HasOutputFor<N>(T) )>
    static constexpr Index getOutputPortsNumberImpl(std::nullptr_t);