这相当于元编程:我应该编写一个函数还是应该内联编写代码。更喜欢编写类型特征的原因与更喜欢编写函数的原因相同:它更具自记录性,可重用,更易于调试。喜欢编写尾随 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>>;
组合两个尾随返回类型需要写出所有两个表达式......
Negation
通过类型特征,可以很容易地检查类型doesn'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); }
The 0
and int/...
有点奇怪吧?
static_assert
如果我不想在某个概念上进行 SFINAE,而只想通过明确的信息进行硬失败,该怎么办?
template <class T>
struct requires_fooability {
static_assert(fooable<T>{}, "T must be fooable!");
};
Concepts
当(如果?)我们得到概念时,显然,当涉及到与元编程相关的所有内容时,实际使用概念要强大得多:
template <fooable T> void bar(T ) { ... }