CPP-Templates-2nd--第十九章 萃取的实现 19.4-19.5

 

目录

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-- 

19.6 探测成员(Detecting Members)

另一种对基于 SFINAE 的萃取的应用是,创建一个可以判断一个给定类型 T 是否含有名为 X 的成员(类型或者非类型成员)的萃取。

19.6.1 探测类型成员(Detecting Member Types)
#include <type_traits>
// 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 HasSizeTypeT : std::false_type
{};
// partial specialization (may be SFINAE’d away):
template<typename T>
struct HasSizeTypeT<T, VoidT<typename T::size_type>> : 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<CXR>::value; // OK: prints true

但是与之类似的代码却不会输出我们所期望的结果:

std::cout << HasSizeTypeT<CXR&>::value; // OOPS: prints false
std::cout << HasSizeTypeT<CXR&&>::value; // OOPS: prints false

这或许会让人感到意外。引用类型确实没有成员

可以在 HasSizeTypeT 的偏特化中使 用我们之前介绍的 RemoveReference 萃取:

template<typename T>
struct HasSizeTypeT<T, VoidT<RemoveReference<T>::size_type>> :
std::true_type {
};
注入类的名字(Injected Class Names)

首先参考第十三章:

  1. 如果名称的作用域由域操作符(::)或是成员访问操作符(.->)显式指定,我们就称该名称为限定名称(qualified name)。例如,this->count是一个限定名称,但是count本身则不是(尽管字面上count实际上指代的也是一个类成员)。

13.2.3 注入的类名称:类的名称会被注入到类本身的作用域中,因此在该作用域中作为非限定名称可访问。(然而,它作为限定名称不可访问,因为这种符号表示用于表示构造函数。) 

同样值得注意的是,对于注入类的名字(参见第 13.2.3 节),我们上述检测类型成员的萃取 也会返回 true。比如对于:

