背景信息

我正在研究类似数据流的设计模式。下面提供的类旨在表示输出数据分发机制。 level1是CRTP的基类。 getOutput<N>中的level1是可用于从派生类的实例获取输出数据的函数。根据模板参数N,它调用用户定义的方法getOutputImpl之一。这些方法应在(CRTP样式)派生类中提供。每个方法getOutputImpl都定义一个与用户定义的派生类相关的输出端口。 getOutputImpl方法的输入类型是由设计定义的。 getOutputImpl方法的输出类型可以不同。但是,根据设计,输出类型必须具有结构std::unique_ptr<TOutputType>,其中TOutputType可以是任何类。更多背景信息可以在这里找到:previous question

问题

为了自动识别用户定义的端口数(即getOutputImpl方法),在getOutputPortsNumber(void)基类中提供了level1方法。此方法基于以下想法:所有用户定义的方法getOutputImpl的返回类型为std::unique_ptr<TOutputType>。因此,可以在基类中定义不具有此返回类型的另一种getOutputImpl方法(例如,其具有void返回类型:void getOutputImpl(...))。

如果在用户定义的派生类(此示例中为void getOutputImpl(...))中定义了DataflowOutputClass以及其他用户定义的std::unique_ptr<TOutputType> getOutputImpl(...)方法,则上述方法将起作用。但是,将其他void getOutputImpl(...)方法移到基本level1类时,出现编译错误:no matching function for call to 'DataflowOutputClass<int>::getOutputImpl(PortIdxType<2ul>, const PolyIndex&) const

代码

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;
}

最佳答案

在原始代码中,我确定了三个问题:

  • std::declval<TLeafType*>() -> getOutput尝试在不完整的类中查找名称。
  • std::declval<TLeafType*>() -> getOutput<N>没有命名功能模板getOutput
  • 派生类中的getOutputImpl声明隐藏与基类名称相同的所有成员函数。

  • 1在不完整的类(class)中查找姓名std::declval<TLeafType*>() -> getOutput的返回类型使用表达式DataflowOutputClass::getOutputPortsNumberImpl
    实例化类模板导致实例化所有成员函数的声明。当您通过level1<DataflowOutputClass<T>>类中的DataflowOutputClass使用CRTP派生时,编译器需要在实例化派生类之前实例化level1<..>。因此,在level1<DataflowOutputClass<T>>的实例化期间,DataflowOutputClass<T>类仍然不完整。
    一种解决方法是通过使DataflowOutputClass::getOutputPortsNumberImpl返回类型取决于函数template的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;}
    
    现在,返回类型取决于函数模板的模板参数。仅在实例化函数时才能解析此返回类型。通过使用getOutputPortsNumber中的main隐式实例化该函数,其中派生类已完成。
    注意,不必在派生类的范围内查找名称getOutput,也可以使用默认的T = level1。如果使用以下命令,则不会在派生类中查找名称:
    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;}
    
    但是,要确定此getOutputPortsNumberImpl的返回类型,必须实例化getOutput的定义,因为getOutput使用返回类型推导。其定义将遭受与原始代码类似的问题:它试图查找不完整类型的名称。
    通过使调用函数模板的返回类型依赖于函数模板参数来解决返回类型推论的问题,对我来说似乎是一个较差的解决方法,但是可以用更简单的方法代替整个技术,请参见下文。

    2 declval<TLeafType*> -> getOutput<N>没有命名功能模板
    在#1中,我们已经用std::declval<T*>() -> getOutput<N>(PolyIndex({}))替换了它,但是问题是相同的。考虑:
    bool operator> (bool, PolyIndex);
    
    // class template level1
    
    struct derived
    {
        int getOutput;
    };
    
    使用此设置,像declval<T*>() -> getOutput<N>(PolyIndex({}))这样的表达式可以解析为:
    (
      (declval<T*>()->getOutput)  <  N
    )
    >
    (
      PolyIndex({})
    )
    
    (x < N) > PolyIndex{}
    为了使编译器了解getOutput是模板,请使用template关键字:
    std::declval<T*>() -> template getOutput<N>(PolyIndex{})
    
    (不需要额外的()来初始化PolyIndex。)

    3派生类成员函数隐藏基类成员函数
    通常,派生类中的任何成员都会隐藏与基类同名的成员。要使派生类中的成员函数与基类中的成员函数重载,可以使用using-声明将基成员的名称“注入(inject)”到派生类中:
    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不需要使用声明的解决方案
    当前,需要使用声明,因为OP的元编程必须为表达式getOutput<N>(PolyIndex({}))生成有效的返回类型。当前的方法区分void和非void返回类型。相反,我们可以简单地检测getOutput<N>(PolyIndex{})表达式是否格式正确。为此,我将使用Walter E. Brown的void_t技术:
    template<typename T>
    struct voider { using type = T; };
    
    template<typename T>
    using void_if_well_formed = typename voider<T>::type;
    
    我们将按以下方式使用它:
    void_if_well_formed< decltype(expression) >
    
    如果表达式格式正确,将产生void类型。否则,如果表达式由于立即上下文中的替换失败而格式错误,则整个void_if_well_formed<..>将在立即上下文中产生替换失败。可以通过一种称为SFINAE的技术来使用这些类型的错误:替换失败不是错误。在“即时上下文不是错误”中,可以更恰当地将其命名为“替换失败”。
    可以利用SFINAE通过声明两个功能模板。让expression<T>()代表依赖于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*);
    
    如果现在通过test<some_type>(nullptr)调用测试,则首选第一个重载,因为参数类型与函数参数类型完全匹配。但是,第二次过载也是可行的。如果第一个过载由于SFINAE格式错误,则将其从过载集中删除,而选择第二个过载:
    template<typename T>
    using test_result = decltype( test<T>(nullptr) );
    
    使用这些技术,我们可以实现level1,如下所示:
    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);}
    
    };
    
    使用slightly more work,我们甚至可以这样编写:
    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);
    

    07-24 21:46