现代C++语言核心特性解析part22

第41章 概念和约束(C++20)

41.1 使用std::enable_if约束模板

标准库中的std::enable_if模板元函数,SFINAE规则使该模板元函数能辅助模板的开发者限定实例化模板的模板实参类型

template <class T, class U = std::enable_if_t<std::is_integral_v<T>>>
struct X {};
X<int> x1; // 编译成功
X<std::string> x2; // 编译失败

类模板X的模板形参class U =std::enable_if_t <std::is_integral_v< T>>只是作为一个约束条件存在,当T的类型为整型时,std::is_integral_v < T>返回true,于是std::enable_if_t<std::is_integral_v< T>>返回类型void,所以X< int>实际上是X<int,void>的一个合法类型。反之,对于X< std::string>来说,T的类型不为整型,std:: enable_if不存在嵌套类型type,于是std::enable_if_t<std::is_ integral_v< T>>无法符合语法规范,导致编译失败。
以下是enable_if的一种实现方法:

template<bool B, class T = void>
struct enable_if {};
template<class T>
struct enable_if<true, T> { using type = T; };

41.2 概念的背景介绍

概念是对C++核心语言特性中模板功能的扩展。它在编译时进行评估,对类模板、函数模板以及类模板的成员函数进行约束:它限制了能被接受为模板形参的实参集。
在C++20中,上一节的例子可以改写为:

template <class C>
concept IntegerType = std::is_integral_v<C>;
template <IntegerType T>
struct X {};

这份代码还可以简化为:

template <class T>
requires std::is_integral_v<T>
struct X {};

41.3 使用concept和约束表达式定义概念

我们可以使用concept关键字来定义概念

template <class C>
concept IntegerType = std::is_integral_v<C>;

其中IntegerType是概念名,这里的std::is_integral_v< C>称为约束表达式。
这里所谓的计算都是编译期执行的,概念的最终结果是一个bool类型的纯右值:

template <class T> concept TestConcept = true;
static_assert(TestConcept<int>);

约束表达式还支持一般的逻辑操作,包括合取和析取:

// 合取
template <class C>
concept SignedIntegerType = std::is_integral_v<C> && std::is_signed_v<C>;
// 析取
template <class C>
concept IntegerFloatingType = std::is_integral_v<C> || std::is_floating_point_v<C>;

41.4 requires子句和约束检查顺序

除了使用concept关键字来定义概念,我们还可以使用requires子句直接约束模板实参

template <class T>
requires std::is_integral_v<T> && std::is_signed_v<C>
struct X {};

requires子句对于该常量表达式还有一些额外的要求。
1.是一个初等表达式或带括号的任意表达式。

constexpr bool bar() { return true; }
template <class T>
requires bar()
struct X {};

由于这里的bar()不是初等表达式,不符合语法规则,因此编译失败,需要修改为:

constexpr bool bar() { return true; }
template <class T>
requires (bar())
struct X {};

2.使用&&或者||运算符链接上述表达式:

constexpr bool bar() { return true; }
template <class T>
requires (bar()) && true || false
struct X {};

template <class T> requires std::is_integral_v<T>
void foo();
template <class T>
void foo() requires std::is_integral_v<T>;

当一个模板同时具备多种约束时,如何确定优先级

template <class C>
concept ConstType = std::is_const_v<C>;
template <class C>
concept IntegralType = std::is_integral_v<C>;
template <ConstType T>
requires std::is_pointer_v<T>
void foo(IntegralType auto) requires std::is_same_v<T, char * const> {}

对于函数模板调用:

foo<int>(1.5);

