CTTCG 19.1 用 SFINAE 实现 Traits

原理是用 SFINAE 机制安全地匹配几个函数或函数模板,然后再通过函数匹配信息将结果(true or false)嵌入 Trait 类中。Trait 约束条件被包含在函数模板的声明中。

实现 traits 最核心的要点是模拟被替换类型的行为,然后让行为不满足的那些替换被 SFINAE 筛掉。

函数返回基本类型 → 静态常量定义

类模板实例化时会调用 test 函数,如果条件命中则优先匹配 char test(void *),否则匹配另外一个。通过判断匹配到的函数的返回值类型可以确定条件是否被命中。这里的条件是类型 T 可以默认构造。

⚠ 如果把第一个 test 的参数 U 去掉,默认参数改成 typename = decltype(T()),就无法编译。因为 T 会在实例化时被替代过去,导致函数模板本身定义有错误。这个过程没有发生在函数模板匹配阶段,不会受到 SFINAE 的保护。

也有通过判断返回值的类型大小确认是否命中的实现。但是在有些低性能设备上所有的整型可能具有相同的大小,因此有必要使用 charchar (&)[2] 等手段来保证返回值的大小不同。

函数返回标准库的常量 Trait → alias 模板

这里的 IsDefaultConstructibleHelper 包装了 Type。所以还需要继承一次取得和 C++ 标准库相同形式的 Trait 类。

如果上面的工具方法裸露在外面,就能用 alias template 直接得到 Trait 类:

打印的结果是 01(各占一行)。

(最简单)偏特化 + 继承常量 Trait

这种用法也是在标准库中大量出现的。

上面通过偏特化和继承直接达到了目的。这种写法更加简单,因为判断只有二元性,因此只需要提供原始模板 1 次,然后偏特化 1 次。C++17 已经有了 std::void_t 模板,能够达到和上面的 VoidT 相似的效果。

在这个原始模板中,模板参数默认值可以直接写成 typename = void,不用写 typename = VoidT<>

__cpp_lib_void_t 是一个编译器宏。C++标准倡导编译器使用类似的宏以标志是否已经实现某一个语言特性。

(最通用)用 generic lambda 构造 Traits 工厂

template<typename F, typename... Args,
typename = decltype(std::declval<F>()(std::declval<Args&&>()...))>
std::true_type isValidImpl(void*);

// fallback if helper SFINAE’d out:
template<typename F, typename... Args>
std::false_type isValidImpl(...);

// define a lambda that takes a lambda f and returns
// whether calling f with args is valid
inline constexpr
auto isValid = [auto f](auto f) {
    return [auto&&... args](auto&&... args) {
        return decltype(
            isValidImpl<decltype(f), decltype(args)&&...>(nullptr)
        ){};
    };
};

// helper template to represent a type as a value
template<typename T>
struct TypeT {
    using Type = T;
};

// helper to wrap a type as a value
template<typename T>
constexpr auto type = TypeT<T>{};

// helper to unwrap a wrapped type in unevaluated contexts
template<typename T>
T valueT(TypeT<T>); // no definition needed
  • 后面三个和 Type 相关的定义都是为了把类型完整地保留起来(直接传值会遇到 decay 的情况)。
  • 第二个 isValidImpl 的实现是一个保险,这个和前面非工厂的思路是一样的;第一个 isValidImpl 的实现要求函数 f 的声明和参数 Args... 是相合的。传入 nullptr 是想要让 (void *) 版本被优先匹配,而 (...) 版本被低优先级考虑;不传入任何值时是 void... 进行比较,这时没有优劣之分。
  • isValid 中传入声明中带有约束的函数对象,可以构造检查特定约束的函数对象。

x 的类型是 TypeT,所以使用前需要使用 valueT 来提取其包含的真实类型。上图代码传入的 lambda 检查了特定类型是否能够默认构造。现在可以这样使用:

不过还需要用户显式调用使用 type 模板。接下来可以构造 alias,使得用法更贴近标准库:

template<typename T>
using IsDefaultConstructibleT = decltype(isDefaultConstructible(std::declval<T>()));

构造 alias 还解决了参数数量检查的问题。isValid 装饰器返回的函数对象是接受多个参数的。我们提供的约束本身也可能会需要多个参数,所以从这一点上看没有问题。但是如果参数数量不对,编译并不会报错,而是会因为 SFINAE 而得到 std::false_type。如果还需要对参数数量做严格检查,则只应该直接调用 IsDefaultConstructibleT,而不是上面的 isDefaultConstrutible