struct size_type {
};
struct Sizeable : size_type {
};
static_assert(HasSizeTypeT<Sizeable>::value, "Compiler bug: Injected
class name missing");

后面的 static_assert 会成功,因为 size_type 会将其自身的名字当作类型成员,而且这一成员 会被继承。如果 static_assert 不会成功的话,那么我就发现了一个编译器的问题。

19.6.2 探测任意类型成员

在定义了诸如 HasSizeTypeT 的萃取之后,我们会很自然的想到该如何将该萃取参数化,以对 任意名称的类型成员做探测。 不幸的是,目前这一功能只能通过宏来实现,因为还没有语言机制可以被用来描述“潜在” 的名字。当前不使用宏的、与该功能最接近的方法是使用泛型 lambda,正如在第 19.6.4 节 介绍的那样。

#include <type_traits> // for true_type, false_type, and void_t
#define
DEFINE_HAS_TYPE(MemType) \
template<typename, typename = std::void_t<>> \
struct HasTypeT_##MemType \
: std::false_type {
}; \
template<typename T> \
struct HasTypeT_##MemType<T, std::void_t<typename T::MemType>> \
: std::true_type { } // ; intentionally skipped

每 一 次 对 DEFINE_HAS_TYPE(MemberType) 的 使 用 都 相 当 于 定 义 了 一 个 新 的 HasTypeT_MemberType 萃取。比如,我们可以用之来探测一个类型是否有 value_type 或者 char_type 类型成员:

#include "hastype.hpp"
#include <iostream>
#include <vector>
DEFINE_HAS_TYPE(value_type);
DEFINE_HAS_TYPE(char_type);
int main()
{
std::cout << "int::value_type: " << HasTypeT_value_type<int>::value
<< ’\n’;
std::cout << "std::vector<int>::value_type: " <<
HasTypeT_value_type<std::vector<int>>::value << ’\n’;
std::cout << "std::iostream::value_type: " <<
HasTypeT_value_type<std::iostream>::value << ’\n’;
std::cout << "std::iostream::char_type: " <<
HasTypeT_char_type<std::iostream>::value << ’\n’;
}
19.6.3 探测非类型成员

可以继续修改上述萃取,以让其能够测试数据成员和(单个的)成员函数:

#include <type_traits> // for true_type, false_type, and void_t
#define
DEFINE_HAS_MEMBER(Member) \
template<typename, typename = std::void_t<>> \
struct HasMemberT_##Member \
: std::false_type { }; \
template<typename T> \
struct HasMemberT_##Member<T,
std::void_t<decltype(&T::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<std::vector<int>>::value; // false

但是,正如在第 8.4.1 节所说的那样,SFINAE 会确保我们不会在函数模板声明中创建非法的 类型和表达式,从而我们可以使用重载技术进一步测试某个表达式是否是病态的。 也就是说,可以很简单地测试我们能否按照某种形式调用我们所感兴趣的函数,即使该函数 被重载了,相关调用可以成功。正如在第 19.5 节介绍的 IsConvertibleT 一样,此处的关键是 能否构造一个表达式,以测试我们能否在 decltype 中调用 begin(),并将该表达式用作额外 的模板参数的默认值:

#include <utility> // for declval
#include <type_traits> // for true_type, false_type, and void_t
// primary template:
template<typename, typename = std::void_t<>>
struct HasBeginT : std::false_type {
};
// partial specialization (may be SFINAE’d away):
template<typename T>
struct HasBeginT<T, std::void_t<decltype(std::declval<T>
().begin())>> : std::true_type {
};

这里我们使用 decltype(std::declval ().begin())来测试是否能够调用 T 的 begin()。

探测其它的表达式

相同的技术还可以被用于其它的表达式,甚至是多个表达式的组合。比如,我们可以测试对 类型为 T1 和 T2 的对象,是否有合适的<运算符可用:

#include <utility> // for declval
#include <type_traits> // for true_type, false_type, and void_t
// primary template:
template<typename, typename, typename = std::void_t<>>
struct HasLessT : std::false_type
{};
// partial specialization (may be SFINAE’d away):
template<typename T1, typename T2>
struct HasLessT<T1, T2, std::void_t<decltype(std::declval<T1>() <
std::declval<T2>())>>: std::true_type
{};

和往常一样,问题的难点在于该如何为所要测试的条件定义一个有效的表达式,并通过使用 decltype 将其放入 SFINAE 的上下文中,在该表达式无效的时候,SFINAE 机制会让我们最终  选择主模板:

decltype(std::declval() < std::declval())

采用这种方式探测表达式有效性的萃取是很稳健的:如果表达式没有问题,它会返回 true, 而如果<运算符有歧义,被删除,或者不可访问的话,它也可以准确的返回 false。

正如在第 2.3.1 节介绍的那样,我们也可以通过使用该萃取去要求模板参数 T 必须要支持< 运算符:

template<typename T>
class C
{
static_assert(HasLessT<T>::value, "Class C requires comparable
elements"); …
};

值得注意的是,基于 std::void_t 的特性,我们可以将多个限制条件放在同一个萃取中:

#include <utility> // for declval
#include <type_traits> // for true_type, false_type, and void_t
// primary template:
template<typename, typename = std::void_t<>>
struct HasVariousT : std::false_type
{};
// partial specialization (may be SFINAE’d away):
template<typename T>
struct HasVariousT<T, std::void_t<decltype(
std::declval<T> ().begin()),
typename T::difference_type,
typename T::iterator>> :
std::true_type
{};
19.6.4 用泛型 Lambda 探测成员

下面这个例子展示了定义可以检测数据或者类型成员是否存在(比如 first 或者 size_type), 或者有没有为两个不同类型的对象定义 operator <的萃取的方式:

#include "isvalid.hpp"
#include<iostream>
#include<string>
#include<utility>
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<pair<int, int>>) << '\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<CX>) << '\n'; // true
	if constexpr (!hasSizeType(type<int>)) {
		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<char>) << '\n'; //yields true
	cout << hasLess(type<string>, type<string>) << '\n'; //yields true
	cout << hasLess(type<string>, type<int>) << '\n'; //yields false
	cout << hasLess(type<string>, "hello") << '\n'; //yields true
}

这里再次回顾#include "isvalid.hpp"的内容:

inline constexpr
auto isValid = [](auto f) {
    return [](auto&&... args) {
        return decltype(isValidImpl<decltype(f),
            decltype(args) &&...>(nullptr)){};
    };
};

 isValid 接受一个闭包函数A作为参数,返回一个闭包函数B,返回的闭包函数B接受的参数个数不固定,

实际使用过程中,

constexpr auto hasLess = isValid([](auto x, auto y) ->
        decltype(valueT(x) < valueT(y)) {});
    cout << hasLess(42, type<char>) << '\n'; //yields true

 hasless为返回的函数B,这个函数接受两个参数,返回decltype(isValidImpl<>(nullptr) ){};利用{}初始化返回一个临时对象。

auto&& 是万能引用 不能用auto& 这样只接受左值参数不能绑定到右值。

---------------------------

请再次注意,hasSizeType 通过使用 std::decay 将参数 x 中的引用删除了,因为我们不能访问 引用中的类型成员。如果不这么做,该萃取(对于引用类型)会始终返回 false,从而导致 第二个重载的 isValidImpl<>被使用。

为了能够使用统一的泛型语法(将类型用于模板参数),我们可以继续定义额外的辅助工具。 比如:

#include "isvalid.hpp"
#include<iostream>
#include<string>
#include<utility>
constexpr auto hasFirst
= isValid([](auto&& x) -> decltype((void)&x.first) {});
template<typename T>
using HasFirstT = decltype(hasFirst(std::declval<T>()));
constexpr auto hasSizeType = isValid([](auto&& x) -> typename
std::decay_t<decltype(x)>::size_type {});
template<typename T>
using HasSizeTypeT = decltype(hasSizeType(std::declval<T>()));
constexpr auto hasLess = isValid([](auto&& x, auto&& y) -> decltype(x
< y) { });
template<typename T1, typename T2>
using HasLessT = decltype(hasLess(std::declval<T1>(),
std::declval<T2>()));
int main()
{
using namespace std;
cout << "first: " << HasFirstT<pair<int,int>>::value << ’\n’;
// true
struct CX {
using size_type = std::size_t;
};
cout << "size_type: " << HasSizeTypeT<CX>::value << ’\n’; // true
cout << "size_type: " << HasSizeTypeT<int>::value << ’\n’; // false

cout << HasLessT<int, char>::value << ’\n’; // true
cout << HasLessT<string, string>::value << ’\n’; // true
cout << HasLessT<string, int>::value << ’\n’; // false
cout << HasLessT<string, char*>::value << ’\n’; // true
}

现在可以像下面这样使用 HasFirstT: 

HasFirstT<std::pair<int,int>>::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。

这是一种执行基于类型的检查而不是基于值的检查的方法。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值