目录
19.4 基于 SFINAE 的萃取(SFINAE-Based Traits)
19.4.3 将泛型 Lambdas 用于 SFINAE(Using Generic Lambdas for SFINAE)
参考:https://github.com/Walton1128/CPP-Templates-2nd--
基于 SFINAE 的两个主要技术是:用 SFINAE 排除某些重载函数,以及用 SFINAE 排除某些偏 特化。
将 SFINAE 用于函数重载,以判断一个类型是否是 默认可构造的,对于可以默认构造的类型,就可以不通过值初始化来创建对象。也就是说, 对于类型 T,诸如 T()的表达式必须是有效的。
通过函数重载实现一个基于 SFINAE 的萃取的常规方式是声明两个返回值类型不同的同名 (test())重载函数模板:
- template<…> static char test(void*);
- template<…> static long test(…);
第一个重载函数只有在所需的检查成功时才会被匹配到(后文会讨论其实现方式)。第二个 重载函数是用来应急的:它会匹配任意调用,但是由于它是通过”...”(省略号)进行匹配的, 因此其它任何匹配的优先级都比它高
返回值 value 的具体值取决于最终选择了哪一个 test 函数:
- static constexpr bool value
- = IsSameT<decltype(test<…>(nullptr)), char>::value;
如果选择的是第一个 test()函数,由于其返回值类型是 char,value 会被初始化伟 isSame,也就是 true。否则,value 会被初始化伟 isSame,也就是 false。
- #include "issame.hpp"
- template<typename T>
- struct IsDefaultConstructibleT {
- private:
- // test() trying substitute call of a default constructor for
- //T passed as U :
- template<typename U, typename = decltype(U())>
- static char test(void*);// test() fallback:
- template<typename>
- static long test(…);
- public:
- static constexpr bool value =
- IsSameT<decltype(test
(nullptr)), char>::value; - };
现在,到了该处理我们所需要检测的属性的时候了。目标是只有当我们所关心的测试条件被 满足的时候,才可以使第一个 test()有效。在这个例子中,我们想要测试的条件是被传递进 来的类型 T 是否是可以被默认构造的。
为了实现这一目的,我们将 T 传递给 U,并给第一个 test()声明增加一个无名的(dummy)模板参数,该模板参数被一个只有在这一转换有效的 情况下才有效的构造函数进行初始化。
在这个例子中,我们使用的是只有当存在隐式或者显 式的默认构造函数 U()时才有效的表达式。我们对 U()的结果施加了 deltype 操作,这样就可 以用其结果初始化一个类型参数了。
第二个模板参数不可以被推断,因为我们不会为之传递任何参数。而且我们也不会为之提供 显式的模板参数。因此,它只会被替换,如果替换失败,基于 SFINAE,相应的 test()声明会 被丢弃掉,因此也就只有应急方案可以匹配相应的调用。
但是需要注意,我们不能在第一个 test()声明里直接使用模板参数 T:
- template<typename T>
- struct IsDefaultConstructibleT {
- private:
- // ERROR: test() uses T directly:
- template<typename, typename = decltype(T())>
- static char test(void*);
- // test() fallback:
- template<typename>
- static long test(…);
- public:
- static constexpr bool value
- = IsSameT<decltype(test
(nullptr)), char>::value; - };
个人理解:
首先根据第十四章14.1 On-Demand实例化:
与普通类的情况一样,如果你声明的是一个指向某种类型的指针或引用(#2处的声明),那么在声明的作用域中,你并不需要看到该类模板的定义。例如,声明函数g的参数类型并不需要模板C的完整定义。然而,一旦某个组件需要知道模板特化体的大小或是访问了该特化体的成员,那么就需要看到完整的类模板定义。这就解释了为什么#6处必须看到类模板的定义。
类模板内的模板函数也是用到了才会被实例化。
- struct S {
- S() = delete;
- };
- IsDefaultConstructibleT
::value //yields false
1.对于编译出错的情况来说:
当进行到IsDefaultConstructibleT::value.。时,会把类模板IsDefaultConstructibleT里的T变成S。
- struct IsDefaultConstructibleT {
- private:
-
- template<typename, typename = decltype(S())>
- static char test(void*);// test() fallback:
- template<typename>
- static long test(…);
- public:
- static constexpr bool value =
- IsSameT<decltype(test
(nullptr)), char>::value; - };
此时对于 template
是进行类 IsDefaultConstructibleT 的定义,不在模板替换的立即上下文中(参考第十五章15.7.1 立即上下文),所以会编译出错。
2.对于执行SFINAE的情况来说:
当进行到IsDefaultConstructibleT::value.。时,IsDefaultConstructibleT里的T变成S。
- struct IsDefaultConstructibleT {
- private:
-
- template<typename U, typename = decltype(U())>
- static char test(void*);// test() fallback:
- template<typename>
- static long test(…);
- public:
- static constexpr bool value =
- IsSameT<decltype(test
(nullptr)), char>::value; - };
然后再进行到
static constexpr bool value =
IsSameT
时。会把函数模板test<>里的U替换成S:
- struct IsDefaultConstructibleT {
- private:
-
- template<typename S, typename = decltype(S())>
- static char test(void*);// test() fallback:
- template<typename>
- static long test(…);
- public:
- static constexpr bool value =
- IsSameT<decltype(test
(nullptr)), char>::value; - };
这时 template
在最早的实现技术中,会基于返回值类型的大小来判断使用了哪一个重载函数(也会 用到 0 和 enum,因为在当时 nullptr 和 constexpr 还没有被引入)
Making SFINAE-based Traits Predicate Traits 将基于SFINAE的萃取预测萃取
正如在第 19.3.3 节介绍的那样,返回 bool 值的萃取,应该返回一个继承自 std::true_type 或 者 std::false_type 的值。使用这一方式,同样可以解决在某些平台上 sizeof(char) == sizeof(long) 的问题。
为了这一目的,我们需要间接定义一个 IsDefaultConstructibleT。该萃取本身需要继承自一个 辅助类的 Type 成员,该辅助类会返回所需的基类。
优化之后,完整的 IsDefaultConstructibleT 的实现如下:
- #include
- template<typename T>
- struct IsDefaultConstructibleHelper {
- private:
- // test() trying substitute call of a default constructor for
- T passed as U:
- template<typename U, typename = decltype(U())>
- static std::true_type test(void*);
- // test() fallback:
- template<typename>
- static std::false_type test(…);
- public:
- using Type = decltype(test
(nullptr)); - };
- template<typename T>
- struct IsDefaultConstructibleT :
- IsDefaultConstructibleHelper
::Type { - };
此时我们也不再需要使用 IsSameT 萃取了。
现在,如果第一个 test()函数模板是有效的话,那么它就将是被选择的重载函数,因此成员 IsDefaultConstructibleHelper::Type 会 被 其 返 回 值 类 型 std::true_type 初 始 化 。 这 样 的 话 IsConvertibleT<>就会继承自 std::true_type。 如 果 第 一 个 test() 函 数 模 板 是 无 效 的 话 , 那 么 它 就 会 被 SFINAE 剔 除 掉 , IsDefaultConstructibleHelper::Type 也 就 会 被 应 急 test() 的 返 回 值 类 型 初 始 化 , 也 就 是 std::false_type。
另一种实现基于 SFINAE 的萃取的方式会用到偏特化。这里,我们同样可以使用上文中用来 判断类型 T 是否是可以被默认初始化的例子:
- #include "issame.hpp"
- #include
//defines true_type and false_type - // 别名模板,helper to ignore any number of template parameters:
- template<typename …> using VoidT = void;
- // primary template:
- template<typename, typename = VoidT<>>
- struct IsDefaultConstructibleT : std::false_type
- { };
- // partial specialization (may be SFINAE’d away):
- template<typename T>
- struct IsDefaultConstructibleT
decltype(T())>> : - std::true_type
- { }
- ;
此处一个比较有意思的地方是,第二个模板参数的默认值被设定为一个辅助别名模板 VoidT。 这使得我们能够定义各种使用了任意数量的编译期类型构造的偏特化。
很显然,这一定义类型萃取的方法看上去要比之前介绍的使用了函数模板重载的方法精简的 多。但是该方法要求要能够将相应的条件放进模板参数的声明中。而使用了函数重载的类模板则使得我们能够使用额外的辅助函数或者辅助类。
无论使用哪一种技术,在定义萃取的时候总是需要用到一些样板代码:重载并调用两个 test() 成员函数,或者实现多个偏特化。接下来我们会展示在 C++17 中,如何通过指定一个泛型 lambda 来做条件测试,将样板代码的数量最小化。
- #include
- // helper: checking validity of f (args…) for F f and Args… args:
- template<typename F, typename… Args,
- typename = decltype(std::declval
() (std::declval()…))> - 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) {
- return [](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
{}; - // helper to unwrap a wrapped type in unevaluated contexts
-
- template<typename T>
- T valueT(TypeT
) ; // no definition needed
在深入讨论内部的 lambda 表达式之前,先来看一个 isValid 的典型用法:
- constexpr auto isDefaultConstructible
- = isValid([](auto x) -> decltype((void)decltype(valueT(x))() {})
可以像下面这样使用 isDefaultConstructible:
- isDefaultConstructible(type<int>) //true (int is
- defaultconstructible)
- isDefaultConstructible(type<int&>) //false (references are not
- default-constructible)
为 了 理 解 各 个 部 分 是 如 何 工 作 的 , 先 来 看 看 当 isValid 的 参 数 f 被 绑 定 到 isDefaultConstructible 的泛型 lambda 参数时,isValid 内部的 lambda 表达式会变成什 么样子。通过对 isValid 的定义进行替换,我们得到如下等价代码:
- constexpr auto isDefaultConstructible= [](auto&&… args) {
- return decltype(isValidImpl<decltype([](auto x) ->
- decltype((void)decltype(valueT(x))())),
- decltype(args)&&…> (nullptr)){};
- };
为了使 SFINAE 能够工作,替换必须发生在被替换模板的立即上下文(immediate context,参见第 15.7.1 节)中。在我们这个例子中,被替换的模板是 isValidImpl 的第一个声 明,而且泛型 lambda 的调用运算符被传递给了 isValid。因此,被测试的内容必须出现在 lambda 的返回类型中,而不是函数体中。
到目前为止,这一技术看上去好像并没有那么有竞争力,因为无论是实现中涉及的表达式还 是其使用方式都要比之前的技术复杂得多。但是,一旦有了 isValid,并且对其进行了很好的 理解,有很多萃取都可以只用一个声明实现。比如,对是否能够访问名为 first 的成员进行 测试,就非常简洁(完整的例子请参见 19.6.4 节):
- constexpr auto hasFirst
- = isValid([](auto x) -> decltype((void)valueT(x).first) {});
作为一般的设计原则,在给定了合理的模板参数的情况下,萃取模板永远不应该在实例化阶 段出错。而一般的做法往往是进行两次相应的检查:
1. 一次是检查相关操作是否有效
2. 一次是计算其结果
让我们将这一原则用于在第 19.3.1 节介绍的 ElementT:它从一个容器类型生成该容器的元 素类型。同样的,由于其结果依赖于该容器类型所包含的成员类型 value_type,因此主模板 应该只有在容器类型包含 value_type 成员的时候,才去定义成员类型 Type:
- template<typename C, bool = HasMemberT_value_type
::value> - struct ElementT {
- using Type = typename C::value_type;
- };
- template<typename C>
- struct ElementT
false> { - };
我们将定义一个能够判断一种类型是否可以被转化成另外一种类型的萃取,比如当我 们期望某个基类或者其某一个子类作为参数的时候。IsConvertibleT 就可以判断其第一个类型 参数是否可以被转换成第二个类型参数:
- #include
// for true_type and false_type - #include
// for declval - template<typename FROM, typename TO>
- struct IsConvertibleHelper {
- private:
- // test() trying to call the helper aux(TO) for a FROM passed as F :
- static void aux(TO);
- template<typename F, typename T,
- typename = decltype(aux(std::declval
()))> - static std::true_type test(void*);
- // test() fallback:
- template<typename, typename>
- static std::false_type test(…);
- public:
- using Type = decltype(test
(nullptr)); - };
- template<typename FROM, typename TO>
- struct IsConvertibleT : IsConvertibleHelper
::Type { - };
- template<typename FROM, typename TO>
- using IsConvertible = typename IsConvertibleT
::Type; - template<typename FROM, typename TO>
- constexpr bool isConvertible = IsConvertibleT
::value;
请注意这里是如何在不调用任何构造函数的情况下,通过使用在第 19.3.4 节介绍的 std::declval 生成一个类型的值的。如果这个值可以被转换成 TO,对 aux()的调用就是有效的, 相应的 test()调用也就会被匹配到。否则,会触发 SFINAE 错误,导致应急 test()被调用。
然后,我们就可以像下面这样使用该萃取了:
- IsConvertibleT<int, int>::value //yields true
- IsConvertibleT<int, std::string>::value //yields false
- IsConvertibleT<char const*, std::string>::value //yields true
- IsConvertibleT
char const*>::value //yields false
下面 3 种情况还不能被上面的 IsConvertibleT 正确处理:
1. 向数组类型的转换要始终返回 false,但是在上面的代码中,aux()声明中的类型为 TO 的 参数会退化成指针类型,因此对于某些 FROM 类型,它会返回 true。
2. 向指针类型的转换也应该始终返回 false,但是和 1 中的情况一样,上述实现只会将它 们当作退化后的类型。
3. 向(被 const/volatile 修饰)的 void 类型的转换需要返回 true。但是不幸的是,在 TO 是void 的时候,上述实现甚至不能被正确实例化,因为参数类型不能包含 void 类型(而 且 aux()的定义也用到了这一参数) void 类型无法实例化或传递给模板函数。
对于这几种情况,我们需要对它们进行额外的偏特化。但是,为所有可能的与 const 以及 volatile 的组合情况都分别进行偏特化是很不明智的。相反,我们为辅助类模板引入了一个 额外的模板参数:
- template<typename FROM, typename TO, bool = IsVoidT
::value || - IsArrayT
::value || IsFunctionT::value> - struct IsConvertibleHelper {
- using Type = std::integral_constant<bool, IsVoidT
::value && - IsVoidT
::value>; - };
- template<typename FROM, typename TO>
- struct IsConvertibleHelper
false> { … //previous implementation of IsConvertibleHelper here - };
至于 IsArrayT 和 IsFunctionT 的实现,请分别参见第 19.8.2 节和第 19.8.3 节。
C++标准库中也提供了与之对应的 std::is_convertible<>,具体请参见第 D.3.3 节