1.模板形参列表中的形参的约束表达式,其中检查顺序就是形参出现的顺序。也就是说使用concept定义的概念约束的形参会被优先检查,放到刚刚的例子中foo< int>();最先不符合的是ConstType的约束表达式std::is_const_v< C>。
2.模板形参列表之后的requires子句中的约束表达式。这意味着,如果foo的模板实参通过了前一个约束检查后将会面临std::is_pointer_v< T>的检查。
3.简写函数模板声明中每个拥有受约束auto占位符类型的形参所引入的约束表达式。还是放到例子中看,如果前两个约束条件已经满足,编译器则会检查函数实参是否满足IntegralType的约束。
4.函数模板声明尾部requires子句中的约束表达式。所以例子中最后检查的是std::is_same_v<T, char * const>。
对于foo< int>(1.5);,不满足所有约束条件,编译器报错提示不满足ConstType< T>的约束。
对于foo< const int>(1.5);,满足ConstType< T>,但是不满足其他条件,编译器报错提示不满足std::is_pointer_v< T>的约束。
对于foo<int * const>(1.5);,满足前两个条件,但是不满足其他条件,编译器报错提示不满足IntegralType< auto>的约束。
对于foo<int * const>(1);,满足前3个条件,但是不满足其他条件,编译器报错提示不满足std::is_same_v<T, char *const>的约束。
foo<char * const>(1);满足所有条件,编译成功。

41.5 原子约束

原子约束是表达式和表达式中模板形参到模板实参映射的组合(简称为形参映射)。

template <int N> constexpr bool Atomic = true;
template <int N> concept C = Atomic<N>;
template <int N> concept Add1 = C<N + 1>;
template <int N> concept AddOne = C<N + 1>;
template <int M> void f()
requires Add1<2 * M> {};
template <int M> void f()
requires AddOne<2 * M> && true {};
f<0>(); // 编译成功

虽然概念Add1和AddOne使用了不同的名称,但是实际上是相同的,因为在这两个函数中概念C的原子约束都是Atomic< N>,其形参映射都为N~2 * M + 1。
接下来让我们把形参映射改变一下:

template <int N> void f2()
requires Add1<2 * N> {};
template <int N> void f2()
requires Add1<N * 2> && true {};
f2<0>(); // 编译失败

虽然都是用了概念Add1,但是它们的形参映射不同,分别为2 * N + 1和N * 2 + 1,所以Add1<N * 2> && true并不能包含Add1 <2 * N>,而对于f2< 0>();而言, 两个f2函数模板都满足约束,这里的二义性让编译器不知所措,导致编译失败。

当约束表达式中存在原子约束时,如果约束表达式结果相同,则约束表达式应该是相同的,否则会导致编译失败

template <class T> concept sad = false;
template <class T> int f1(T) requires (!sad<T>) { return 1; };
template <class T> int f1(T) requires (!sad<T>) && true {return 2;};
f1(0); // 编译失败

需要注意的是,逻辑否定表达式是一个原子约束。所以以上代码会产生二义性,原子约束表达式!sad< T>并不来自相同的约束表达式。为了让代码能成功编译,需要修改代码为:

template <class T> concept not_sad = !sad<T>;
template <class T> int f2(T) requires (not_sad<T>) { return 3; };
template <class T> int f2(T) requires (not_sad<T>) && true { return 4; };
f2(0);
template <class T> int f3(T) requires (not_sad<T> == true) {return 5; };
template <class T> int f3(T) requires (not_sad<T> == true) && true { return 6; };
f3(0);
template <class T> concept not_sad_is_true = !sad<T> == true;
template <class T> int f4(T) requires (not_sad_is_true<T>) { return 7; };
template <class T> int f4(T) requires (not_sad_is_true<T>) && true{ return 8; };
f4(0);

同样的理由,f3(0);会因为二义性无法通过编译,而f4(0)可以编译成功并最后返回8。

41.6 requires表达式

requires关键字除了可以引入requires子句,还可以用来定义一个requires表达式,该表达式同样是一个纯右值表达式,表达式为true时表示满足约束条件,反之false表示不满足约束条件。

template <class T>
concept Check = requires {
	T().clear();
};
template <Check T>
struct G {};
G<std::vector<char>> x; // 编译成功
G<std::string> y; // 编译成功
G<std::array<char, 10>> z; // 编译失败

