这是元编程的等价物:我应该写一个函数还是直接在代码中写。偏爱编写类型特征的原因与偏爱编写函数的原因相同:它更具自我说明性,可重用,易于调试。偏爱编写尾随 decltype 的原因与偏爱内联编写代码的原因类似:它是一次性的,不可重用,那么为什么要花费精力将其分解并想出一个合理的名称呢?
但是以下是您可能需要类型特征的原因:
重复
假设我有一个要多次检查的特征。像 fooable
。如果我只编写一次类型特征,我可以将其视为概念:
template <class, class = void>
struct fooable : std::false_type {};
template <class T>
struct fooable<T, void_t<decltype(std::declval<T>().foo())>>
: std::true_type {};
现在我可以在很多地方使用同样的概念:
template <class T, std::enable_if_t<fooable<T>{}>* = nullptr>
void bar(T ) { ... }
template <class T, std::enable_if_t<fooable<T>{}>* = nullptr>
void quux(T ) { ... }
对于检查多个表达式的概念,您不希望每次都重复它。
可组合性
与重复相伴随,组合两种不同的类型特征很容易:
template <class T>
using fooable_and_barable = std::conjunction<fooable<T>, barable<T>>
合并两个返回类型需要编写出两个表达式的全部内容...
否定
使用类型特征轻松检查类型不满足某一特征。只需使用!fooable<T>::value
,即可实现。无法编写一个尾随的decltype
表达式来检查某些内容是否无效。当您拥有两个不相交的重载时,可能会遇到这种情况:
template <class T, std::enable_if_t<fooable<T>::value>* = nullptr>
void bar(T ) { ... }
template <class T, std::enable_if_t<!fooable<T>::value>* = nullptr>
void bar(T ) { ... }
这很好地引出了...
标签分派
假设我们有一个短的类型特性,使用类型特性进行标签分派会更清晰:
template <class T> void bar(T , std::true_type fooable) { ... }
template <class T> void bar(T , std::false_type not_fooable) { ... }
template <class T> void bar(T v) { bar(v, fooable<T>{}); }
比起其他情况,这样会更好:
template <class T> auto bar(T v, int ) -> decltype(v.foo(), void()) { ... }
template <class T> void bar(T v, ... ) { ... }
template <class T> void bar(T v) { bar(v, 0); }
这里的 0
和 int/...
有点奇怪,对吧?
static_assert
如果我不想在概念上进行 SFINAE,而是想要通过明确的信息来硬性失败怎么办?
template <class T>
struct requires_fooability {
static_assert(fooable<T>{}, "T must be fooable!");
};
概念
如果我们某时(是否?)获得了概念,那么在涉及元编程的所有方面中,实际使用概念显然更加强大:
template <fooable T> void bar(T ) { ... }
typename = void_t<>
可以改为typename = void
,这样更清晰易懂。 - vsoftcodecltype
代替void_t
。但我正在询问略有不同的事情。无论如何,谢谢。 - skypjack