目录
19.6 探测成员(Detecting Members)
19.6.1 探测类型成员(Detecting Member Types)
处理引用类型
注入类的名字(Injected Class Names)
19.6.2 探测任意类型成员
19.6.3 探测非类型成员
探测成员函数
探测其它的表达式
19.6.4 用泛型 Lambda 探测成员
参考:https://github.com/Walton1128/CPP-Templates-2nd--
另一种对基于 SFINAE 的萃取的应用是,创建一个可以判断一个给定类型 T 是否含有名为 X 的成员(类型或者非类型成员)的萃取。
#include
// defines true_type and false_type
// helper to ignore any number of template parameters:
template using VoidT = void;
// primary template:
template>
struct HasSizeTypeT : std::false_type
{};
// partial specialization (may be SFINAE’d away):
template
struct HasSizeTypeT> : std::true_type
{} ;
需要注意的是,如果类型成员 size_type 是 private 的,HasSizeTypeT 会返回 false,因为我们 的萃取模板并没有访问该类型的特殊权限,因此 typename T::size_type 是无效的(触发 SFINAE)。也就是说,该萃取所做的事情是测试我们是否能够访问类型成员 size_type。
HasSizeTypeT 一类的萃取,在处理引用类型的时候可能会遇到让人意外的事情。
struct CXR {
using size_type = char&; // Note: type size_type is a reference type
};
std::cout << HasSizeTypeT::value; // OK: prints true
但是与之类似的代码却不会输出我们所期望的结果:
std::cout << HasSizeTypeT::value; // OOPS: prints false
std::cout << HasSizeTypeT::value; // OOPS: prints false
这或许会让人感到意外。引用类型确实没有成员,
可以在 HasSizeTypeT 的偏特化中使 用我们之前介绍的 RemoveReference 萃取:
template
struct HasSizeTypeT::size_type>> :
std::true_type {
};
首先参考第十三章:
::
)或是成员访问操作符(.
或->
)显式指定,我们就称该名称为限定名称(qualified name)。例如,this->count
是一个限定名称,但是count
本身则不是(尽管字面上count
实际上指代的也是一个类成员)。13.2.3 注入的类名称:类的名称会被注入到类本身的作用域中,因此在该作用域中作为非限定名称可访问。(然而,它作为限定名称不可访问,因为这种符号表示用于表示构造函数。)
同样值得注意的是,对于注入类的名字(参见第 13.2.3 节),我们上述检测类型成员的萃取 也会返回 true。比如对于:
struct size_type {
};
struct Sizeable : size_type {
};
static_assert(HasSizeTypeT::value, "Compiler bug: Injected
class name missing");
后面的 static_assert 会成功,因为 size_type 会将其自身的名字当作类型成员,而且这一成员 会被继承。如果 static_assert 不会成功的话,那么我就发现了一个编译器的问题。
在定义了诸如 HasSizeTypeT 的萃取之后,我们会很自然的想到该如何将该萃取参数化,以对 任意名称的类型成员做探测。 不幸的是,目前这一功能只能通过宏来实现,因为还没有语言机制可以被用来描述“潜在” 的名字。当前不使用宏的、与该功能最接近的方法是使用泛型 lambda,正如在第 19.6.4 节 介绍的那样。
#include // for true_type, false_type, and void_t
#define
DEFINE_HAS_TYPE(MemType) \
template> \
struct HasTypeT_##MemType \
: std::false_type {
}; \
template \
struct HasTypeT_##MemType> \
: std::true_type { } // ; intentionally skipped
每 一 次 对 DEFINE_HAS_TYPE(MemberType) 的 使 用 都 相 当 于 定 义 了 一 个 新 的 HasTypeT_MemberType 萃取。比如,我们可以用之来探测一个类型是否有 value_type 或者 char_type 类型成员:
#include "hastype.hpp"
#include
#include
DEFINE_HAS_TYPE(value_type);
DEFINE_HAS_TYPE(char_type);
int main()
{
std::cout << "int::value_type: " << HasTypeT_value_type::value
<< ’\n’;
std::cout << "std::vector::value_type: " <<
HasTypeT_value_type>::value << ’\n’;
std::cout << "std::iostream::value_type: " <<
HasTypeT_value_type::value << ’\n’;
std::cout << "std::iostream::char_type: " <<
HasTypeT_char_type::value << ’\n’;
}
可以继续修改上述萃取,以让其能够测试数据成员和(单个的)成员函数:
#include // for true_type, false_type, and void_t
#define
DEFINE_HAS_MEMBER(Member) \
template> \
struct HasMemberT_##Member \
: std::false_type { }; \
template \
struct HasMemberT_##Member> \
: std::true_type { } // ; intentionally skipped
当&::Member 无效的时候,偏特化实现会被 SFINAE 掉。为了使条件有效,必须满足如下条 件: Member 必须能够被用来没有歧义的识别出 T 的一个成员(比如,它不能是重载成员你 函数的名字,也不能是多重继承中名字相同的成员的名字)。
成员必须可以被访问。
成员必须是非类型成员以及非枚举成员(否则前面的&会无效)。
如果 T::Member 是 static 的数据成员,那么与其对应的类型必须没有提供使得 &T::Member 无效的 operator&(比如,将 operator&设成不可访问的)
注意,HasMember 萃取只可以被用来测试是否存在“唯一”一个与特定名称对应的成员。 如果存在两个同名的成员的话,该测试也会失败,比如当我们测试某些重载成员函数是否存 在的时候:
DEFINE_HAS_MEMBER(begin);
std::cout << HasMemberT_begin>::value; // false
但是,正如在第 8.4.1 节所说的那样,SFINAE 会确保我们不会在函数模板声明中创建非法的 类型和表达式,从而我们可以使用重载技术进一步测试某个表达式是否是病态的。 也就是说,可以很简单地测试我们能否按照某种形式调用我们所感兴趣的函数,即使该函数 被重载了,相关调用可以成功。正如在第 19.5 节介绍的 IsConvertibleT 一样,此处的关键是 能否构造一个表达式,以测试我们能否在 decltype 中调用 begin(),并将该表达式用作额外 的模板参数的默认值:
#include // for declval
#include // for true_type, false_type, and void_t
// primary template:
template>
struct HasBeginT : std::false_type {
};
// partial specialization (may be SFINAE’d away):
template
struct HasBeginT
().begin())>> : std::true_type {
};
这里我们使用 decltype(std::declval ().begin())来测试是否能够调用 T 的 begin()。
相同的技术还可以被用于其它的表达式,甚至是多个表达式的组合。比如,我们可以测试对 类型为 T1 和 T2 的对象,是否有合适的<运算符可用:
#include // for declval
#include // for true_type, false_type, and void_t
// primary template:
template>
struct HasLessT : std::false_type
{};
// partial specialization (may be SFINAE’d away):
template
struct HasLessT() <
std::declval())>>: std::true_type
{};
和往常一样,问题的难点在于该如何为所要测试的条件定义一个有效的表达式,并通过使用 decltype 将其放入 SFINAE 的上下文中,在该表达式无效的时候,SFINAE 机制会让我们最终 选择主模板:
decltype(std::declval() < std::declval())
采用这种方式探测表达式有效性的萃取是很稳健的:如果表达式没有问题,它会返回 true, 而如果<运算符有歧义,被删除,或者不可访问的话,它也可以准确的返回 false。
正如在第 2.3.1 节介绍的那样,我们也可以通过使用该萃取去要求模板参数 T 必须要支持< 运算符:
template
class C
{
static_assert(HasLessT::value, "Class C requires comparable
elements"); …
};
值得注意的是,基于 std::void_t 的特性,我们可以将多个限制条件放在同一个萃取中:
#include // for declval
#include // for true_type, false_type, and void_t
// primary template:
template>
struct HasVariousT : std::false_type
{};
// partial specialization (may be SFINAE’d away):
template
struct HasVariousT ().begin()),
typename T::difference_type,
typename T::iterator>> :
std::true_type
{};
下面这个例子展示了定义可以检测数据或者类型成员是否存在(比如 first 或者 size_type), 或者有没有为两个不同类型的对象定义 operator <的萃取的方式:
#include "isvalid.hpp"
#include
#include
#include
int main()
{
using namespace std;
cout << boolalpha;
// define to check for data member first:
constexpr auto hasFirst = isValid([](auto x) ->
decltype((void)valueT(x).first) {});
cout << "hasFirst: " << hasFirst(type>) << '\n'; // true
// define to check for member type size_type:
constexpr auto hasSizeType = isValid([](auto x) -> typename
decltype(valueT(x))::size_type{ });
struct CX {
using size_type = std::size_t;
};
cout << "hasSizeType: " << hasSizeType(type) << '\n'; // true
if constexpr (!hasSizeType(type)) {
cout << "int has no size_type\n";
}
// define to check for <:
constexpr auto hasLess = isValid([](auto x, auto y) ->
decltype(valueT(x) < valueT(y)) {});
cout << hasLess(42, type) << '\n'; //yields true
cout << hasLess(type, type) << '\n'; //yields true
cout << hasLess(type, type) << '\n'; //yields false
cout << hasLess(type, "hello") << '\n'; //yields true
}
这里再次回顾#include "isvalid.hpp"的内容:
inline constexpr
auto isValid = [](auto f) {
return [](auto&&... args) {
return decltype(isValidImpl(nullptr)){};
};
};
isValid 接受一个闭包函数A作为参数,返回一个闭包函数B,返回的闭包函数B接受的参数个数不固定,
实际使用过程中,
constexpr auto hasLess = isValid([](auto x, auto y) ->
decltype(valueT(x) < valueT(y)) {});
cout << hasLess(42, type) << '\n'; //yields true
hasless为返回的函数B,这个函数接受两个参数,返回decltype(isValidImpl<>(nullptr) ){};利用{}初始化返回一个临时对象。
auto&& 是万能引用 不能用auto& 这样只接受左值参数不能绑定到右值。
---------------------------
请再次注意,hasSizeType 通过使用 std::decay 将参数 x 中的引用删除了,因为我们不能访问 引用中的类型成员。如果不这么做,该萃取(对于引用类型)会始终返回 false,从而导致 第二个重载的 isValidImpl<>被使用。
为了能够使用统一的泛型语法(将类型用于模板参数),我们可以继续定义额外的辅助工具。 比如:
#include "isvalid.hpp"
#include
#include
#include
constexpr auto hasFirst
= isValid([](auto&& x) -> decltype((void)&x.first) {});
template
using HasFirstT = decltype(hasFirst(std::declval()));
constexpr auto hasSizeType = isValid([](auto&& x) -> typename
std::decay_t::size_type {});
template
using HasSizeTypeT = decltype(hasSizeType(std::declval()));
constexpr auto hasLess = isValid([](auto&& x, auto&& y) -> decltype(x
< y) { });
template
using HasLessT = decltype(hasLess(std::declval(),
std::declval()));
int main()
{
using namespace std;
cout << "first: " << HasFirstT>::value << ’\n’;
// true
struct CX {
using size_type = std::size_t;
};
cout << "size_type: " << HasSizeTypeT::value << ’\n’; // true
cout << "size_type: " << HasSizeTypeT::value << ’\n’; // false
cout << HasLessT::value << ’\n’; // true
cout << HasLessT::value << ’\n’; // true
cout << HasLessT::value << ’\n’; // false
cout << HasLessT::value << ’\n’; // true
}
现在可以像下面这样使用 HasFirstT:
HasFirstT>::value
它会为一个包含两个 int 的 pair 调用 hasFirst,其行为和之前的讨论一致。
关于这里decltype为什么要有void:
constexpr auto hasFirst
= isValid([](auto&& x) -> decltype((void)&x.first) {});
一种说法是decltype必须接受表达式(decltype需要一个表达式,而不是一个类型。void()在这里实际上不是一个类型,而是一个表达式 c++ - What does the void() in decltype(void()) mean exactly? - Stack Overflow)
但是decltype specifier - cppreference.com
表明decltype可以接受实体。
Syntax
decltype ( entity ) (1) (since C++11)
decltype ( expression ) (2) (since C++11)
另一种说法是“' (void) '强制转换通常用于显式丢弃表达式的值。它告诉编译器不要生成任何使用或计算' x.first '值的代码。当您只对表达式的类型而不是其值感兴趣时,这很有用。”来自gpt。
这是一种执行基于类型的检查而不是基于值的检查的方法。