基于 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;
}
在原始代码中,我发现了三个问题:
std::declval<TLeafType*>() -> getOutput
尝试在不完整的 class.
中查找名称
std::declval<TLeafType*>() -> getOutput<N>
没有命名函数模板 getOutput
.
派生 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);
背景资料
我正在研究类似数据流的设计模式。下面显示的 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;
}
在原始代码中,我发现了三个问题:
中查找名称std::declval<TLeafType*>() -> getOutput
尝试在不完整的 class.std::declval<TLeafType*>() -> getOutput<N>
没有命名函数模板getOutput
.派生 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
。如果我们使用:
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);