requires表达式还支持形参列表,使用形参列表可以使requires表达式更加灵活清晰

template <class T>
concept Check = requires(T a, T b) {
	a.clear();
	a + b;
};

形参列表和普通函数的形参列表类似,不同的是这些形参并不存在生命周期和存储方式,只在编译期起作用,而且只有在requires表达式作用域内才是有效的。

template<typename T>
concept C = requires(T t,) { // 编译失败,requires表达式的形参列表不能使用…
	t;
};

在上面的requires表达式中,a.clear()和a + b可以说是对模板实参的两个要求,这些要求在C++标准中称为要求序列(requirement-seq)。要求序列分为4种,包括简单要求、类型要求、复合要求以及嵌套要求。

41.6.1 简单要求

简单要求是不以requires关键字开始的要求,它只断言表达式的有效性,并不做表达式的求值操作。如果表达式替换模板实参失败,则该要求的计算结果为false:

template<typename T> concept C =
requires (T a, T b) {
	a + b;
};

在上面的代码中a + b是一个简单要求,编译器会断言a + b的合法性,但不会计算其最终结果。

41.6.2 类型要求

类型要求是以typename关键字开始的要求,紧跟typename的是一个类型名,通常可以用来检查嵌套类型、类模板以及别名模板特化的有效性。如果模板实参替换失败,则要求表达式的计算结果为false:

template<typename T, typename T::type = 0> struct S;
template<typename T> using Ref = T&;
template<typename T> concept C = requires {
	typename T::inner; // 要求嵌套类型
	typename S<T>; // 要求类模板特化
	typename Ref<T>; // 要求别名模板特化
};
template <C c>
struct M {};
struct H {
using type = int;
using inner = double;
};
M<H> m;
41.6.3 复合要求

相对于简洁的简单要求和类型要求,复合要求则稍微复杂一些

template <class T>
concept Check = requires(T a, T b) {
	{a.clear()} noexcept;
	 {a + b} noexcept -> std::same_as<int>;
};

在上面的代码中,{a.clear()} noexcept;和{a + b}noexcept -> std::same_as< int>;是需要断言的复合要求。复合要求可以由3个部分组成:{}中的表达式、noexcept以及->后的返回类型约束,其中noexcept和->后的返回类型约束是可选的。根据标准,断言一个复合要求需要按照以下顺序。
1.替换模板实参到{E}中的表达式E,检测表达式的有效性。
2.如果使用了noexcept,则需要检查并确保{E}中的表达式E不会有抛出异常的可能。
3.如果使用了->后的返回类型约束,则需要将模板实参替换到返回类型约束中,并且确保表达式E的结果类型,即decltype((E)),满足返回类型约束。

41.6.4 嵌套要求

正如简单要求中提到的,嵌套要求是以requires开始的要求,它通常根据局部形参来指定其他额外的要求。例如:

template <class T>
concept Check = requires(T a, T b) {
	requires std::same_as<decltype((a + b)), int>;
};

在上面的代码中,requires std::same_as<decltype((a+ b)), int>;是一个嵌套要求,它要求表达式a + b的结果类型与int相同,可以等同于:

template <class T>
concept Check = requires(T a, T b) {
	{a + b} -> std::same_as<int>;
};

41.7 约束可变参数模板

使用概念约束可变参数模板实际上就是将各个实参替换到概念的约束表达式后合取各个结果。

template<class T> concept C1 = true;
template<C1… T> struct s1 {};

s1包展开后的约束为(C1< T> &&…),具体来说对于s1<int,double, std::string>,其约束实际上为(C1< int> &&C1< double> && C1< std:: string>)。

template<class… Ts> concept C2 = true;
template<C2… T> struct s2 {};

在这种情况下包展开的结果依然是(C2< T> &&…)。
对于:

template<class T, class U> concept C3 = true;
template<C3<int> T> struct s3 {}

经过模板实参替换后实际的约束为C<T, int>

