基于 C++ CRTP 的数据流输出 class 设计简化

C++ CRTP based dataflow output class design simplification


背景资料

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


问题

为了允许自动识别用户定义端口的数量(即方法 getOutputImpl),在基础 level1 class 中提供了方法 getOutputPortsNumber(void)。此方法基于所有用户定义方法 getOutputImpl 的 return 类型为 std::unique_ptr<TOutputType> 的想法。因此,可以在基础 class 中定义一个附加的 getOutputImpl 方法,它没有这种 return 类型(例如,它有一个 void return 类型:void getOutputImpl(...)).

如果 void getOutputImpl(...) 在用户定义的派生 class(本例中为 DataflowOutputClass)以及其他用户定义的 std::unique_ptr<TOutputType> getOutputImpl(...) 方法。然而,当额外的 void getOutputImpl(...) 方法被移动到基础 level1 class 时,我得到一个编译错误: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;
}

在原始代码中,我发现了三个问题:

  1. std::declval<TLeafType*>() -> getOutput 尝试在不完整的 class.

    中查找名称
  2. std::declval<TLeafType*>() -> getOutput<N> 没有命名函数模板 getOutput.

  3. 派生 class 中的 getOutputImpl 声明隐藏了与基 class.

    同名的所有成员函数

1 查找一个不完整的名字 class

表达式std::declval<TLeafType*>() -> getOutput用于DataflowOutputClass::getOutputPortsNumberImpl的return类型。

实例化 class 模板会导致实例化所有成员函数的声明。当您在 DataflowOutputClass class 中通过 level1<DataflowOutputClass<T>> 使用 CRTP 派生时,编译器需要在实例化派生的 class 之前实例化 level1<..>。因此,level1<DataflowOutputClass<T>>的实例化过程中,DataflowOutputClass<T>class仍然是不完整的

一种解决方法是通过使 DataflowOutputClass::getOutputPortsNumberImpl 的 return 类型依赖于函数模板的模板参数来推迟确定它:

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

现在,return 类型依赖于函数模板 的模板参数。这个return类型只能在实例化函数时解析。该函数通过在 main 中使用 getOutputPortsNumber 隐式实例化,其中派生的 class 已经完成。

请注意,在派生class的范围内不必查找名称getOutput,您也可以默认T = level1。如果我们使用:

,我们将不会在派生的 class 中查找名称
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的return类型,需要实例化getOutput的定义,因为getOutput使用了return类型推导.它的定义会遇到与原始代码类似的问题:它会尝试查找不完整类型的名称。

通过使调用函数模板的 return 类型依赖于函数模板参数来解决 return 类型推导的问题对我来说似乎是一个糟糕的修复,但整个技术可以被更简单的东西取代,见下文。


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派生class成员函数隐藏基class成员函数

一般来说,派生 class 中的任何成员都隐藏了基 class 中同名的成员。要用基 class 中的成员函数重载派生 class 中的成员函数,您可以使用 using-declaration 来“注入”基本成员到派生 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 不需要 using 声明的解决方案

目前,需要 using 声明,因为 OP 的元编程必须为表达式 getOutput<N>(PolyIndex({})) 生成有效的 return 类型。当前的方法将 void 与非 void return 类型区分开来。相反,我们可以简单地检测表达式 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);