您现在的位置是:首页 >其他 >C++17完整导引-基本特性网站首页其他
C++17完整导引-基本特性
结构化绑定
结构化绑定允许你用一个对象的元素或成员同时实例化多个实体。
例如,假设你定义了一个有两个不同成员的结构体:
struct MyStruct {
int i = 0;
std::string s;
};
MyStruct ms;
你可以通过如下的声明直接把该结构体的两个成员绑定到新的变量名:
auto [u, v] = ms;
这里,变量u
和v
的声明方式称为 结构化绑定。
下列每一种声明方式都是支持的:
auto [u2, v2] {ms};
auto [u3, v3] (ms);
结构化绑定对于返回结构体或者数组的函数来说非常有用。例如,考虑一个返回结构体的函数:
MyStruct getStruct() {
return MyStruct{42, "hello"};
}
你可以直接把返回的数据成员赋值给两个新的局部变量:
auto [id, val] = getStruct(); // id和val分别是返回结构体的i和s成员
这个例子中,id
和val
分别是返回结构体中的i
和s
成员。 它们的类型分别是int
和std::string
,可以被当作两个不同的对象来使用:
if (id > 30) {
std::cout << val;
}
这么做的好处是可以直接访问成员。另外,把值绑定到能体现语义的变量名上,可以使代码的可读性更强。
下面的代码演示了使用结构化绑定带来的显著改进。
在不使用结构化绑定的情况下遍历std::map<>
的元素需要这么写:
for (const auto& elem : mymap) {
std::cout << elem.first << ": " << elem.second << '
';
}
map
的元素的类型是key
和value
组成的std::pair
类型, 该类型的两个成员分别是first
和second
。 在上边的例子中我们必须使用成员的名字来访问key
和value
,而通过使用结构化绑定,能大大提升代码的可读性:
for (const auto& [key, val] : mymap) {
std::cout << key << ": " << val << '
';
}
上面的例子中我们可以使用准确体现语义的变量名直接访问每个元素的key
和value
成员。
细语
结构化绑定时引入的新变量名其实都指向这个匿名对象的成员/元素。
绑定到一个匿名实体(可直接看预处理代码理解)
对auto [u, v] = ms;
的精确解读应该是:我们用ms
初始化了一个新的实体e
, 并且让结构化绑定中的u
和v
变成e
中成员的别名,类似于如下定义:
auto e = ms;
aliasname u = e.i;
aliasname v = e.s;
这意味着u
和v
仅仅是ms
的一份本地拷贝的成员的别名。 然而,我们没有为e
声明一个名称,因此我们不能直接访问这个匿名对象。 注意u
和v
并不是e.i
和e.s
的引用(而是它们的别名)。
decltype(u)
的结果是成员i
的类型,declytpe(v)
的结果是成员s
的类型。
因此std::cout << u << ' ' << v << '
';
会打印出e.i
和e.s
(分别是ms.i
和ms.s
的拷贝)。
e
的生命周期和结构化绑定的生命周期相同,当结构化绑定离开作用域时e
也会被自动销毁。
另外,除非使用了引用,否则修改用于初始化的变量并不会影响结构化绑定引入的变量(反过来也一样):
MyStruct ms{42, "hello"};
auto [u, v] = ms;
ms.i = 77;
std::cout << u; // 打印出42
u = 99;
std::cout << ms.i; // 打印出77
在这个例子中u
和ms.i
有不同的内存地址。
当使用结构化绑定来绑定返回值时,规则是相同的。如auto [u, v] = getStruct();
初始化的行为等价于我们用getStruct()
的返回值初始化了一个新的实体e
, 之后结构化绑定的u
和v
变成了e
的两个成员的别名,类似于如下定义:
auto e = getStruct();
aliasname u = e.i;
aliasname v = e.s;
也就是说,结构化绑定绑定到了一个 新的 实体e
上,而不是直接绑定到了返回值上。
匿名实体e
同样遵循通常的内存对齐规则,结构化绑定的每一个变量都会根据相应成员的类型进行对齐。
实例:
#include <iostream>
#include <string>
using namespace std;
class MyStruct {
public:
MyStruct(int it, string st):i(it),s(st){}
~MyStruct(){cout << "destruct"<<endl;}
int i = 0;
std::string s;
};
int main() {
MyStruct ms{1,"Test"};
{
auto [u, v] = ms;
}
}
可以看到{auto [u, v] = ms;}
的汇编代码如下
lea rdx, [rbp-112]
lea rax, [rbp-160]
mov rsi, rdx
mov rdi, rax
call MyStruct::MyStruct(MyStruct const&) [complete object constructor]
lea rax, [rbp-160]
mov rdi, rax
call MyStruct::~MyStruct() [complete object destructor]
预处理代码如下,这里注意MyStruct __ms16 = MyStruct(ms);
中的__ms16
是一个新的变量,u
是__ms16
的i值的引用。
MyStruct ms = MyStruct{1, std::basic_string<char, std::char_traits<char>, std::allocator<char> >("Test", std::allocator<char>())};
{
MyStruct __ms16 = MyStruct(ms);
int & u = __ms16.i;
std::basic_string<char, std::char_traits<char>, std::allocator<char> > & v = __ms16.s;
};
使用修饰符
我们可以在结构化绑定中使用修饰符,例如const
和引用,这些修饰符会作用在匿名实体e
上。 通常情况下,作用在匿名实体上和作用在结构化绑定的变量上的效果是一样的,但有些时候又是不同的。
例如,我们可以把一个结构化绑定声明为const
引用:
const auto& [u, v] = ms; // 引用,因此u/v指向ms.i/ms.s
预处理的代码如下
const MyStruct & __ms16 = ms;
const int & u = __ms16.i;
const std::basic_string<char, std::char_traits<char>, std::allocator<char> > & v = __ms16.s;
这里,匿名实体被声明为const
引用, 而u
和v
分别是这个引用的成员i
和s
的别名。 因此,对ms
的成员的修改会影响到u
和v
的值,但是我们不能修改u
的值,如u = 100;
会编译通不过。
#include <iostream>
#include <string>
using namespace std;
class MyStruct {
public:
MyStruct(int it, string st):i(it),s(st){}
~MyStruct(){cout << "destruct"<<endl;}
int i = 0;
std::string s;
};
int main() {
MyStruct ms{1,"Test"};
{
const auto &[u, v] = ms;
ms.i = 2;
cout <<"ms.i = "<<ms.i<<" u = "<<u<<endl;
}
}
输出结果为:
ms.i = 2 u = 2
destruct
如果声明为非const
引用,你甚至可以间接地修改用于初始化的对象的成员:
#include <iostream>
#include <string>
using namespace std;
class MyStruct {
public:
MyStruct(int it, string st) : i(it), s(st) {}
~MyStruct() { cout << "destruct" << endl; }
int i = 0;
std::string s;
};
int main() {
MyStruct ms{42, "hello"};
auto& [u, v] = ms; // 被初始化的实体是ms的引用
ms.i = 77; // 影响到u的值
std::cout << u << endl;
u = 99; // 修改了ms.i
std::cout << ms.i << endl;
}
运行结果如下:
77
99
destruct
如果一个结构化绑定是引用类型,而且是对一个临时对象的引用,那么和往常一样, 临时对象的生命周期会被延长到结构化绑定的生命周期:
#include <iostream>
#include <string>
using namespace std;
struct MyStruct {
int i = 0;
std::string s;
};
MyStruct getStruct() {
return MyStruct{42, "hello"};
}
int main() {
const auto& [a, b] = getStruct();
std::cout << "a: " << a << '
'; // OK
}
运行结果:
a: 42
修饰符并不是作用在结构化绑定引入的变量上
修饰符会作用在新的匿名实体上,而不是结构化绑定引入的新的变量名上。
事实上,如下代码中:
const auto& [u, v] = ms; // 引用,因此u/v指向ms.i/ms.s
u
和v
都 不是 引用,只有匿名实体e
是一个引用。
u
和v
分别是ms
对应的成员的类型, 只不过变成了const
的(因为我们不能修改常量引用的成员)。
根据推导,decltype(u)
是const int
,decltype(v)
是const std::string
。
当指明对齐时也是类似:
#include <iostream>
#include <string>
using namespace std;
struct MyStruct {
int i = 0;
std::string s;
};
MyStruct getStruct() {
return MyStruct{42, "hello"};
}
int main() {
alignas(16) auto [u, v] = getStruct(); // 对齐匿名实体,而不是v
cout<< "sizeof(u) "<< sizeof(u)<<" sizeof(v) "<<sizeof(v)<<endl;
}
这里alignas(16) auto [u, v] = getStruct()
,我们对齐了匿名实体而不是u
和v
。 这意味着u
作为第一个成员会按照16字节对齐,但v
不会。
因此,即使使用了auto
结构化绑定也不会发生 类型退化(decay)。 例如,如果我们有一个原生数组组成的结构体:
struct S {
const char x[6];
const char y[3];
};
那么如下声明之后:
S s1{};
auto [a, b] = s1; // a和b的类型是结构体成员的精确类型,sizeof(a) 6 sizeof(b) 3
这里a
的类型仍然是const char[6]
。 再次强调,auto
关键字应用在匿名实体上,这里匿名实体整体并不会发生类型退化。 这和用auto
初始化新对象不同,如下代码中会发生类型退化:
auto a2 = a; // a2的类型是a的退化类型
move语义
move
语义也遵循之前介绍的规则,如下声明:
MyStruct ms = {42, "Jim"};
auto&& [v, n] = std::move(ms); // 匿名实体是ms的右值引用
预处理代码如下
MyStruct ms = {42, std::basic_string<char, std::char_traits<char>, std::allocator<char> >("Jim", std::allocator<char>())};
typename std::remove_reference<MyStruct &>::type && __move13 = std::move(ms);
int & v = __move13.i;
std::basic_string<char, std::char_traits<char>, std::allocator<char> > & n = __move13.s;
这里v
和n
指向的匿名实体是ms
的右值引用, 同时ms
仍持有值:
std::cout << "ms.s: " << ms.s << '
'; // 打印出"Jim"
然而,你可以对指向ms.s
的n
进行移动赋值:
std::string s = std::move(n); // 把ms.s移动到s
std::cout << "ms.s: " << ms.s << '
'; // 打印出未定义的值
std::cout << "n: " << n << '
'; // 打印出未定义的值
std::cout << "s: " << s << '
'; // 打印出"Jim"
像通常一样,值被移动走的对象处于一个值未定义但却有效的状态。因此可以打印它们的值, 但不要对打印出的值做任何假设。
上面的例子和直接用ms
被移动走的值进行结构化绑定有些不同:
MyStruct ms = {42, "Jim"};
auto [v, n] = std::move(ms); // 新的匿名实体持有从ms处移动走的值
预处理代码如下:
MyStruct ms = {42, std::basic_string<char, std::char_traits<char>, std::allocator<char> >("Jim", std::allocator<char>())};
MyStruct __move13 = MyStruct(std::move(ms));
int & v = __move13.i;
std::basic_string<char, std::char_traits<char>, std::allocator<char> > & n = __move13.s;
这里新的匿名实体是用ms
被移动走的值来初始化的。因此,ms
已经失去了值:
std::cout << "ms.s: " << ms.s << '
'; // 打印出未定义的值
std::cout << "n: " << n << '
'; // 打印出"Jim"
你可以继续用n
进行移动赋值或者给n
赋予新值,但已经不会再影响到ms.s
了:
std::string s = std::move(n); // 把n移动到s
n = "Lara";
std::cout << "ms.s: " << ms.s << '
'; // 打印出未定义的值
std::cout << "n: " << n << '
'; // 打印出"Lara"
std::cout << "s: " << s << '
'; // 打印出"Jim"
适用场景
理论上讲,结构化绑定适用于任何有public
数据成员的结构体、 C
风格数组和“类似元组(tuple-like
)的对象”:
-
对于所有非静态数据成员都是
public
的 结构体和类 , 你可以把每一个成员绑定到一个新的变量名上。 -
对于 原生数组 ,你可以把数组的每一个元素都绑定到新的变量名上。
-
对于任何类型,你可以使用 tuple-like API 来绑定新的名称, 无论这套
API
是如何定义“元素”的。对于一个类型 type 这套API需要如下的组件:std::tuple_size<type>::value
要返回元素的数量。std::tuple_element<idx, type>::type
要返回第idx
个元素的类型。- 一个全局或成员函数
get<idx>()
要返回第idx
个元素的值。
标准库类型std::pair<>
、std::tuple<>
、std::array<>
就是提供了这些API的例子。 如果结构体和类提供了tuple-like API
,那么将会使用这些API
进行绑定,而不是直接绑定数据成员。
在任何情况下,结构化绑定中声明的变量名的数量都必须和元素或数据成员的数量相同。 你不能跳过某个元素,也不能重复使用变量名。然而,你可以使用非常短的名称例如'_'
(有人喜欢这个名字,有的也讨厌它,但注意全局命名空间不允许使用它), 但这个名字在同一个作用域只能使用一次:
auto [_, val1] = getStruct(); // OK
auto [_, val2] = getStruct(); // ERROR:变量名_已经被使用过
预处理代码如下:
MyStruct __move15 = MyStruct(static_cast<const MyStruct &&>(std::move(ms)));
int & _ = __move15.i;
std::basic_string<char, std::char_traits<char>, std::allocator<char> > & n = __move15.s;
目前还不支持嵌套化的结构化绑定。
结构体和类
上面已经介绍了对只有public
成员的结构体和类使用结构化绑定的方法, 一个典型的应用是直接对包含多个数据的返回值使用结构化绑定。 然而有一些边缘情况需要注意。
注意要使用结构化绑定需要继承时遵循一定的规则。所有的非静态数据成员必须在同一个类中定义 (也就是说,这些成员要么是全部直接来自于最终的类,要么是全部来自同一个父类):
struct B {
int a = 1;
int b = 2;
};
struct D1 : B {
};
auto [x, y] = D1{}; // OK
struct D2 : B {
int c = 3;
};
auto [i, j, k] = D2{}; // 编译期ERROR:cannot decompose class type 'D2': both it and its base class 'B' have non-static data members
注意只有当public
成员的顺序保证是固定的时候你才应该使用结构化绑定。 否则如果B
中的int a
和int b
的顺序发生了变化,
x
和y
的值也会随之变化。为了保证固定的顺序, C++17
为一些标准库结构体(例如insert_return_type
) 定义了成员顺序。
联合还不支持使用结构化绑定。
原生数组
下面的代码用C
风格数组的两个元素初始化了x
和y
:
int arr[] = { 47, 11 };
auto [x, y] = arr; // x和y是arr中的int元素的拷贝
auto [z] = arr; // ERROR:元素的数量不匹配
预处理代码如下:
int __arr15[2] = {arr[0], arr[1]};
int & x = __arr15[0];
int & y = __arr15[1];
注意这是C++
中少数几种原生数组会按值拷贝的场景之一。
只有当数组的长度已知时才可以使用结构化绑定。 数组作为按值传入的参数时不能使用结构化绑定,因为数组会 退化(decay) 为相应的指针类型。
注意C++
允许通过引用来返回带有大小信息的数组,结构化绑定可以应用于返回这种数组的函数:
auto getArr() -> int(&)[2]; // getArr()返回一个原生int数组的引用
...
auto [x, y] = getArr(); // x和y是返回的数组中的int元素的拷贝
你也可以对std::array
使用结构化绑定,这是通过tuple-like API
来实现的。
std::pair
, std::tuple
和std::array
结构化绑定机制是可拓展的,你可以为任何类型添加对结构化绑定的支持。 标准库中就为std::pair<>
、std::tuple<>
、std::array<>
添加了支持。
std::array
例如,下面的代码为getArray()
返回的std::array<>
中的四个元素绑定了 新的变量名a
,b
,c
,d
:
std::array<int, 4> getArray();
...
auto [a, b, c, d] = getArray(); // a,b,c,d是返回值的拷贝中的四个元素的别名
这里a
,b
,c
,d
被绑定到getArray()
返回的std::array
类型的元素上。
使用非临时变量的non-const
引用进行绑定,还可以进行修改操作。例如:
#include <array>
#include <iostream>
using namespace std;
int main() {
std::array<int, 4> stdarr{1, 2, 3, 4};
//1
auto& [a, b, c, d] = stdarr;
a += 10; // OK:修改了stdarr[0]
cout << "stdarr[0] " << stdarr.at(0) << endl;
//2
const auto& [e, f, g, h] = stdarr;
// e += 10; // ERROR:引用指向常量对象 error: assignment of read-only reference 'e'
//3
auto&& [i, j, k, l] = stdarr;
i += 10; // OK:修改了stdarr[0]
cout << "stdarr[0] " << stdarr.at(0) << endl;
//4
auto [m, n, o, p] = stdarr;
m += 10; // OK:但是修改的是stdarr[0]的拷贝
cout << "stdarr[0] " << stdarr.at(0) << endl;
}
预处理代码如下:
//1
std::array<int, 4> & __stdarr10 = stdarr;
int & a = std::get<0UL>(__stdarr10);
int & b = std::get<1UL>(__stdarr10);
int & c = std::get<2UL>(__stdarr10);
int & d = std::get<3UL>(__stdarr10);
a = static_cast<std::tuple_element<0, std::array<int, 4> >::type>(a + 10);
std::operator<<(std::cout, "stdarr[0] ").operator<<(stdarr.at(0)).operator<<(std::endl);
//2
const std::array<int, 4> & __stdarr15 = stdarr;
const int & e = std::get<0UL>(__stdarr15);
const int & f = std::get<1UL>(__stdarr15);
const int & g = std::get<2UL>(__stdarr15);
const int & h = std::get<3UL>(__stdarr15);
//3
std::array<int, 4> & __stdarr19 = stdarr;
int & i = std::get<0UL>(__stdarr19);
int & j = std::get<1UL>(__stdarr19);
int & k = std::get<2UL>(__stdarr19);
int & l = std::get<3UL>(__stdarr19);
i = static_cast<std::tuple_element<0, std::array<int, 4> >::type>(i + 10);
std::operator<<(std::cout, "stdarr[0] ").operator<<(stdarr.at(0)).operator<<(std::endl);
//4
std::array<int, 4> __stdarr24 = std::array<int, 4>(stdarr);
int && m = std::get<0UL>(static_cast<std::array<int, 4> &&>(__stdarr24));
int && n = std::get<1UL>(static_cast<std::array<int, 4> &&>(__stdarr24));
int && o = std::get<2UL>(static_cast<std::array<int, 4> &&>(__stdarr24));
int && p = std::get<3UL>(static_cast<std::array<int, 4> &&>(__stdarr24));
m = static_cast<std::tuple_element<0, std::array<int, 4> >::type>(m + 10);
std::operator<<(std::cout, "stdarr[0] ").operator<<(stdarr.at(0)).operator<<(std::endl);
运行结果:
stdarr[0] 11
stdarr[0] 21
stdarr[0] 21
然而像往常一样,我们不能用临时对象(prvalue
)初始化一个非 const
引用:
auto& [a, b, c, d] = getArray(); // ERROR
std::tuple
下面的代码将a
,b
,c
初始化为getTuple()
返回的std::tuple<>
的拷贝的三个元素的别名:
#include <array>
#include <iostream>
#include <tuple>
using namespace std;
std::tuple<char, float, std::string> getTuple(){return std::tuple('n', 11.22, "tuple");}
int main() {
auto [a, b, c] = getTuple(); // a,b,c的类型和值与返回的tuple中相应的成员相同
cout<< " a = "<<a<<"; b ="<<b<<"; c ="<<c<<endl;
}
预处理代码如下:
std::tuple<char, float, std::basic_string<char, std::char_traits<char>, std::allocator<char> > > getTuple()
{
return std::tuple<char, float, std::basic_string<char, std::char_traits<char>, std::allocator<char> > >(std::tuple<char, double, const char *>('n', 11.220000000000001, "tuple"));
}
int main()
{
std::tuple<char, float, std::basic_string<char, std::char_traits<char>, std::allocator<char> > > __getTuple10 = getTuple();
char && a = std::get<0UL>(static_cast<std::tuple<char, float, std::basic_string<char, std::char_traits<char>, std::allocator<char> > > &&>(__getTuple10));
float && b = std::get<1UL>(static_cast<std::tuple<char, float, std::basic_string<char, std::char_traits<char>, std::allocator<char> > > &&>(__getTuple10));
std::basic_string<char, std::char_traits<char>, std::allocator<char> > && c = std::get<2UL>(static_cast<std::tuple<char, float, std::basic_string<char, std::char_traits<char>, std::allocator<char> > > &&>(__getTuple10));
std::operator<<(std::operator<<(std::operator<<(std::operator<<(std::operator<<(std::cout, " a = "), a), "; b =").operator<<(b), "; c ="), c).operator<<(std::endl);
return 0;
}
其中a
的类型是char
,b
的类型是float
,c
的类型是std::string
。
std::pair
std::map
的insert()
示例演示前,先了解下这个insert()
的函数。
#include <iomanip>
#include <iostream>
#include <map>
#include <string>
using namespace std::literals;
template <typename It>
void print_insertion_status(It it, bool success) {
std::cout << "插入 " << it->first << (success ? " 成功
" : " 失败
");
}
int main() {
std::map<std::string, float> heights;
// 重载 3 :从右值引用插入 c++17
// std::pair<iterator, bool> insert( value_type&& value );
const auto [it_hinata, success] = heights.insert({"Hinata"s, 162.8});
print_insertion_status(it_hinata, success);
{
// 重载 1 :从左值引用插入 C++11
// std::pair<iterator, bool> insert( const value_type& value );
const auto [it, success2] = heights.insert(*it_hinata);
print_insertion_status(it, success2);
}
{
// 重载 2 :经由转发到 emplace 插入 c++11
// template< class P > std::pair<iterator, bool> insert( P&& value );
const auto [it, success] = heights.insert({"Kageyama", 180.6});
print_insertion_status(it, success);
}
{
// 重载 6 :带位置提示从右值引用插入 c++17
// iterator insert( const_iterator pos, value_type&& value );
const std::size_t n = std::size(heights);
const auto it = heights.insert(it_hinata, {"Azumane"s, 184.7});
print_insertion_status(it, std::size(heights) != n);
}
{
// 重载 4 :带位置提示从左值引用插入 c++11
// iterator insert( const_iterator pos, const value_type& value );
const std::size_t n = std::size(heights);
const auto it = heights.insert(it_hinata, *it_hinata);
print_insertion_status(it, std::size(heights) != n);
}
{
// 重载 5 :带位置提示经由转发到 emplace 插入
// template< class P > iterator insert( const_iterator pos, P&& value );
const std::size_t n = std::size(heights);
const auto it = heights.insert(it_hinata, {"Tsukishima", 188.3});
print_insertion_status(it, std::size(heights) != n);
}
auto node_hinata = heights.extract(it_hinata);
std::map<std::string, float> heights2;
// 重载 7 :从范围插入 c++11
// template< class InputIt > void insert( InputIt first, InputIt last );
heights2.insert(std::begin(heights), std::end(heights));
// 重载 8 :从 initializer_list 插入 c++11
// void insert( std::initializer_list<value_type> ilist );
heights2.insert({{"Kozume"s, 169.2}, {"Kuroo", 187.7}});
// 重载 9 :插入结点 c++17
// insert_return_type insert( node_type&& nh );
const auto status = heights2.insert(std::move(node_hinata));
print_insertion_status(status.position, status.inserted);
node_hinata = heights2.extract(status.position);
{
// 重载 10 :插入结点带位置提示 c++17
// iterator insert( const_iterator pos, node_type&& nh );
const std::size_t n = std::size(heights2);
const auto it =
heights2.insert(std::begin(heights2), std::move(node_hinata));
print_insertion_status(it, std::size(heights2) != n);
}
// 打印结果 map
std::cout << std::left << '
';
for (const auto& [name, height] : heights2)
std::cout << std::setw(10) << name << " | " << height << "cm
";
}
运行结果
插入 Hinata 成功
插入 Hinata 失败
插入 Kageyama 成功
插入 Azumane 成功
插入 Hinata 失败
插入 Tsukishima 成功
插入 Hinata 成功
插入 Hinata 成功
Azumane | 184.7cm
Hinata | 162.8cm
Kageyama | 180.6cm
Kozume | 169.2cm
Kuroo | 187.7cm
Tsukishima | 188.3cm
作为另一个例子,考虑如下对关联/无序容器的insert()
成员的返回值进行处理的代码:
#include <iomanip>
#include <iostream>
#include <map>
#include <string>
using namespace std::literals;
template <typename It>
void print_insertion_status(It it, bool success) {
std::cout << "插入 " << it->first << (success ? " 成功
" : " 失败
");
}
int main() {
std::map<std::string, int> coll;
auto ret = coll.insert({"new", 42});
print_insertion_status(ret.first, ret.second);
}
预处理代码如下:
using namespace std::literals;
template<typename It>
void print_insertion_status(It it, bool success)
{
(std::operator<<(std::cout, "346217222345205245 ") << it->first) << (success ? " 346210220345212237
" : " 345244261350264245
");
}
/* First instantiated from: insights.cpp:14 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
void print_insertion_status<std::_Rb_tree_iterator<std::pair<const std::basic_string<char, std::char_traits<char>, std::allocator<char> >, int> > >(std::_Rb_tree_iterator<std::pair<const std::basic_string<char, std::char_traits<char>, std::allocator<char> >, int> > it, bool success)
{
std::operator<<(std::operator<<(std::operator<<(std::cout, "346217222345205245 "), it.operator->()->first), (success ? " 346210220345212237
" : " 345244261350264245
"));
}
#endif
int main()
{
std::map<std::basic_string<char, std::char_traits<char>, std::allocator<char> >, int, std::less<std::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::pair<const std::basic_string<char, std::char_traits<char>, std::allocator<char> >, int> > > coll = std::map<std::basic_string<char, std::char_traits<char>, std::allocator<char> >, int, std::less<std::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::pair<const std::basic_string<char, std::char_traits<char>, std::allocator<char> >, int> > >();
std::pair<std::_Rb_tree_iterator<std::pair<const std::basic_string<char, std::char_traits<char>, std::allocator<char> >, int> >, bool> ret = coll.insert(std::pair<const std::basic_string<char, std::char_traits<char>, std::allocator<char> >, int>{"new", 42});
print_insertion_status(std::_Rb_tree_iterator<std::pair<const std::basic_string<char, std::char_traits<char>, std::allocator<char> >, int> >(ret.first), ret.second);
return 0;
}
运行结果如下:
插入 new 成功
通过使用结构化绑定来代替返回的std::pair<>
对象的first
和second
成员,代码的可读性大大增强:
std::map<std::string, int> coll;
auto [pos, isOk] = coll.insert({"new", 42});
std::cout << "插入 " << (isOk ? " 成功
" : " 失败
");
预处理代码如下:
std::map<std::basic_string<char>, int, std::less<std::basic_string<char> >, std::allocator<std::pair<const std::basic_string<char>, int> > > coll = std::map<std::basic_string<char>, int, std::less<std::basic_string<char> >, std::allocator<std::pair<const std::basic_string<char>, int> > >();
std::pair<std::_Rb_tree_iterator<std::pair<const std::basic_string<char>, int> >, bool> __coll9 = coll.insert(std::pair<const std::basic_string<char>, int>{"new", 42});
std::_Rb_tree_iterator<std::pair<const std::basic_string<char>, int> > && pos = std::get<0UL>(static_cast<std::pair<std::_Rb_tree_iterator<std::pair<const std::basic_string<char>, int> >, bool> &&>(__coll9));
bool && isOk = std::get<1UL>(static_cast<std::pair<std::_Rb_tree_iterator<std::pair<const std::basic_string<char>, int> >, bool> &&>(__coll9));
std::operator<<(std::operator<<(std::cout, "346217222345205245 "), (isOk ? " 346210220345212237
" : " 345244261350264245
"));
注意在这种场景中,C++17
中提供了一种使用带初始化的if
语句 来进行改进的方法。
为pair
和tuple
的结构化绑定赋予新值
在声明了一个结构化绑定之后,你通常不能同时修改所有绑定的变量, 因为结构化绑定只能一起声明但不能一起使用。然而,如果被赋的值可以赋给一个std::pair<>
或std::tuple<>
,你可以使用std::tie()
把值一起赋给所有变量。例如:
std::tuple<char, float, std::string> getTuple() {return std::tuple{'a', 11.22, "test"};}
int main() {
auto [a, b, c] = getTuple(); // a,b,c的类型和值与返回的tuple相同
std::tie(a, b, c) = getTuple(); // a,b,c的值变为新返回的tuple的值
}
预处理后的代码如下:
std::tuple<char, float, std::basic_string<char, std::char_traits<char>, std::allocator<char> > > getTuple()
{
return std::tuple<char, float, std::basic_string<char, std::char_traits<char>, std::allocator<char> > >(std::tuple<char, double, const char *>{'a', 11.220000000000001, "test"});
}
int main()
{
std::tuple<char, float, std::basic_string<char, std::char_traits<char>, std::allocator<char> > > __getTuple14 = getTuple();
char && a = std::get<0UL>(static_cast<std::tuple<char, float, std::basic_string<char, std::char_traits<char>, std::allocator<char> > > &&>(__getTuple14));
float && b = std::get<1UL>(static_cast<std::tuple<char, float, std::basic_string<char, std::char_traits<char>, std::allocator<char> > > &&>(__getTuple14));
std::basic_string<char, std::char_traits<char>, std::allocator<char> > && c = std::get<2UL>(static_cast<std::tuple<char, float, std::basic_string<char, std::char_traits<char>, std::allocator<char> > > &&>(__getTuple14));
std::tie(a, b, c).operator=(getTuple());
return 0;
}
这种方法可以被用来处理返回多个值的循环,例如在循环中使用搜索器:
std::boyer_moore_searcher bmsearch{sub.begin(), sub.end()};
for (auto [beg, end] = bmsearch(text.begin(), text.end());
beg != text.end();
std::tie(beg, end) = bmsearch(end, text.end())) {
...
}
为结构化绑定提供Tuple-Like API
你可以通过提供 tuple-like API 为任何类型添加对结构化绑定的支持, 就像标准库为std::pair<>
、std::tuple<>
、std::array<>
做的一样:
支持只读结构化绑定
下面的例子演示了怎么为一个类型Customer
添加结构化绑定支持, 类的定义如下:
#include <string>
#include <utility> // for std::move()
class Customer {
private:
std::string first;
std::string last;
long val;
public:
Customer (std::string f, std::string l, long v)
: first{std::move(f)}, last{std::move(l)}, val{v} {
}
std::string getFirst() const {
return first;
}
std::string getLast() const {
return last;
}
long getValue() const {
return val;
}
};
我们可以用如下代码添加tuple-like API
:
#include "customer1.hpp"
#include <utility> // for tuple-like API
// 为类Customer提供tuple-like API来支持结构化绑定:
template<>
struct std::tuple_size<Customer> {
static constexpr int value = 3; // 有三个属性
};
template<>
struct std::tuple_element<2, Customer> {
using type = long; // 最后一个属性的类型是long
};
template<std::size_t Idx>
struct std::tuple_element<Idx, Customer> {
using type = std::string; // 其他的属性都是string
};
// 定义特化的getter:
template<std::size_t> auto get(const Customer& c);
template<> auto get<0>(const Customer& c) { return c.getFirst(); }
template<> auto get<1>(const Customer& c) { return c.getLast(); }
template<> auto get<2>(const Customer& c) { return c.getValue(); }
这里,我们为顾客的三个属性定义了tuple-like API
,并映射到三个getter
(也可以自定义其他映射):
- 顾客的名(first name)是
std::string
类型 - 顾客的姓(last name)是
std::string
类型 - 顾客的消费金额是
long
类型
属性的数量被定义为std::tuple_size
模板函数对Customer
类型的特化版本:
template<>
struct std::tuple_size<Customer> {
static constexpr int value = 3; // 我们有3个属性
};
属性的类型被定义为std::tuple_element
的特化版本:
template<>
struct std::tuple_element<2, Customer> {
using type = long; // 最后一个属性的类型是long
};
template<std::size_t Idx>
struct std::tuple_element<Idx, Customer> {
using type = std::string; // 其他的属性都是string
};
第三个属性的类型是long
,被定义为Idx
为2时的完全特化版本。 其他的属性类型都是std::string
,被定义为部分特化版本(优先级比全特化版本低)。 这里声明的类型就是结构化绑定时decltype
返回的类型。
最后,我们在和类Customer
同级的命名空间中定义了函数get<>()
的 重载版本作为getter
:
template<std::size_t> auto get(const Customer& c);
template<> auto get<0>(const Customer& c) { return c.getFirst(); }
template<> auto get<1>(const Customer& c) { return c.getLast(); }
template<> auto get<2>(const Customer& c) { return c.getValue(); }
在这种情况下,我们有一个主函数模板的声明和针对所有情况的全特化版本。
注意函数模板的全特化版本必须使用和声明时相同的类型(包括返回值类型都必须完全相同)。 这是因为我们只是提供特化版本的“实现”,而不是新的声明。下面的代码将不能通过编译:
template<std::size_t> auto get(const Customer& c);
template<> std::string get<0>(const Customer& c) { return c.getFirst(); }
template<> std::string get<1>(const Customer& c) { return c.getLast(); }
template<> long get<2>(const Customer& c) { return c.getValue(); }
通过使用新的编译期if
语句特性,我们可以把get<>()
函数的实现
合并到一个函数里:
template<std::size_t I> auto get(const Customer& c) {
static_assert(I < 3);
if constexpr (I == 0) {
return c.getFirst();
}
else if constexpr (I == 1) {
return c.getLast();
}
else { // I == 2
return c.getValue();
}
}
有了这个API
,我们就可以为类型Customer
使用结构化绑定:
#include <string>
#include <iostream>
#include <utility> // for std::move()
using namespace std;
class Customer {
private:
std::string first;
std::string last;
long val;
public:
Customer (std::string f, std::string l, long v)
: first{std::move(f)}, last{std::move(l)}, val{v} {
}
std::string getFirst() const {
return first;
}
std::string getLast() const {
return last;
}
long getValue() const {
return val;
}
};
void printCustomer(const Customer &t) {
cout<<"Customer first = "<< t.getFirst() <<" ; last= "<<t.getLast()<<"; val= "<<t.getValue()<<endl;
}
// 为类Customer提供tuple-like API来支持结构化绑定:
template<>
struct std::tuple_size<Customer> {
static constexpr int value = 3; // 有三个属性
};
template<>
struct std::tuple_element<2, Customer> {
using type = long; // 最后一个属性的类型是long
};
template<std::size_t Idx>
struct std::tuple_element<Idx, Customer> {
using type = std::string; // 其他的属性都是string
};
// 定义特化的getter:
template<std::size_t> auto get(const Customer& c);
template<> auto get<0>(const Customer& c) { return c.getFirst(); }
template<> auto get<1>(const Customer& c) { return c.getLast(); }
template<> auto get<2>(const Customer& c) { return c.getValue(); }
template<std::size_t I> auto get(const Customer& c) {
static_assert(I < 3);
if constexpr (I == 0) {
return c.getFirst();
}
else if constexpr (I == 1) {
return c.getLast();
}
else { // I == 2
return c.getValue();
}
}
int main() {
Customer c{"Tim", "Starr", 42};
printCustomer(c);
auto [f, l, v] = c;
std::cout << "f/l/v: =/" << f << " /" << l << " /" << v << '
';
// 修改结构化绑定的变量
std::string s{std::move(f)};
l = "Waters";
v += 10;
std::cout << "f/l/v: =/" << f << " /" << l << " /"<< v << '
';
std::cout << "c: " << c.getFirst() << ' '
<< c.getLast() << ' ' << c.getValue() << '
';
std::cout << "s: " << s << '
';
return 0;
}
运行结果如下:
Customer first = Tim ; last= Starr; val= 42
f/l/v: =/Tim /Starr /42
f/l/v: =/ /Waters /52
c: Tim Starr 42
s: Tim
如下初始化之后:
auto [f, l, v] = c;
像之前的例子一样,Customer c
被拷贝到一个匿名实体。 当结构化绑定离开作用域时匿名实体也被销毁。
另外,对于每一个绑定f
、l
、v
, 它们对应的get<>()
函数都会被调用。 因为定义的get<>
函数返回类型是auto
,所以这3个getter
会返回成员的拷贝, 这意味着结构化绑定的变量的地址不同于c
中成员的地址。 因此,修改c
的值并不会影响绑定变量(反之亦然)。
使用结构化绑定等同于使用get<>()
函数的返回值,因此:
std::cout << "f/l/v: " << f << ' ' << l << ' ' << v << '
';
只是简单的输出变量的值(并不会再次调用getter
函数)。另外
std::string s{std::move(f)};
l = "Waters";
v += 10;
std::cout << "f/l/v: " << f << ' ' << l << ' ' << v << '
';
这段代码修改了绑定变量的值。
因此,这段程序通常有如下输出:
Customer first = Tim ; last= Starr; val= 42
f/l/v: =/Tim /Starr /42
f/l/v: =/ /Waters /52
c: Tim Starr 42
s: Tim
第二行的输出依赖于被move
的string
的值,一般情况下是空串,但也有可能是其他有效的值。
你也可以在迭代一个元素类型为Customer
的vector
时使用结构化绑定:
std::vector<Customer> cc;
cc.push_back({"Tim", "Starr", 42});
cc.push_back({"Tony", "OMG", 43});
cc.push_back({"Spny", "MG", 43});
for (const auto& [first, last, val] : cc) {
std::cout << first << ' ' << last << ": " << val << '
';
}
在这个循环中,因为使用了const auto&
所以不会有Customer
被拷贝。 然而,结构化绑定的变量初始化时会调用get<>()
函数返回姓和名的拷贝。
之后,循环体内的输出语句中使用了结构化绑定的变量,不需要再次调用getter
。 最后在每一次迭代结束的时候,拷贝的字符串会被销毁。
运行结果如下:
Tim Starr: 42
Tony OMG: 43
Spny MG: 43
注意对绑定变量使用decltype
会推导出变量自身的类型, 不会受到匿名实体的类型修饰符的影响。也就是说这里decltype(first)
的类型是
const std::string
而不是引用。
这里贴下完整的预处理代码,可加深理解
#include <string>
#include <iostream>
#include <utility> // for std::move()
#include <vector>
using namespace std;
class Customer
{
private:
std::basic_string<char> first;
std::basic_string<char> last;
long val;
public:
inline Customer(std::basic_string<char> f, std::basic_string<char> l, long v)
: first{std::basic_string<char>{std::move(f)}}
, last{std::basic_string<char>{std::move(l)}}
, val{v}
{
}
inline std::basic_string<char> getFirst() const
{
return std::basic_string<char>(this->first);
}
inline std::basic_string<char> getLast() const
{
return std::basic_string<char>(this->last);
}
inline long getValue() const
{
return this->val;
}
// inline Customer(Customer &&) noexcept = default;
// inline ~Customer() noexcept = default;
};
void printCustomer(const Customer & t)
{
std::operator<<(std::operator<<(std::operator<<(std::operator<<(std::operator<<(std::cout, "Customer first = "), t.getFirst()), " ; last= "), t.getLast()), "; val= ").operator<<(t.getValue()).operator<<(std::endl);
}
template<>
struct std::tuple_size<Customer>
{
inline static constexpr const int value = 3;
};
template<>
struct std::tuple_element<2, Customer>
{
using type = long;
};
template<std::size_t Idx>
struct std::tuple_element<Idx, Customer>
{
using type = std::basic_string<char>;
};
template<std::size_t >
auto get(const Customer & c);
;
template<>
std::basic_string<char> get<0>(const Customer & c)
{
return c.getFirst();
}
template<>
std::basic_string<char> get<1>(const Customer & c)
{
return c.getLast();
}
template<>
long get<2>(const Customer & c)
{
return c.getValue();
}
template<std::size_t I>
auto get(const Customer & c)
{
/* PASSED: static_assert(I < 3); */
if constexpr(I == 0) {
return c.getFirst();
} else /* constexpr */ {
if constexpr(I == 1) {
return c.getLast();
} else /* constexpr */ {
return c.getValue();
}
}
}
int main()
{
std::vector<Customer, std::allocator<Customer> > cc = std::vector<Customer, std::allocator<Customer> >();
cc.push_back(std::vector<Customer>::value_type{std::basic_string<char>("Tim", std::allocator<char>()), std::basic_string<char>("Starr", std::allocator<char>()), 42});
cc.push_back(std::vector<Customer>::value_type{std::basic_string<char>("Tony", std::allocator<char>()), std::basic_string<char>("OMG", std::allocator<char>()), 43});
cc.push_back(std::vector<Customer>::value_type{std::basic_string<char>("Spny", std::allocator<char>()), std::basic_string<char>("MG", std::allocator<char>()), 43});
{
std::vector<Customer, std::allocator<Customer> > & __range1 = cc;
__gnu_cxx::__normal_iterator<Customer *, std::vector<Customer, std::allocator<Customer> > > __begin1 = __range1.begin();
__gnu_cxx::__normal_iterator<Customer *, std::vector<Customer, std::allocator<Customer> > > __end1 = __range1.end();
for(; __gnu_cxx::operator!=(__begin1, __end1); __begin1.operator++()) {
Customer const & __operator70 = __begin1.operator*();
const std::basic_string<char> && first = static_cast<const std::basic_string<char>>(get<0UL>(__operator70));
const std::basic_string<char> && last = static_cast<const std::basic_string<char>>(get<1UL>(__operator70));
const long && val = static_cast<const long>(get<2UL>(__operator70));
std::operator<<(std::operator<<(std::operator<<(std::operator<<(std::operator<<(std::cout, first), ' '), last), ": ").operator<<(val), '
');
}
}
return 0;
}
支持可写结构化绑定
实现tuple-like API
时可以返回non-const
引用,这样结构化绑定就带有写权限。 设想类Customer
提供了读写成员的API:
#include <string>
#include <utility> // for std::move()
class Customer {
private:
std::string first;
std::string last;
long val;
public:
Customer (std::string f, std::string l, long v)
: first{std::move(f)}, last{std::move(l)}, val{v} {
}
const std::string& firstname() const {
return first;
}
std::string& firstname() {
return first;
}
const std::string& lastname() const {
return last;
}
long value() const {
return val;
}
long& value() {
return val;
}
};
为了支持读写,我们需要为常量和非常量引用定义重载的getter:
#include <utility> // for tuple-like API
// 为类Customer提供tuple-like API来支持结构化绑定:
template<>
struct std::tuple_size<Customer> {
static constexpr int value = 3; // 有3个属性
};
template<>
struct std::tuple_element<2, Customer> {
using type = long; // 最后一个属性的类型是long
}
template<std::size_t Idx>
struct std::tuple_element<Idx, Customer> {
using type = std::string; // 其他的属性是string
}
// 定义特化的getter:
template<std::size_t I> decltype(auto) get(Customer& c) {
static_assert(I < 3);
if constexpr (I == 0) {
return c.firstname();
}
else if constexpr (I == 1) {
return c.lastname();
}
else { // I == 2
return c.value();
}
}
template<std::size_t I> decltype(auto) get(const Customer& c) {
static_assert(I < 3);
if constexpr (I == 0) {
return c.firstname();
}
else if constexpr (I == 1) {
return c.lastname();
}
else { // I == 2
return c.value();
}
}
template<std::size_t I> decltype(auto) get(Customer&& c) {
static_assert(I < 3);
if constexpr (I == 0) {
return std::move(c.firstname());
}
else if constexpr (I == 1) {
return std::move(c.lastname());
}
else { // I == 2
return c.value();
}
}
注意你必须提供这3个版本的特化来处理常量对象、非常量对象、可移动对象 。 为了能返回引用,你应该使用decltype(auto)
作为返回类型 。
这里我们又一次使用了编译期if
语句特性, 这可以让我们的getter
的实现变得更加简单。如果没有这个特性,我们必须写出所有的全特化版本,例如:
template<std::size_t> decltype(auto) get(const Customer& c);
template<std::size_t> decltype(auto) get(Customer& c);
template<std::size_t> decltype(auto) get(Customer&& c);
template<> decltype(auto) get<0>(const Customer& c) { return c.firstname(); }
template<> decltype(auto) get<0>(Customer& c) { return c.firstname(); }
template<> decltype(auto) get<0>(Customer&& c) { return c.firstname(); }
template<> decltype(auto) get<1>(const Customer& c) { return c.lastname(); }
template<> decltype(auto) get<1>(Customer& c) { return c.lastname(); }
...
再次强调,主函数模板声明必须和全特化版本拥有完全相同的签名(包括返回值)。 下面的代码不能通过编译:
template<std::size_t> decltype(auto) get(Customer& c);
template<> std::string& get<0>(Customer& c) { return c.firstname(); }
template<> std::string& get<1>(Customer& c) { return c.lastname(); }
template<> long& get<2>(Customer& c) { return c.value(); }
你现在可以对Customer
类使用结构化绑定了,并且还能通过绑定修改成员的值:
#include "structbind2.hpp"
#include <iostream>
int main()
{
Customer c{"Tim", "Starr", 42};
auto [f, l, v] = c;
std::cout << "f/l/v: " << f << ' ' << l << ' ' << v << '
';
// 通过引用修改结构化绑定
auto&& [f2, l2, v2] = c;
std::string s{std::move(f2)};
f2 = "Ringo";
v2 += 10;
std::cout << "f2/l2/v2: " << f2 << ' ' << l2 << ' ' << v2 << '
';
std::cout << "c: " << c.firstname() << ' '
<< c.lastname() << ' ' << c.value() << '
';
std::cout << "s: " << s << '
';
}
程序的输出如下:
f/l/v: Tim Starr 42
f2/l2/v2: Ringo Starr 52
c: Ringo Starr 52
s: Tim
带初始化的if
和switch
语句
if
和switch
语句现在允许在条件表达式里添加一条初始化语句。
例如,你可以写出如下代码:
#include <iostream>
using namespace std;
enum status {
Failed = 0,
success = 1,
};
status check() { return status::Failed; }
int main() {
if (status s = Failed; s != status::success) {
cout << "Failed = " << s << endl;
} else {
cout << "ok = " << s << endl;
;
}
return 0;
}
预处理代码如下
#include <iostream>
using namespace std;
enum status
{
Failed = static_cast<unsigned int>(0),
success = static_cast<unsigned int>(1)
};
status check()
{
return Failed;
}
int main()
{
{
status s = Failed;
if(static_cast<int>(s) != static_cast<int>(success)) {
std::operator<<(std::cout, "Failed = ").operator<<(static_cast<int>(s)).operator<<(std::endl);
} else {
std::operator<<(std::cout, "ok = ").operator<<(static_cast<int>(s)).operator<<(std::endl);
;
}
}
return 0;
}
输出结果如下:
Failed = 0
其中的初始化语句status s = Failed;
初始化了s
,s
将在整个if
语句中有效(包括else
分支里)。
带初始化的if
语句
在if
语句的条件表达式里定义的变量将在整个if
语句中有效
(包括 then 部分和 else 部分)。例如:
if (std::ofstream strm = getLogStrm(); coll.empty()) {
strm << "<no data>
";
}
else {
for (const auto& elem : coll) {
strm << elem << '
';
}
}
// strm不再有效
在整个if
语句结束时strm
的析构函数会被调用。
另一个例子是关于锁的使用,假设我们要在并发的环境中执行一些依赖某个条件的任务:
if (std::lock_guard<std::mutex> lg{collMutex}; !coll.empty()) {
std::cout << coll.front() << '
';
}
这个例子中,如果使用类模板参数推导,可以改写成如下代码:
if (std::lock_guard lg{collMutex}; !coll.empty()) {
std::cout << coll.front() << '
';
}
上面的代码等价于:
{
std::lock_guard<std::mutex> lg{collMutex};
if (!coll.empty()) {
std::cout << coll.front() << '
';
}
}
细微的区别在于前者中lg
在if
语句的作用域之内定义,和条件语句在相同的作用域。
注意这个特性的效果和传统for
循环里的初始化语句完全相同。
上面的例子中为了让lock_guard
生效,必须在初始化语句里明确声明一个变量名,
否则它就是一个临时变量,会在创建之后就立即销毁。因此,初始化一个没有变量名的临时
lock_guard
是一个逻辑错误,因为当执行到条件语句时锁就已经被释放了:
if (std::lock_guard<std::mutex>{collMutex}; // 运行时ERROR
!coll.empty()) { // 锁已经被释放了
std::cout << coll.front() << '
'; // 锁已经被释放了
}
原则上讲,使用简单的_
作为变量名就已经足够了:
if (std::lock_guard<std::mutex> _{collMutex}; // OK,但是...
!coll.empty()) {
std::cout << coll.front() << '
';
}
你也可以同时声明多个变量,并且可以在声明时初始化:
if (auto x = qqq1(), y = qqq2(); x != y) {
std::cout << "return values " << x << " and " << y << "differ
";
}
或者:
if (auto x{qqq1()}, y{qqq2()}; x != y) {
std::cout << "return values " << x << " and " << y << "differ
";
}
另一个例子是向map
或者unordered map
插入元素。
你可以像下面这样检查是否成功:
std::map<std::string, int> coll;
...
if (auto [pos, ok] = coll.insert({"new", 42}); !ok) {
// 如果插入失败,用pos处理错误
const auto& [key, val] = *pos;
std::cout << "already there: " << key << '
';
}
这里,我们用了结构化绑定给返回值的成员和pos
指向的值的成员声明了新的名称,
而不是直接使用first
和second
成员。在C++17之前,相应的处理代码必须像下面这样写:
auto ret = coll.insert({"new", 42});
if (!ret.second) {
// 如果插入失败,用ret.first处理错误
const auto& elem = *(ret.first);
std::cout << "already there: " << elem.first << '
';
}
注意这个拓展也适用于编译期if
语句特性。
带初始化的switch
语句
通过使用带初始化的switch
语句,我们可以在对条件表达式求值之前初始化一个对象/实体。
例如,我们可以先声明一个文件系统路径,然后再根据它的类别进行处理:
namespace fs = std::filesystem;
...
switch (fs::path p{name}; status(p).type()) {
case fs::file_type::not_found:
std::cout << p << " not found
";
break;
case fs::file_type::directory:
std::cout << p << ":
";
for (const auto& e : std::filesystem::directory_iterator{p}) {
std::cout << "- " << e.path() << '
';
}
break;
default:
std::cout << p << " exists
";
break;
}
这里,初始化的路径p
可以在整个switch
语句中使用。
内联变量
出于可移植性和易于整合的目的,在头文件中提供完整的类和库的定义是很重要的。
然而,在C++17
之前,只有当这个库既不提供也不需要全局对象的时候才可以这样做。
自从C++17
开始,你可以在头文件中以inline
的方式 定义 全局变量/对象:
class MyClass {
inline static std::string msg{"OK"}; // OK(自C++17起)
...
};
inline MyClass myGlobalObj; // 即使被多个CPP文件包含也OK
汇编代码如下
class MyClass
{
inline static std::basic_string<char, std::char_traits<char>, std::allocator<char> > msg = std::basic_string<char, std::char_traits<char>, std::allocator<char> >{"OK", std::allocator<char>()};
};
只要一个编译单元内没有重复的定义即可。此例中的定义即使被多个编译单元使用,也会指向同一个对象。
动机
在C++
里不允许在类里初始化非常量静态成员:
class MyClass {
static std::string msg{"OK"}; // 编译期ERROR
...
};
可以在类定义的外部定义并初始化非常量静态成员,但如果被多个CPP
文件同时包含的话又会引发新的错误:
class MyClass {
static std::string msg;
...
};
std::string MyClass::msg{"OK"}; // 如果被多个CPP文件包含会导致链接ERROR
根据 一次定义原则 (ODR),一个变量或实体的定义只能出现在一个编译单元内——除非该变量或实体被定义为inline
的。
即便使用预处理来进行保护也没有用:
#ifndef MYHEADER_HPP
#define MYHEADER_HPP
class MyClass {
static std::string msg;
...
};
std::string MyClass::msg{"OK"}; // 如果被多个CPP文件包含会导致链接ERROR
#endif
问题并不在于头文件是否可能被重复包含多次,而是两个不同的CPP
文件都包含了这个头文件,因而都定义了MyClass::msg
。
出于同样的原因,如果你在头文件中定义了一个类的实例对象也会出现相同的链接错误:
class MyClass {
...
};
MyClass myGlobalObject; // 如果被多个CPP文件包含会导致链接ERROR
解决方法
对于一些场景,这里有一些解决方法:
- 你可以在一个
class/struct
的定义中初始化数字或枚举类型的常量静态成员:
class MyClass {
static const bool trace = false; // OK,字面类型
...
};
然而,这种方法只能初始化字面类型,例如基本的整数、浮点数、指针类型或者用常量表达式初始化了所有内部非静态成员的类,并且该类不能有用户自定义的或虚的析构函数。
另外,如果你需要获取这个静态常量成员的地址(例如你想定义一个它的引用)的话,那么你必须在那个编译单元内定义它并且不能在其他编译单元内再次定义。
- 你可以定义一个返回
static
的局部变量的内联函数:
inline std::string& getMsg() {
static std::string msg{"OK"};
return msg;
}
- 你可以定义一个返回该值的
static
的成员函数:
class MyClass {
static std::string& getMsg() {
static std::string msg{"OK"};
return msg;
}
...
};
- 你可以使用变量模板(自C++14起):
template<typename T = std::string>
T myGlobalMsg{"OK"};
- 你可以为静态成员定义一个模板类:
template<typename = void>
class MyClassStatics
{
static std::string msg;
};
template<typename T>
std::string MyClassStatics<T>::msg{"OK"};
然后继承它:
class MyClass : public MyClassStatics<>
{
...
};
然而,所有这些方法都会导致签名重载,可读性也会变差,使用该变量的方式也变得不同。另外,全局变量的初始化可能会推迟到第一次使用时。所以那些假设变量一开始就已经初始化的写法是不可行的(例如使用一个对象来监控整个程序的过程)。
使用
现在,使用了inline
修饰符之后,即使定义所在的头文件被多个CPP
文件包含,也只会有一个全局对象:
class MyClass {
inline static std::string msg{"OK"}; // 自从C++17起OK
...
};
inline MyClass myGlobalObj; // 即使被多个CPP文件包含也OK
这里使用的inline
和函数声明时的inline
有相同的语义:
- 它可以在多个编译单元中定义,只要所有定义都是相同的。
- 它必须在每个使用它的编译单元中定义
将变量定义在头文件里,然后多个CPP
文件再都包含这个头文件,就可以满足上述两个要求。程序的行为就好像只有一个变量一样。
你甚至可以利用它在头文件中定义原子类型:
inline std::atomic<bool> ready{false};
像通常一样,** 当你定义std::atomic
类型的变量时必须进行初始化。**
注意你仍然必须确保在你初始化内联变量之前它们的类型必须是完整的。
例如,如果一个struct
或者class
有一个自身类型的static
成员,那么这个成员只能在类型声明之后再进行定义:
struct MyType {
int value;
MyType(int i) : value{i} {
}
// 一个存储该类型最大值的静态对象
static MyType max; // 这里只能进行声明
...
};
inline MyType MyType::max{0};
另一个使用内联变量的例子见追踪所有new
调用的头文件。
constexpr static
成员现在隐含inline
对于静态成员,constexpr
修饰符现在隐含着inline
。自从C++17
起,如下声明 定义 了静态数据成员n
:
struct D {
static constexpr int n = 5; // C++11/C++14: 声明
// 自从C++17起: 定义
}
和下边的代码等价:
struct D {
inline static constexpr int n = 5;
};
注意在C++17
之前,你就可以只有声明没有定义。考虑如下声明:
struct D {
static constexpr int n = 5;
};
如果不需要D::n
的定义的话只有上面的声明就够了,
例如当D::n
以值传递时:
std::cout << D::n; // OK,ostream::operator<<(int)只需要D::n的值
如果D::n
以引用传递到一个非内联函数,并且该函数调用没有被优化掉的话,该调用将会导致错误。例如:
int twice(const int& i);
std::cout << twice(D::n); // 通常会导致ERROR
这段代码违反了 一次定义原则 (ODR)。如果编译器进行了优化,那么这段代码可能会像预期一样工作,也可能会因为缺少定义导致链接错误。如果不进行优化,那么几乎肯定会因为缺少D::n
的定义而导致错误。
如果创建一个D::n
的指针那么更可能因为缺少定义导致链接错误(但在某些编译模式下仍然可能正常编译):
const int* p = &D::n; // 通常会导致ERROR
因此在C++17之前,你必须在一个编译单元内定义D::n
:
constexpr int D::n; // C++11/C++14: 定义
// 自从C++17起: 多余的声明(已被废弃)
现在当使用C++17
进行构建时,类中的声明本身就成了定义,因此即使没有上边的定义,上面的所有例子现在也都可以正常工作。上边的定义现在仍然有效但已经成了废弃的多余声明。
内联变量和thread_local
通过使用thread_local
可以为每个线程创建一个内联变量:
struct ThreadData {
inline static thread_local std::string name; // 每个线程都有自己的name
...
};
inline thread_local std::vector<std::string> cache; // 每个线程都有一份cache
作为一个完整的例子,考虑如下头文件:
#include <string>
#include <iostream>
struct MyData {
inline static std::string gName = "global"; // 整个程序中只有一个
inline static thread_local std::string tName = "tls"; // 每个线程有一个
std::string lName = "local"; // 每个实例有一个
...
void print(const std::string& msg) const {
std::cout << msg << '
';
std::cout << "- gName: " << gName << '
';
std::cout << "- tName: " << tName << '
';
std::cout << "- lName: " << lName << '
';
}
};
inline thread_local MyData myThreadData; // 每个线程一个对象
你可以在包含main()
的编译单元内使用它:
#include "inlinethreadlocal.hpp"
#include <thread>
void foo();
int main()
{
myThreadData.print("main() begin:");
myThreadData.gName = "thraed1 name";
myThreadData.tName = "thread1 name";
myThreadData.lName = "thread1 name";
myThreadData.print("main() later:");
std::thread t(foo);
t.join();
myThreadData.print("main() end:");
}
你也可以在另一个定义了foo()
函数的编译单元内使用这个头文件,这个函数会在另一个线程中被调用:
#include "inlinethreadlocal.hpp"
void foo()
{
myThreadData.print("foo() begin:");
myThreadData.gName = "thread2 name";
myThreadData.tName = "thread2 name";
myThreadData.lName = "thread2 name";
myThreadData.print("foo() end:");
}
程序的输出如下:
main() begin:
- gName: global
- tName: tls
- lName: local
main() later:
- gName: thread1 name
- tName: thread1 name
- lName: thread1 name
foo() begin:
- gName: thread1 name
- tName: tls
- lName: local
foo() end:
- gName: thread2 name
- tName: thread2 name
- lName: thread2 name
main() end:
- gName: thread2 name
- tName: thread1 name
- lName: thread1 name
聚合体扩展
C++
有很多初始化对象的方法。其中之一叫做 聚合体初始化(aggregate initialization) ,这是聚合体专有的一种初始化方法。从C
语言引入的初始化方式是用花括号括起来的一组值来初始化类:
struct Data {
std::string name;
double value;
};
Data x = {"test1", 6.778};
自从C++11起,可以忽略等号:
Data x{"test1", 6.778};
自从C++17起,聚合体可以拥有基类。也就是说像下面这种从其他类派生出的子类也可以使用这种初始化方法:
struct MoreData : Data {
bool done;
}
MoreData y{{"test1", 6.778}, false};
如你所见,聚合体初始化时可以用一个子聚合体初始化来初始化类中来自基类的成员。另外,你甚至可以省略子聚合体初始化的花括号:
MoreData y{"test1", 6.778, false};
预处理代码:
MoreData y = {{std::basic_string<char, std::char_traits<char>, std::allocator<char> >("test1", std::allocator<char>()), 6.7779999999999996}, false};
这样写将遵循嵌套聚合体初始化时的通用规则,你传递的实参被用来初始化哪一个成员取决于它们的顺序。
动机
如果没有这个特性,那么所有的派生类都不能使用聚合体初始化,这意味着你要像下面这样定义构造函数:
struct Cpp14Data : Data {
bool done;
Cpp14Data (const std::string& s, double d, bool b) : Data{s, d}, done{b} {
}
};
Cpp14Data y{"test1", 6.778, false};
现在我们不再需要定义任何构造函数就可以做到这一点。我们可以直接使用嵌套花括号的语法来实现初始化,如果给出了内层初始化需要的所有值就可以省略内层的花括号:
MoreData x{{"test1", 6.778}, false}; // 自从C++17起OK
MoreData y{"test1", 6.778, false}; // OK
注意因为现在派生类也可以是聚合体,所以其他的一些初始化方法也可以使用:
MoreData u; // OOPS:value/done未初始化
MoreData z{}; // OK: value/done初始化为0/false
如果觉得这样很危险,可以使用成员初始值:
struct Data {
std::string name;
double value{0.0};
};
struct Cpp14Data : Data {
bool done{false};
};
或者,继续提供一个默认构造函数。
使用
聚合体初始化的一个典型应用场景是对一个派生自C风格结构体并且添加了新成员的类进行初始化。例如:
struct Data {
const char* name;
double value;
};
struct CppData : Data {
bool critical;
void print() const {
std::cout << '[' << name << ',' << value << "]
";
}
};
CppData y{{"test1", 6.778}, false};
y.print();
这里,内层花括号里的参数被传递给基类Data
。
注意你可以跳过初始化某些值。在这种情况下,跳过的成员将会进行默认初始化(基础类型会被初始化为0
、false
或者nullptr
,类类型会默认构造)。
例如:
CppData x1{}; // 所有成员默认初始化为0值
CppData x2{{"msg"}} // 和{{"msg", 0.0}, false}等价
CppData x3{{}, true}; // 和{{nullptr, 0.0}, true}等价
CppData x4; // 成员的值未定义
注意使用空花括号和不使用花括号完全不同:
x1
的定义会把所有成员默认初始化为0值,因此字符指针name
被初始化为nullptr
,double
类型的value
初始化为0.0
,bool
类型的flag
初始化为false
。x4
的定义没有初始化任何成员。所有成员的值都是未定义的。
你也可以从非聚合体派生出聚合体。例如:
struct MyString : std::string {
void print() const {
if (empty()) {
std::cout << "<undefined>
";
}
else {
std::cout << c_str() << '
';
}
}
};
MyString x{{"hello"}};
MyString y{"world"};
注意这不是通常的具有多态性的public
继承,因为std::string
没有虚成员函数,你需要避免混淆这两种类型。你甚至可以从多个基类和聚合体中派生出聚合体:
template<typename T>
struct D : std::string, std::complex<T>
{
std::string data;
};
你可以像下面这样使用和初始化:
D<float> s{{"hello"}, {4.5, 6.7}, "world"}; // 自从C++17起OK
D<float> t{"hello", {4.5, 6.7}, "world"}; // 自从C++17起OK
std::cout << s.data; // 输出:"world"
std::cout << static_cast<std::string>(s); // 输出:"hello"
std::cout << static_cast<std::complex<float>>(s); //输出:(4.5,6.7)
内部嵌套的初值列表将按照继承时基类声明的顺序传递给基类。这个新的特性也可以帮助我们用很少的代码定义重载的lambda
。
定义
总的来说,在C++17
中满足如下条件之一的对象被认为是 聚合体 :
-
是一个数组
-
或者是一个满足如下条件的 类类型 (
class
、struct
、union
): -
没有用户定义的和
explicit
的构造函数 -
没有使用
using
声明继承的构造函数 -
没有
private
和protected
的非静态数据成员 -
没有
virtual
函数 -
没有
virtual, private, protected
的基类
然而,要想使用聚合体初始化来 初始化 聚合体,那么还需要满足如下额外的约束:
- 基类中没有
private
或者protected
的成员 - 没有
private
或者protected
的构造函数
下一节就有一个因为不满足这些额外约束导致编译失败的例子。
C++17
引入了一个新的类型特征is_aggregate<>
来测试一个类型是否是聚合体:
template<typename T>
struct D : std::string, std::complex<T> {
std::string data;
};
D<float> s{{"hello"}, {4.5, 6.7}, "world"}; // 自从C++17起OK
std::cout << std::is_aggregate<decltype(s)>::value; // 输出1(true)
向后的不兼容性
注意下面的例子不能再通过编译:
struct Derived;
struct Base {
friend struct Derived;
private:
Base() {
}
};
struct Derived : Base {
};
int main()
{
Derived d1{}; // 自从C++17起ERROR error: 'Base::Base()' is private within this context
Derived d2; // 仍然OK(但可能不会初始化)
}
在C++17
之前,Derived
不是聚合体。因此
Derived d1{};
会调用Derived
隐式定义的默认构造函数,这个构造函数会调用基类Base
的构造函数。尽管基类的默认构造函数是private
的,但在派生类的构造函数里调用它也是有效的,因为派生类被声明为友元类。自从C++17
起,例子中的Derived
是一个聚合体,所以它没有隐式的默认构造函数(构造函数没有使用using
声明继承)。因此,d1
的初始化将是一个聚合体初始化,如下表达式:std::is_aggregate<Derived>::value
将返回true
。然而,因为基类有一个private
的构造函数(见上一节)所以不能使用花括号来初始化。这和派生类是否是基类的友元无关。
强制省略拷贝或传递未实质化的对象
- 从技术上讲,
C++17
引入了一个新的规则:当以值传递或返回一个临时对象的时候必须省略对该临时对象的拷贝。 - 从效果上讲,我们实际上是传递了一个 未实质化的对象(unmaterialized object) 。
接下来首先从技术上介绍这个特性,之后再介绍实际效果和术语 materialization 。
动机
自从第一次标准开始,C++
就允许在某些情况下 省略(elision) 拷贝操作,即使这么做可能会影响程序的运行结果(例如,拷贝构造函数里的一条打印语句可能不会再执行)。当用临时对象初始化一个新对象时就很容易出现这种情况,尤其是当一个函数以值传递或返回临时对象的时候。
例如:
class MyClass
{
...
};
void foo(MyClass param) { // param用传递进入的实参初始化
...
}
MyClass bar() {
return MyClass{}; // 返回临时对象
}
int main()
{
foo(MyClass{}); // 传递临时对象来初始化param
MyClass x = bar(); // 使用返回的临时对象初始化x
foo(bar()); // 使用返回的临时对象初始化param
}
然而,因为这种优化并不是强制性的,所以例子中的情况要求该对象必须有隐式或显式的拷贝/移动构造函数。也就是说,**尽管因为优化的原因大多数情况下并不会真的调用拷贝/移动函数,但它们必须存在。**因此,如果将上例中的MyClass
类换成如下定义则上例代码将不能通过编译:
class MyClass
{
public:
...
// 没有拷贝/移动构造函数的定义
MyClass(const MyClass&) = delete;
MyClass(MyClass&&) = delete;
...
};
只要没有拷贝构造函数就足以产生错误了,因为移动构造函数只有在没有用户声明的拷贝构造函数(或赋值操作符或析构函数)时才会隐式存在。(上例中只需要将拷贝构造函数定义为delete
的就不会再有隐式定义的移动构造函数)
自从C++17
起用临时变量初始化对象时省略拷贝变成了强制性的。事实上,之后我将会看到我们我们传递为参数或者作为返回值的临时变量将会被用来 实质化(materialize) 一个新的对象。这意味着即使上例中的MyClass
完全不允许拷贝,示例代码也能成功编译。然而,注意其他可选的省略拷贝的场景仍然是可选的,这些场景中仍然需要一个拷贝或者移动构造函数。例如:
MyClass foo()
{
MyClass obj;
...
return obj; // 仍然需要拷贝/移动构造函数的支持
}
这里,foo()
中有一个具名的变量obj
(当使用它时它是 左值(lvalue) )。因此, 具名返回值优化(named return value optimization) (NRVO)会生效,然而该优化仍然需要拷贝/移动支持。当obj
是形参的时候也会出现这种情况:
MyClass bar(MyClass obj) // 传递临时变量时会省略拷贝
{
...
return obj; // 仍然需要拷贝/移动支持
}
当传递一个临时变量(也就是 纯右值(prvalue) )作为实参时不再需要拷贝/移动,但如果返回这个参数的话仍然需要拷贝/移动支持因为返回的对象是具名的。作为变化的一部分,术语值类型体系的含义也做了很多修改和说明。
作用
这个特性的一个显而易见的作用就是减少拷贝会带来更好的性能。尽管很多主流编译器之前就已经进行了这种优化,但现在这一行为有了标准的 保证 。尽管移动语义能显著的减少拷贝开销,但如果直接不拷贝还是能带来很大的性能提升(例如当对象有很多基本类型成员时移动语义还是要拷贝每个成员)。另外这个特性可以减少输出参数的使用,转而直接返回一个值(前提是这个值直接在返回语句里创建)。
另一个作用是可以定义一个 总是 可以工作的工厂函数,因为现在它甚至可以返回不允许拷贝或移动的对象。例如,考虑如下泛型工厂函数:
#include <utility>
template <typename T, typename... Args>
T create(Args&&... args)
{
...
return T{std::forward<Args>(args)...};
}
这个工厂函数现在甚至可以用于std::atomic<>
这种既没有拷贝又没有移动构造函数的类型:
#include "factory.hpp"
#include <memory>
#include <atomic>
int main()
{
int i = create<int>(42);
std::unique_ptr<int> up = create<std::unique_ptr<int>>(new int{42});
std::atomic<int> ai = create<std::atomic<int>>(42);
}
另一个效果就是对于移动构造函数被显式删除的类,现在也可以返回临时对象来初始化新的对象:
class CopyOnly {
public:
CopyOnly() {
}
CopyOnly(int) {
}
CopyOnly(const CopyOnly&) = default;
CopyOnly(CopyOnly&&) = delete; // 显式delete
};
CopyOnly ret() {
return CopyOnly{}; // 自从C++17起OK
}
CopyOnly x = 42; // 自从C++17起OK
在C++17
之前x
的初始化是无效的,因为 拷贝初始化 (使用=
初始化)需要把42
转换为一个临时对象,然后要用这个临时对象初始化x
原则上需要移动构造函数,尽管它可能不会被调用。(只有当移动构造函数 不是 用户自定义时拷贝构造函数才能作为移动构造函数的备选项)
更明确的值类型体系
用临时变量初始化新对象时强制省略临时变量拷贝的提议的一个副作用就是,为了支持这个提议, 值类型体系(value category) 进行了很多修改。
值类型体系
C++
中的每一个表达式都有值类型。这个类型描述了表达式的值可以用来做什么。
历史上的值类型体系
C++
以前只有从C语言继承而来的 左值(lvalue) 和 右值(rvalue) ,根据赋值语句划分:
x = 42;
这里表达式x
是 左值 因为它可以出现在赋值等号的左边,42
是 右值 因为它只能出现在表达式的右边。然而,当ANSI-C出现之后事情就变得更加复杂了,因为如果x
被声明为const int
的话它将不能出现在赋值号左边,但它仍然是一个(不能修改的)左值。
之后,C++11
又引入了可移动的对象。从语义上分析,可移动对象只能出现在赋值号右侧但它却可以被修改,因为赋值号能移走它们的值。出于这个原因,类型 到期值(xvalue) 被引入,原来的右值被重命名为 纯右值(prvalue) 。
从C++11起的值类型体系
我们有了核心的值类型体系 lvalue(左值) ,prvalue(纯右值) (“pure rvalue”)和 xvalue(到期值) (“eXpiring value”)。复合的值类型体系有 glvalue(广义左值) (“generalized lvalue”,它是 lvalue 和 xvalue 的复合)和 rvalue(右值) ( xvalue 和 prvalue 的复合)。
lvalue(左值) 的例子有:
- 只含有单个变量、函数或成员的表达式
- 只含有字符串字面量的表达式
- 内建的一元
*
运算符(解引用运算符)的结果 - 一个返回lvalue(左值)引用( type& )的函数的返回值
prvalue(纯右值) 的例子有: - 除字符串字面量和用户自定义字面量之外的字面量组成的表达式
- 内建的一元
&
运算符(取地址运算符)的运算结果 - 内建的数学运算符的结果
- 一个返回值的函数的返回值
- 一个lambda表达式
xvalue(到期值) 的例子有: - 一个返回rvalue(右值)引用( type&& )的函数的返回值
(尤其是std::move()
的返回值) - 把一个对象转换为rvalue(右值)引用的操作的结果
简单来讲: - 所有用作表达式的变量名都是 lvalue(左值) 。
- 所有用作表达式的字符串字面量是 lvalue(左值) 。
- 所有其他的字面量(
4.2, true, nullptr
)是 prvalue(纯右值) 。 - 所有临时对象(尤其是以值返回的对象)是 prvalue(纯右值) 。
std::move()
的结果是一个 xvalue(到期值)
例如:
class X {
};
X v;
const X c;
void f(const X&); // 接受任何值类型
void f(X&&); // 只接受prvalue和xvalue,但是相比上边的版本是更好的匹配
f(v); // 给第一个f()传递了一个可修改lvalue
f(c); // 给第一个f()传递了不可修改的lvalue
f(X()); // 给第二个f()传递了一个prvalue
f(std::move(v)); // 给第二个f()传递了一个xvalue
值得强调的一点是严格来讲glvalue(广义左值)、prvalue(纯右值)、xvalue(到期值)是描述表达式的术语而 不是 描述值的术语(这意味着这些术语其实是误称)。例如,一个变量自身并不是左值,只含有这个变量的表达式才是左值:
int x = 3; // 这里,x是一个变量,不是一个左值
int y = x; // 这里,x是一个左值
在第一条语句中,3
是一个纯右值,用它初始化了变量(不是左值)x
。在第二条语句中,x
是一个左值(该表达式的求值结果指向一个包含有数值3
的对象)。左值x
被转换为一个纯右值,然后用来初始化y
。
自从C++17
起的值类型体系
C++17
再次明确了值类型体系.
理解值类型体系的关键是现在广义上来说,我们只有两种类型的表达式:
- glvaue: 描述对象或函数 位置 的表达式
- prvalue: 用于 初始化 的表达式
而 xvalue 可以认为是一种特殊的位置,它代表一个资源可以被回收利用的对象(通常是因为该对象的生命周期即将结束)。
C++17引入了一个新的术语:(临时对象的) 实质化(materialization) ,目前prvalue就是一种临时对象。因此, 临时对象实质化转换(temporary materialization conversion) 是一种prvalue到xvalue的转换。
在任何情况下prvalue出现在需要glvalue(lvalue或者xvalue)的地方都是有效的,此时会创建一个临时对象并用该prvalue来初始化(注意prvalue主要就是用来初始化的值)。然后该prvalue会被临时创建的xvalue类型的临时对象替换。因此上面的例子严格来讲是这样的:
void f(const X& p); // 接受一个任何值类型体系的表达式
// 但实际上需要一个glvalue
f(X()); // 传递了一个prvalue,该prvalue实质化为xvalue
因为这个例子中的f()
的形参是一个引用,所以它需要glvaue类型的实参。然而,表达式X()
是一个prvalue。此时“临时变量实质化”规则会产生作用,表达式X()
会“转换为”一个xvalue类型的临时对象。
注意实质化的过程中并没有创建新的/不同的对象。左值引用p
仍然 绑定到 xvalue和prvalue,尽管后者现在会转换为一个xvalue。
因为prvalue不再是对象而是可以被用来初始化对象的表达式,所以当使用prvalue来初始化对象时不再需要prvalue是可移动的,进而省略临时变量拷贝的特性可以完美实现。我们现在只需要简单的传递初始值,然后它会被自动 实质化 来初始化新对象。
未实质化的返回值传递
所有以值返回临时对象(prvalue)的过程都是在传递未实质化的返回值:
- 当我们返回一个非字符串字面量的字面量时:
int f1() { // 以值返回int
return 42;
}
- 当我们用
auto
或类型名作为返回类型并返回一个临时对象时:
auto f2() { // 以值返回退化的类型
...
return MyType{...};
}
- 当使用
decltype(auto)
作为返回类型并返回临时对象时:
decltype(auto) f3() { // 返回语句中以值返回临时对象
...
return MyType{...}
}
注意当初始化表达式(此处是返回语句)是一个创建临时对象(prvalue)的表达式时decltype(auto)
将会推导出值类型。因为我们在这些场景中都是以值返回一个prvalue,所以我们完全不需要任何拷贝/移动。
lambda
表达式扩展
C++11
引入的lambda
和C++14
引入的泛型lambda
是一个很大的成功。它允许我们将函数作为参数传递,这让我们能更轻易的指明一种行为。
C++17
扩展了lambda
表达式的应用场景:
- 在常量表达式中使用(也就是在编译期间使用)
- 在需要当前对象的拷贝时使用(例如,当在不同的线程中调用
lambda
时)
constexpr
lambda
自从C++17
起,lambda
表达式会尽可能的隐式声明constexpr
。也就是说,任何只使用有效的编译期上下文(例如,只有字面量,没有静态变量,没有虚函数,没有try/catch
,没有new/delete
的上下文)的lambda
都可以被用于编译期。
例如,你可以使用一个lambda
表达式计算参数的平方,并将计算结果用作std::array<>
的大小,即使这是一个编译期的参数:
auto squared = [](auto val) { // 自从C++17起隐式constexpr
return val*val;
};
std::array<int, squared(5)> a; // 自从C++17起OK => std::array<int, 25>
c++17
预编译的代码如下:
class __lambda_7_17
{
public:
template<class type_parameter_0_0>
inline /*constexpr */ auto operator()(type_parameter_0_0 val) const
{
return val * val;
}
#ifdef INSIGHTS_USE_TEMPLATE
template<>
inline /*constexpr */ int operator()<int>(int val) const
{
return val * val;
}
#endif
private:
template<class type_parameter_0_0>
static inline /*constexpr */ auto __invoke(type_parameter_0_0 val)
{
return __lambda_7_17{}.operator()<type_parameter_0_0>(val);
}
};
__lambda_7_17 squared = __lambda_7_17{};
std::array<int, 25> a = std::array<int, 25>();
使用编译期上下文中不允许的特性将会使lambda
失去成为constexpr
的能力,不过你仍然可以在运行时上下文中使用lambda
:
auto squared2 = [](auto val) { // 自从C++17起隐式constexpr
static int calls = 0; // OK,但会使该lambda不能成为constexpr
...
return val*val;
};
std::array<int, squared2(5)> a; // ERROR:在编译期上下文中使用了静态变量
std::cout << squared2(5) << '
'; // OK
报错信息如下:
<source>:11:29: error: 'main()::<lambda(auto:1)> [with auto:1 = int]' called in a constant expression
11 | std::array<int, squared2(5)> a; // ERROR:在编译期上下文中使用了静态变量
| ~~~~~~~~^~~
<source>:7:21: note: 'main()::<lambda(auto:1)> [with auto:1 = int]' is not usable as a 'constexpr' function because:
7 | auto squared2 = [](auto val) { // 自从C++17起隐式constexpr
| ^
<source>:8:20: error: 'calls' defined 'static' in 'constexpr' context
8 | static int calls = 0; // OK,但会使该lambda不能成为constexpr
| ^~~~~
<source>:11:29: note: in template argument for type 'long unsigned int'
11 | std::array<int, squared2(5)> a; // ERROR:在编译期上下文中使用了静态变量
| ~~~~~~~~^~~
为了确定一个lambda
是否能用于编译期,你可以将它声明为constexpr
:
auto squared3 = [](auto val) constexpr { // 自从C++17起OK
return val*val;
};
预处理代码如下:
class __lambda_7_21
{
public:
template<class type_parameter_0_0>
inline /*constexpr */ auto operator()(type_parameter_0_0 val) const
{
return val * val;
}
#ifdef INSIGHTS_USE_TEMPLATE
template<>
inline /*constexpr */ int operator()<int>(int val) const
{
return val * val;
}
#endif
private:
template<class type_parameter_0_0>
static inline /*constexpr */ auto __invoke(type_parameter_0_0 val)
{
return __lambda_7_21{}.operator()<type_parameter_0_0>(val);
}
public:
// /*constexpr */ __lambda_7_21() = default;
};
如果指明返回类型的话,语法看起来像下面这样:
auto squared3i = [](int val) constexpr -> int { // 自从C++17起OK
return val*val;
};
预处理代码如下:
class __lambda_11_22
{
public:
inline /*constexpr */ int operator()(int val) const
{
return val * val;
}
using retType_11_22 = auto (*)(int) -> int;
inline constexpr operator retType_11_22 () const noexcept
{
return __invoke;
};
private:
static inline /*constexpr */ int __invoke(int val)
{
return __lambda_11_22{}.operator()(val);
}
public:
// /*constexpr */ __lambda_11_22() = default;
};
关于constexpr
函数的规则也适用于lambda
:如果一个lambda
在运行时上下文中使用,那么相应的函数体也会在运行时才会执行。然而,如果在声明了constexpr
的lambda
内使用了编译期上下文中不允许的特性将会导致编译错误:
auto squared4 = [](auto val) constexpr {
static int calls = 0; // ERROR:在编译期上下文中使用了静态变量
...
return val*val;
};
一个隐式或显式的constexpr
lambda
的函数调用符也是constexpr
。也就是说,如下:
auto squared = [](auto val) { // 从C++17起隐式constexpr
return val*val;
};
将会被转换为如下 闭包类型(closure type) :
class CompilerSpecificName {
public:
...
template<typename T>
constexpr auto operator() (T val) const {
return val*val;
}
};
注意,这里生成的闭包类型的函数调用运算符自动声明为constexpr
。自从C++17
起,如果lambda被显式或隐式地定义为constexpr
,那么生成的函数调用运算符将自动是constexpr
。
注意如下定义:
auto squared1 = [](auto val) constexpr { // 编译期lambda调用
return val*val;
};
和如下定义:
constexpr auto squared2 = [](auto val) { // 编译期初始化squared2
return val*val;
};
是不同的。如下是预处理代码
#include <array>
#include <iostream>
using namespace std;
int main()
{
class __lambda_7_21
{
public:
template<class type_parameter_0_0>
inline /*constexpr */ auto operator()(type_parameter_0_0 val) const
{
return val * val;
}
private:
template<class type_parameter_0_0>
static inline /*constexpr */ auto __invoke(type_parameter_0_0 val)
{
return __lambda_7_21{}.operator()<type_parameter_0_0>(val);
}
public:
// /*constexpr */ __lambda_7_21() = default;
};
__lambda_7_21 squared1 = __lambda_7_21{};
class __lambda_10_30
{
public:
template<class type_parameter_0_0>
inline /*constexpr */ auto operator()(type_parameter_0_0 val) const
{
return val * val;
}
private:
template<class type_parameter_0_0>
static inline /*constexpr */ auto __invoke(type_parameter_0_0 val)
{
return __lambda_10_30{}.operator()<type_parameter_0_0>(val);
}
public:
// /*constexpr */ __lambda_10_30() = default;
};
constexpr const __lambda_10_30 squared2 = __lambda_10_30{};
return 0;
}
第一个例子中如果(只有)lambda
是constexpr
那么它可以被用于编译期,但是squared1
可能直到运行期才会被初始化,这意味着如果静态初始化顺序很重要那么可能导致问题(例如,可能会导致 static initialization order fiasco )。如果用lambda初始化的闭包对象constexpr
,那么该对象将在程序开始时就初始化,但lambda可能还是只能在运行时使用。因此,可以考虑使用如下定义:
constexpr auto squared = [](auto val) constexpr {
return val*val;
};
使用constexpr
lambda
这里有一个使用constexpr
lambda
的例子。假设我们有一个字符序列的哈希函数,这个函数迭代字符串的每一个字符反复更新哈希值:
auto hashed = [](const char* str) {
std::size_t hash = 5381; // 初始化哈希值
while (*str != '