41.8 约束类模板特化

约束可以影响类模板特化的结果,在模板实例化的时候编译器会自动选择更满足约束条件的特化版本进行实例化

template<typename T> concept C = true;
template<typename T> struct X {
	X() { std::cout << "1.template<typename T> struct X" << std::endl; }
};
template<typename T> struct X<T*> {
	X() { std::cout << "2.template<typename T> struct X<T*>" << std::endl; }
};
template<C T> struct X<T> {
	X() { std::cout << "3.template<C T> struct X<T>" << std::endl; }
};
X<int*> s1;
X<int> s2;

输出结果:
2template<typename T> struct X<T*>
3template<C T> struct X<T>

约束在类模板特化上可以发挥很大的作用

template<typename T> concept C = requires (T t) { t.f(); };
template<typename T> struct S {
	S() {
		std::cout << "1.template<typename T> struct S" << std::endl;
	}
};
template<C T> struct S<T> {
	S() {
		std::cout << "2.template<C T> struct S<T>" << std::endl;
	}
};
struct Arg { void f(); };
S<int> s1;
S<Arg> s2;

输出如下:
1template<typename T> struct S
2template<C T> struct S<T>

41.9 约束auto

事实上对auto和decltype(auto)的约束可以扩展到普遍情况

template <class C>
concept IntegerType = std::is_integral_v<C>;
IntegerType auto i1 = 5.2; // 编译失败
IntegerType auto i2 = 11; // 编译成功
IntegerType decltype(auto) i3 = 4.8; // 编译失败
IntegerType decltype(auto) i4 = 7; // 编译成功
IntegerType auto foo1() { return 1.1; } // 编译失败
IntegerType auto foo2() { return 0; } // 编译成功
auto bar1 = []()->IntegerType auto { return 1.0; }; // 编译失败
auto bar2 = []()->IntegerType auto { return 10; }; // 编译成功

概念IntegerType约束auto的推导结果必须是一个整型,于是在声明并初始化i1和i3的时候会导致编译失败。同理,函数foo1返回值为浮点类型也会导致编译失败。对于lambda表达式也是一样,只不过需要显式声明返回类型和约束概念。

第42章 模板特性的其他优化(C++11 C++14)

42.1 外部模板(C++11)

在多份源代码中对同一模板进行相同的实例化是不会有任何链接问题的

// header.h
template<class T> bool foo(T t) { return true; }
// src1.cpp
#include <header.h>
bool b = foo(7);
// src2.cpp
#include <header.h>
bool b = foo(11);

为了优化编译和连接的性能,C++11标准提出了外部模板的特性,这个特性保留了extern关键字的语义并扩展了关键字的功能,让它能够声明一个外部模板实例。

// header.h
template<class T> bool foo(T t) { return true; }
// src1.cpp
#include <header.h>
extern template bool foo<double>(double);
// src2.cpp
#include <header.h>
template bool foo<double>(double);

编译器将不会对src1.cpp生成foo函数模板的实例,而是在链接的时候使用src2.cpp生成的bool foo< double>(double)函数。
外部模板除了可以针对函数模板进行优化,对于类模板也同样适用

// header.h
template<class T> class bar {
public:
void foo(T t) {};
};
// src1.cpp
#include <header.h>
extern template class bar<int>;
extern template void bar<int>::foo(int);
// src2.cpp
#include <header.h>
template class bar<int>;

42.2 连续右尖括号的解析优化(C++11)

在C++11标准中,编译器不再一味地使用贪婪原则将连续的两个右尖括号解析为右移,它会识别左尖括号激活状态并且将右尖括号优先匹配给激活的左尖括号。

42.3 friend声明模板形参(C++11)

在C++11标准中,将一个类声明为另外一个类的友元,可以忽略前者的class关键字。
用模板参数结合友元可以让我们在使用友元的代码上进行切换而不需要多余的代码修改

class InnerVisitor { /*访问SomeDatabase内部数据*/ };
template <typename T> class SomeDatabase {
friend T;
// … 内部数据
public:
// … 外部接口
};
typedef SomeDatabase<InnerVisitor> DiagDatabase;
typedef SomeDatabase<void> StandardDatabase;

42.4 变量模板(C++14)

在C++14的标准中引入了变量模板的特性,有了变量模板,我们不再需要冗余地定义类模板和函数模板,只需要专注要定义的变量即可

#include <iostream>
template<class T>
constexpr T PI = static_cast<T>(3.1415926535897932385L);
int main()
{
	std::cout << PI<float> << std::endl;
}

变量模板的模板形参也可以是非类型的:

#include <iostream>
template<class T, int N>
T PI = static_cast<T>(3.1415926535897932385L) * N;
int main()
{
	PI<float, 2> *= 5;
	std::cout << PI<float, 2> << std::endl;
}

42.5 explicit(bool)

C++20标准扩展了explicit说明符的功能,在新标准中它可以接受一个求值类型为bool的常量表达式,用于指定explicit的功能是否生效。

std::pair<std::string, std::string> safe() {
	return {"meow", "purr"}; // 编译成功
}
std::pair<std::vector<int>, std::vector<int>> unsafe() {
	return {11, 22}; // 编译失败
}
// SFINAE版本
template <typename T1, typename T2>
struct pair {
	template <typename U1=T1, typename U2=T2,
	std::enable_if_t<std::is_constructible_v<T1, U1> && std::is_constructible_v<T2, U2>
	, int> = 0>
	explicit(!std::is_convertible_v<U1, T1> || !std::is_convertible_v<U2, T2>)
	constexpr pair(U1&&, U2&& );
};
// 概念版本
template <typename T1, typename T2>
struct pair {
	template <typename U1=T1, typename U2=T2>
	requires std::is_constructible_v<T1, U1> && std::is_constructible_v<T2, U2>
	explicit(!std::is_convertible_v<U1, T1> || !std::is_convertible_v<U2, T2>)
	constexpr pair(U1&&, U2&& );
};

当U1、U2不能转换到T1和T2的时候,!std::is_convertible_v<U1, T1> || !std::is_convertible_v<U2, T2>的求值为true,explicit(true)表示该构造函数为显式的。反之,当U1、U2可以转换到T1和T2时,最终结果为explicit(false),explicit说明符被忽略,构造函数可以隐式执行。

相比于 C++98 带来的面向对象的革命性,C++11 带来的 XIII  却并非“翻天覆地”式的改变。很多时候,程序员保持着“C++98 式”的观点来看待 C++11 代码也同样是合理的。因为在编程思想上,C++11 依然遵从了一贯的面向对象的思想,并深 入加强了泛型编程的支持。从我们的观察来看,C++11 更多的是对步入“成熟稳重”的中年 时期的 C++ 的一种改造。比如,像 auto 类型推导这样的新特性,展现出的是语言的亲和力 ; 而右值引用、移动语义的特性,则着重于改变一些使用 C++ 程序库时容易发生的性能不佳的 状况。当然,C++11 中也有局部的创新,比如 lambda 函数的引入,以及原子类型的设计等, 都体现了语言与时俱进的活力。语言的诸多方面都在 C++11 中再次被锤炼,从而变得更加合 理、更加条理清晰、更加易用。C++11 对 C++ 语言改进的每一点,都呈现出了经过长时间技 术沉淀的编程语言的特色与风采。所以从这个角度上看,学习 C++11 与 C++98 在思想上是 一脉相承的,程序员可以用较小的代价对 C++ 的知识进行更新换代。而在现实中,只要修改 少量已有代码(甚至不修改) ,就可以使用 C++11 编译器对旧有代码进行升级编译而获得新 标准带来的好处,这也非常具有实用性。因此,从很多方面来看,C++ 程序员都应该乐于升 级换代已有的知识,而学习及使用 C++11 也正是大势所趋。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值