A Complete Guide to C++17 - Basic Features

basic features

structured binding

Structured bindings allow you to instantiate multiple entities at the same time using elements or members of an object .
For example, suppose you define a struct with two distinct members:

struct MyStruct {
    
    
    int i = 0;
    std::string s;
};
MyStruct ms;

You can directly bind the two members of this structure to the new variable name by declaring:

auto [u, v] = ms;

Here, the way variables uand vare declared is called structured binding .
Each of the following declaration styles is supported:

auto [u2, v2] {
    
    ms};
auto [u3, v3] (ms);

Structured bindings are useful for functions that return structs or arrays . For example, consider a function that returns a struct:

MyStruct getStruct() {
    
    
    return MyStruct{
    
    42, "hello"};
}

You can directly assign the returned data members to two new local variables:

auto [id, val] = getStruct();  // id和val分别是返回结构体的i和s成员

In this example, idand are the and members valof the returned structure, respectively . Their types are and , respectively , and can be used as two different objects:isintstd::string

if (id > 30) {
    
    
    std::cout << val;
}

The advantage of this is that members can be accessed directly. In addition, binding values ​​to variable names that reflect semantics can make code more readable.
The code below demonstrates the dramatic improvement that comes with using structured bindings. Elements that are
traversed without using structured binding need to be written like this:std::map<>

for (const auto& elem : mymap) {
    
    
    std::cout << elem.first << ": " << elem.second << '\n';
}

mapThe type of the elements is the type composed of keyand , and the two members of the type are and . In the above example we had to use member names to access and , and by using structured bindings, the readability of the code can be greatly improved :valuestd::pairfirstsecondkeyvalue

for (const auto& [key, val] : mymap) {
    
    
    std::cout << key << ": " << val << '\n';
}

In the above example, we can use the variable name that accurately reflects the semanticskey to directly access the and members of each element value.

Whisper

The new variable names introduced during structured binding actually point to members/elements of this anonymous object .

Bind to an anonymous entity (you can directly see the preprocessing code for understanding)

The auto [u, v] = ms;precise interpretation should be: we msinitialize a new entity with and make the and ein the structured binding become the aliases of the members, similar to the following definition:uve

auto e = ms;
aliasname u = e.i;
aliasname v = e.s;

This is meant uto be an alias for members of vjust a local copy of . msHowever, we didn't edeclare a name for it, so we can't directly access this anonymous object. Note uthat and vare not references to e.iand e.s(but rather their aliases).

  • decltype(u)The result is ithe type of the member,
  • declytpe(v)The result is sthe type of the member.

Thus std::cout << u << ' ' << v << '\n';prints e.iand ( copies of and e.srespectively ).ms.ims.s

ehas the same lifecycle as the structured binding, and eis automatically destroyed when the structured binding goes out of scope.

Also, unless references are used, modifying variables used for initialization does not affect variables introduced by structured bindings (and vice versa):

MyStruct ms{
    
    42, "hello"};
auto [u, v] = ms;
ms.i = 77;
std::cout << u;     // 打印出42
u = 99;
std::cout << ms.i;  // 打印出77

In this example uand ms.ihave different memory addresses.

The rules are the same when using structured binding to bind return values. For example, auto [u, v] = getStruct();the behavior of initialization is equivalent to the fact that we getStruct()initialize a new entity with the return value , and ethen the structurally bound sum becomes the alias of the two members, similar to the following definition:uve

auto e = getStruct();
aliasname u = e.i;
aliasname v = e.s;

That is, structured binding binds to a new entity erather than directly to the return value.

Anonymous entities ealso follow the usual memory alignment rules , and each variable of the structural binding will be aligned according to the type of the corresponding member.

Example:

#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;}The assembly code that can be seen is as follows

  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]

The preprocessing code is as follows, the attention here MyStruct __ms16 = MyStruct(ms);is __ms16a new variable, uwhich is __ms16a reference to the value of 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;
  };

use modifiers

We can use modifiers in structured bindings, such as constand references, which act on anonymous entities e. Usually, the effect on anonymous entities is the same as on structurally bound variables , but sometimes it is different .

For example, we can declare a structured binding as a constreference:

const auto& [u, v] = ms;    // 引用,因此u/v指向ms.i/ms.s

The preprocessing code is as follows

    const MyStruct & __ms16 = ms;
    const int & u = __ms16.i;
    const std::basic_string<char, std::char_traits<char>, std::allocator<char> > & v = __ms16.s;

Here, the anonymous entity is declared as consta reference, and uand are the members and aliases vof this reference, respectively . Therefore, the modification of the member will affect the value of and , but we cannot modify the value of the value, if it will fail to compile.ismsuvuu = 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;
    }
}

The output is:

ms.i = 2  u = 2
destruct

constYou can even indirectly modify members of the object used for initialization if declared as non-reference :

#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; 
}

The result of the operation is as follows:

77
99
destruct

If a structured binding is a reference type and is a reference to a temporary object, then as usual, the lifetime of the temporary object is extended to the lifetime of the structured binding :

#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 << '\n';    // OK
}

operation result:

a: 42

Modifiers are not applied to variables introduced by structured bindings

Modifiers are applied to new anonymous entities, not to new variable names introduced by structured bindings .
In fact, in the following code:

const auto& [u, v] = ms;    // 引用,因此u/v指向ms.i/ms.s

uNeither isv a reference, only the anonymous entity is a reference. and are the types of the corresponding members, respectively, which just become (since we cannot modify members referenced by constants ). By derivation, yes , yes .e
uvmsconst
decltype(u)const intdecltype(v)const std::string

The same is true when specifying alignment:

#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;
}

Here alignas(16) auto [u, v] = getStruct(), we aligned anonymous entities instead of uand v. This means ubeing the first member would be 16-byte aligned, but vnot.

Therefore, no type degradation (decay) occurs even with autostructured binding . For example, if we have a struct of primitive arrays:

struct S {
    
    
    const char x[6];
    const char y[3];
};

Then after the following statement:

S s1{
    
    };
auto [a, b] = s1;    // a和b的类型是结构体成员的精确类型,sizeof(a)  6 sizeof(b) 3

The type here ais still const char[6]. Again, autokeywords are applied to anonymous entities , where no type degradation occurs for anonymous entities as a whole . This is autodifferent from initializing a new object with , and type regression occurs in the following code:

auto a2 = a;    // a2的类型是a的退化类型

move semantics

moveSemantics also follow the rules introduced earlier, declared as follows:

MyStruct ms = {
    
    42, "Jim"};
auto&& [v, n] = std::move(ms);     // 匿名实体是ms的右值引用

The preprocessing code is as follows

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;

The anonymous entity pointed to here is van rvalue reference while still holding the value:nmsms

std::cout << "ms.s: " << ms.s << '\n';  // 打印出"Jim"

However, you can move assign the ms.spointed to:n

std::string s = std::move(n);           // 把ms.s移动到s
std::cout << "ms.s: " << ms.s << '\n';  // 打印出未定义的值
std::cout << "n:    " << n << '\n';     // 打印出未定义的值
std::cout << "s:    " << s << '\n';     // 打印出"Jim"

As usual, the object whose value was moved is in a state where the value is undefined but valid . So their values ​​can be printed, but don't make any assumptions about the printed values .

The above example is msa bit different from the structural binding directly using the moved value:

MyStruct ms = {
    
    42, "Jim"};
auto [v, n] = std::move(ms);    // 新的匿名实体持有从ms处移动走的值

The preprocessing code is as follows:

 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;

Here the new anonymous entity is msinitialized with the moved value. Therefore, msthe values ​​have been lost:

std::cout << "ms.s: " << ms.s << '\n';  // 打印出未定义的值
std::cout << "n:    " << n << '\n';     // 打印出"Jim"

You can continue to nmove assignments or nassign new values ​​to , but it will no longer be affected ms.s:

std::string s = std::move(n);   // 把n移动到s
n = "Lara";
std::cout << "ms.s: " << ms.s << '\n';  // 打印出未定义的值
std::cout << "n:    " << n << '\n';     // 打印出"Lara"
std::cout << "s:    " << s << '\n';     // 打印出"Jim"

Applicable scene

In theory, structured binding works for any publicstruct with data members, Cstyle arrays and "tuple ( tuple-like)-like objects":

  • For structs and classespublic where all non-static data members are , you can bind each member to a new variable name.

  • For native arrays , you can bind each element of the array to a new variable name.

  • For any type, you can use the tuple-like API to bind new names, no matter APIhow the set defines "elements". For a type type this API requires the following components:

    • std::tuple_size<type>::valueThe number of elements to return.
    • std::tuple_element<idx, type>::type
      The type of the th element to return idx.
    • A global or member function get<idx>()to return idxthe value of the th element.

The standard library types std::pair<>, std::tuple<>, std::array<>are examples that provide such APIs. If structs and classes provide them tuple-like API, these will be used APIfor binding instead of binding data members directly .

In any case, there must be as many variable names declared in a structured binding as there are elements or data members. You can't skip an element, and you can't reuse variable names. However, you can use very short names for example '_'(some people love this name, some people hate it, but note that the global namespace does not allow it ), but the name can only be used once in the same scope:

auto [_, val1] = getStruct();   // OK
auto [_, val2] = getStruct();   // ERROR:变量名_已经被使用过

The preprocessing code is as follows:

  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;

Nested structured bindings are not yet supported.

Structures and Classes

The method of using structured binding for structures and classespublic with only members has been introduced above . A typical application is to directly use structured binding for return values ​​containing multiple data . However there are some edge cases to be aware of.

Note that using structured bindings requires certain rules for inheritance. All non-static data members must be defined in the same class (that is, they must either all come directly from the final class, or all come from the same parent class):

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

Note that you should use structured binding only if the orderpublic of the members is guaranteed to be fixed . Otherwise, if the order of the sum in is changed, the value of the sum will also change accordingly. To guarantee a fixed order, a member order is defined for some standard library structures (for example ).Bint aint b
xyC++17insert_return_type

Federation does not yet support the use of structured bindings.

native array

The following code initializes the sum with two Celements of the styles array :xy

int arr[] = {
    
     47, 11 };
auto [x, y] = arr;  // x和y是arr中的int元素的拷贝
auto [z] = arr;     // ERROR:元素的数量不匹配

The preprocessing code is as follows:

  int __arr15[2] = {
    
    arr[0], arr[1]};
  int & x = __arr15[0];
  int & y = __arr15[1];

Note that this is one of the C++few scenarios where native arrays are copied by value .

Structured bindings can only be used when the length of the array is known . Structural binding cannot be used with arrays passed in as a parameter by value, because the array will degenerate (decay) to the corresponding pointer type.

Note C++that to allow returning arrays with size information by reference, structured bindings can be applied to functions returning such arrays:

auto getArr() -> int(&)[2]; // getArr()返回一个原生int数组的引用
...
auto [x, y] = getArr();     // x和y是返回的数组中的int元素的拷贝

You can also std::arrayuse structured bindings, which are tuple-like APIimplemented with .

std::pair, std::tupleandstd::array

The structured binding mechanism is extensible, you can add support for structured binding for any type . Added support for std::pair<>, std::tuple<>, to the standard library .std::array<>

std::array

For example, the following code binds new variable names , , , , to the four elements in getArray()the returned :std::array<>abcd

std::array<int, 4> getArray();
...
auto [a, b, c, d] = getArray(); // a,b,c,d是返回值的拷贝中的四个元素的别名

Here a, b, c, dare bound to elements of the getArray()returned type.std::array

Binding using references to non-temporary variables non-constcan also be modified. For example:

#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;
}

The preprocessing code is as follows:

	//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);

operation result:

stdarr[0] 11
stdarr[0] 21
stdarr[0] 21

However, as usual, we cannot initialize a non- reference with a temporary object ( ) :prvalueconst

auto& [a, b, c, d] = getArray();    // ERROR

std::tuple

The following code initializes a, b, cto an alias for the three elements of the getTuple()returned copy: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;
}

The preprocessing code is as follows:

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

where athe type of is char, bthe type of is float, cthe type of is std::string.

std::pair

std::mapBefore the insert()example demo, let's understand insert()the function of this.

#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 ? " 成功\n" : " 失败\n");
}

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 << '\n';
    for (const auto& [name, height] : heights2)
        std::cout << std::setw(10) << name << " | " << height << "cm\n";
}

operation result

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

As another example, consider the following insert()code that handles the return value of a member of an associative/unordered container:

#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 ? " 成功\n" : " 失败\n");
}
int main() {
    
    
    std::map<std::string, int> coll;
    auto ret = coll.insert({
    
    "new", 42});
    print_insertion_status(ret.first, ret.second);
}

The preprocessing code is as follows:

using namespace std::literals;
template<typename It>
void print_insertion_status(It it, bool success)
{
    
    
  (std::operator<<(std::cout, "\346\217\222\345\205\245 ") << it->first) << (success ? " \346\210\220\345\212\237\n" : " \345\244\261\350\264\245\n");
}


/* 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, "\346\217\222\345\205\245 "), it.operator->()->first), (success ? " \346\210\220\345\212\237\n" : " \345\244\261\350\264\245\n"));
}
#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;
}

The result of the operation is as follows:

插入 new 成功

Code readability is greatly enhanced by using structured bindings instead of the returned std::pair<>object's firstand members:second

  std::map<std::string, int> coll;
  auto [pos, isOk] = coll.insert({
    
    "new", 42});
  std::cout << "插入 "  << (isOk ? " 成功\n" : " 失败\n");

The preprocessing code is as follows:

  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, "\346\217\222\345\205\245 "), (isOk ? " \346\210\220\345\212\237\n" : " \345\244\261\350\264\245\n"));

Note that in this scenario, there is a way to improve C++17using statements with initialization .if

Assign new values ​​to structured bindings for pairandtuple

After declaring a structured binding, you usually cannot modify variables of all bindings at the same time , because structured bindings can only be declared together but not used together. However, if the value being assigned can be assigned to an std::pair<>OR std::tuple<>, you can use std::tie()assign the value to all variables together. For example:

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的值
}

The preprocessed code is as follows:

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

This approach can be used to handle loops that return multiple values, for example using a searcher inside a loop:

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())) {
    
    
    ...
}

Provides for structured bindingTuple-Like API

You can add support for structured bindings to any type by providing a tuple-like API , just like the standard library does for std::pair<>, std::tuple<>, :std::array<>

Support for read-only structured bindings

The following example demonstrates how to Customeradd structured binding support to a type. The class definition is as follows:

#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;
    }
};

We can add it with the following code 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(); }

Here, we define tuple-like APIand map to three attributes for customers getter(you can also customize other mappings):

  • Customer's first name (first name) is std::stringof type
  • The customer's last name is std::stringof type
  • The customer's spending amount is longof type

The number of attributes is defined as a specialization of std::tuple_sizethe template function on the type:Customer

template<>
struct std::tuple_size<Customer> {
    
    
    static constexpr int value = 3; // 我们有3个属性
};

The type of the property is defined as std::tuple_elementa specialized version of:

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

The type of the third attribute is the fully specialized version when longdefined as 2. IdxAll other attribute types std::stringare defined as partial specializations (with lower priority than full specializations). The type declared here is decltypethe type returned by the structured binding.

Finally, we Customerdefine get<>()the overloaded version of the function in the same namespace as the class as 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(); }

In this case we have a declaration of the main function template and fully specialized versions for all cases.

Note that the fully specialized version of the function template must use the same type as when it was declared (including the return value type must be exactly the same). This is because we are only providing an "implementation" of the specialized version, not a new declaration. The following code will not compile:

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(); }

By using the new compile-time ifstatement feature, we can combine get<>()function implementations
into a single function:

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();
    }
}

With this API, we can Customeruse structured binding for types:

#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 << '\n';
    
    // 修改结构化绑定的变量
    std::string s{
    
    std::move(f)};
    l = "Waters";
    v += 10;
    std::cout << "f/l/v:    =/" << f << "  /" << l << "  /"<< v << '\n';
    std::cout << "c:        " << c.getFirst() << ' '
        << c.getLast() << ' ' << c.getValue() << '\n';
    std::cout << "s:        " << s << '\n';

    return 0;
}

The result of the operation is as follows:

Customer first = Tim ; last= Starr; val= 42
f/l/v:    =/Tim  /Starr  /42
f/l/v:    =/  /Waters  /52
c:        Tim Starr 42
s:        Tim

After initialization as follows:

auto [f, l, v] = c;

Like the previous example, Customer cis copied to an anonymous entity. Anonymous entities are also destroyed when the structured binding goes out of scope.

In addition, for each binding f, l, v, their corresponding get<>()functions will be called. Because the defined get<>function return type is auto, these 3 getterwill return a copy of the member, which means that the address of the variable of the structured binding is different from cthe address of the member in . Therefore, the modified cvalue does not affect the bind variable (and vice versa).

Using a structured binding is equivalent to using get<>()the return value of a function, so:

std::cout << "f/l/v:    " << f << ' ' << l << ' ' << v << '\n';

It simply outputs the value of the variable (without calling getterthe function again). in addition

std::string s{
    
    std::move(f)};
l = "Waters";
v += 10;
std::cout << "f/l/v:    " << f << ' ' << l << ' ' << v << '\n';

This code modifies the value of the bind variable.

Therefore, this program usually has the following output:

Customer first = Tim ; last= Starr; val= 42
f/l/v:    =/Tim  /Starr  /42
f/l/v:    =/  /Waters  /52
c:        Tim Starr 42
s:        Tim

The output of the second line depends on the value moveof string, which is usually an empty string, but other valid values ​​are also possible.

You can also use structured binding when iterating Customerover an element of type: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 << '\n';
    }

In this loop, const auto&it will not be Customercopied because it is used. However, when a variable is initialized with a structured binding, the calling get<>()function returns a copy of the first and last name.
Afterwards, the structured bound variable is used in the output statement in the loop body and does not need to be called again getter. Finally at the end of each iteration, the copied string is destroyed.
The result of the operation is as follows:

Tim Starr: 42
Tony OMG: 43
Spny MG: 43

Note that using bind variables decltypewill deduce the type of the variable itself, which will not be affected by the type modifier of the anonymous entity. That is to say, decltype(first)the type here is
const std::stringnot a reference.
Paste the complete preprocessing code here to deepen understanding

#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), '\n');
    }
    
  }
  return 0;
}

Support for writable structured bindings

Implementations tuple-like APIcan return non-constreferences so that structured bindings have write permissions. Suppose the class Customerprovides an API for reading and writing members:

#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;
    }
};

To support reading and writing, we need to define overloaded getters for const and non-const references:

#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();
    }
}

Note that you must provide specializations of these 3 versions to handle const objects, non-const objects, and movable objects. In order to be able to return a reference, you should use decltype(auto)as return type.

Here we use the compile-time ifstatement feature again, which can make our getterimplementation easier. Without this feature, we would have to write all full specializations, for example:

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(); }
...

Again, the main function template declaration must have the exact same signature (including the return value) as the fully specialized version. The following code does not compile:

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(); }

You can now Customeruse structured bindings on classes, and modify the values ​​of members through bindings:

#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 << '\n';
    
    // 通过引用修改结构化绑定
    auto&& [f2, l2, v2] = c;
    std::string s{
    
    std::move(f2)};
    f2 = "Ringo";
    v2 += 10;
    std::cout << "f2/l2/v2: " << f2 << ' ' << l2 << ' ' << v2 << '\n';
    std::cout << "c:        " << c.firstname() << ' '
        << c.lastname() << ' ' << c.value() << '\n';
    std::cout << "s:        " << s << '\n';
}

The output of the program is as follows:

f/l/v:    Tim Starr 42
f2/l2/v2: Ringo Starr 52
c:        Ringo Starr 52
s:        Tim

ifand switchstatement with initialization

ifAnd switchstatements now allow adding an initialization statement inside a conditional expression.

For example, you can write the following code:

#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;
}

The preprocessing code is as follows

#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;
}

The output is as follows:

Failed = 0

The initialization statement in it status s = Failed;is initialized sand swill be valid in the entire ifstatement (including elsebranches).

ifstatement with initialization

ifVariables defined in the conditional expression of the statement will be ifvalid in the entire statement
(including the then part and the else part). For example:

if (std::ofstream strm = getLogStrm(); coll.empty()) {
    
    
    strm << "<no data>\n";
}
else {
    
    
    for (const auto& elem : coll) {
    
    
        strm << elem << '\n';
    }
}
// strm不再有效

The destructor is called at ifthe end of the entire statement.strm

Another example is about the use of locks. Suppose we want to perform some tasks that depend on a certain condition in a concurrent environment:

if (std::lock_guard<std::mutex> lg{
    
    collMutex}; !coll.empty()) {
    
    
    std::cout << coll.front() << '\n';
}

In this example, if class template parameter deduction is used, it can be rewritten as the following code:

if (std::lock_guard lg{
    
    collMutex}; !coll.empty()) {
    
    
    std::cout << coll.front() << '\n';
}

The code above is equivalent to:

{
    
    
    std::lock_guard<std::mutex> lg{
    
    collMutex};
    if (!coll.empty()) {
    
    
        std::cout << coll.front() << '\n';
    }
}

The subtle difference is that in the former lgit ifis defined within the scope of the statement, in the same scope as the conditional statement.

Note that this feature has forexactly the same effect as initialization statements in traditional loops.
In the above example, in order to make lock_guardit effective, a variable name must be explicitly declared in the initialization statement,
otherwise it is a temporary variable and will be destroyed immediately after creation. Therefore, it is a logical error to initialize a temporary without a variable name
lock_guard, because the lock is released when the conditional statement is executed:

if (std::lock_guard<std::mutex>{
    
    collMutex};     // 运行时ERROR
    !coll.empty()) {
    
                                // 锁已经被释放了
    std::cout << coll.front() << '\n';          // 锁已经被释放了
}

_In principle, it is sufficient to use simply as variable name:

if (std::lock_guard<std::mutex> _{
    
    collMutex};   // OK,但是...
    !coll.empty()) {
    
    
    std::cout << coll.front() << '\n';
}

You can also declare multiple variables at the same time, and they can be initialized at declaration time:

if (auto x = qqq1(), y = qqq2(); x != y) {
    
    
    std::cout << "return values " << x << " and " << y << "differ\n";
}

or:

if (auto x{
    
    qqq1()}, y{
    
    qqq2()}; x != y) {
    
    
    std::cout << "return values " << x << " and " << y << "differ\n";
}

Another example is inserting elements into mapor unordered map.
You can check for success like this:

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 << '\n';
}

Here, instead of using the and members directly pos, we declare new names with structural bindings to members of the return value and members of the pointed-to value . Before C++17, the corresponding processing code must be written as follows:
firstsecond

auto ret = coll.insert({
    
    "new", 42});
if (!ret.second) {
    
    
    // 如果插入失败,用ret.first处理错误
    const auto& elem = *(ret.first);
    std::cout << "already there: " << elem.first << '\n';
}

Note that this extension also applies to compile-time ifstatement features.

switchstatement with initialization

By using a statement with initializer switch, we can initialize an object/entity before evaluating the conditional expression.

For example, we can declare a filesystem path and then process it according to its class:

namespace fs = std::filesystem;
...
switch (fs::path p{
    
    name}; status(p).type()) {
    
    
    case fs::file_type::not_found:
        std::cout << p << " not found\n";
        break;
    case fs::file_type::directory:
        std::cout << p << ":\n";
        for (const auto& e : std::filesystem::directory_iterator{
    
    p}) {
    
    
            std::cout << "- " << e.path() << '\n';
        }
        break;
    default:
        std::cout << p << " exists\n";
        break;
}

Here, the initialized path pcan be used throughout switchthe statement.

inline variable

For portability and ease of integration, it is important to provide complete class and library definitions in header files.
However, C++17previously, this was only possible if the library neither provided nor required a global object.

Since the beginning, you can define global variables/objects C++17in header files with inline:

class MyClass {
    
    
    inline static std::string msg{
    
    "OK"}; // OK(自C++17起)
    ...
};

inline MyClass myGlobalObj;  // 即使被多个CPP文件包含也OK

The assembly code is as follows

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>()};
};

As long as there are no duplicate definitions within a compilation unit. The definition in this example refers to the same object even though it is used by multiple compilation units.

motivation

It is not allowed to initialize non-const static members in C++a class:

class MyClass {
    
    
    static std::string msg{
    
    "OK"};   // 编译期ERROR
    ...
};

Non-constant static members can be defined and initialized outside the class definition, but if they are CPPincluded by multiple files at the same time, a new error will be raised:

class MyClass {
    
    
    static std::string msg;
    ...
};
std::string MyClass::msg{
    
    "OK"}; // 如果被多个CPP文件包含会导致链接ERROR

According to the once-definition principle (ODR), the definition of a variable or entity can only appear in one compilation unit - unless the variable or entity is defined as inline.

Even using preprocessing to protect it doesn't help:

#ifndef MYHEADER_HPP
#define MYHEADER_HPP

class MyClass {
    
    
    static std::string msg;
    ...
};
std::string MyClass::msg{
    
    "OK"}; // 如果被多个CPP文件包含会导致链接ERROR

#endif

The question is not whether a header file may be included multiple times, but that two different CPPfiles both include this header file and are therefore both defined MyClass::msg.
For the same reason, the same linkage error occurs if you define an instance object of a class in a header file:

class MyClass {
    
    
    ...
};
MyClass myGlobalObject; // 如果被多个CPP文件包含会导致链接ERROR

Solution

For some scenarios, here are some workarounds:

  • You can class/structinitialize constant static members of numeric or enumeration types in the definition of a:
class MyClass {
    
    
    static const bool trace = false;    // OK,字面类型
    ...
};

However, this method can only initialize literal types, such as basic integers, floating-point numbers, pointer types, or classes that initialize all internal non-static members with constant expressions, and the class cannot have user-defined or virtual destructors function.
In addition, if you need to get the address of this static constant member (for example, you want to define a reference to it), then you must define it in that compilation unit and cannot define it again in other compilation units.

  • You can define an staticinline function that returns a local variable:
inline std::string& getMsg() {
    
    
    static std::string msg{
    
    "OK"};
    return msg;
}
  • You can define a staticmember function that returns this value:
class MyClass {
    
    
    static std::string& getMsg() {
    
    
        static std::string msg{
    
    "OK"};
        return msg;
    }
    ...
};
  • You can use variable templates (since C++14):
template<typename T = std::string>
T myGlobalMsg{
    
    "OK"};
  • You can define a template class for static members:
template<typename = void>
class MyClassStatics
{
    
    
    static std::string msg;
};
template<typename T>
std::string MyClassStatics<T>::msg{
    
    "OK"};

and then inherit it:

class MyClass : public MyClassStatics<>
{
    
    
    ...
};

However, all of these methods lead to signature overloading, less readability, and different ways of using the variable. Also, initialization of global variables may be deferred until first use. Therefore, it is not feasible to write assuming that the variable has been initialized from the beginning (such as using an object to monitor the process of the entire program).

use

Now, with inlinethe modifier, CPPthere will only be one global object even if the header file where the definition resides is included by multiple files:

class MyClass {
    
    
    inline static std::string msg{
    
    "OK"};    // 自从C++17起OK
    ...
};

inline MyClass myGlobalObj; // 即使被多个CPP文件包含也OK

The ones used here have the same semantics inlineas in function declarations :inline

  • It can be defined in multiple compilation units, as long as all definitions are the same.
  • It must be defined in every compilation unit that uses it

The above two requirements can be met by defining the variable in the header file, and then CPPincluding the header file in multiple files. The program behaves as if there was only one variable.

You can even use it to define atomic types in header files:

inline std::atomic<bool> ready{
    
    false};

As usual, ** std::atomicmust be initialized when you define a variable of type. **

Note that you still have to make sure that the types of inline variables are complete before you initialize them.
For example, if a structor classhas a staticmember of its own type, then this member can only be defined after the type declaration:

struct MyType {
    
    
    int value;
    MyType(int i) : value{
    
    i} {
    
    
    }
    // 一个存储该类型最大值的静态对象
    static MyType max;  // 这里只能进行声明
    ...
};
inline MyType MyType::max{
    
    0};

Another example of using inline variables is in newthe header file that tracks all calls.

constexpr staticmember is now implicitlyinline

For static members, constexprmodifiers are now implied inline. Since C++17then, the following declarations define static data members n:

struct D {
    
    
    static constexpr int n = 5; // C++11/C++14: 声明
                                // 自从C++17起: 定义
}

is equivalent to the following code:

struct D {
    
    
    inline static constexpr int n = 5;
};

Note that C++17before, you can have only declarations without definitions. Consider the following statement:

struct D {
    
    
    static constexpr int n = 5;
};

D::nOnly the above declaration is sufficient if no definition is required,
for example when D::npassing by value:

std::cout << D::n;          // OK,ostream::operator<<(int)只需要D::n的值

If D::npassed by reference to a non-inline function, and the function call is not optimized away, the call will result in an error. For example:

int twice(const int& i);
std::cout << twice(D::n);   // 通常会导致ERROR

This code violates the Once Definition Principle (ODR). If the compiler optimizes, then this code may work as expected, or it may cause a link error due to the missing definition. If you don't optimize, you'll almost certainly D::nend up with bugs because of missing definitions.
If you create D::na pointer to one then it is more likely to cause link errors due to missing definitions (but may still compile fine in some compilation modes):

const int* p = &D::n;       // 通常会导致ERROR

So before C++17, you had to define within a compilation unit D::n:

constexpr int D::n;         // C++11/C++14: 定义
                            // 自从C++17起: 多余的声明(已被废弃)

Now when C++17building with , the declaration in the class itself becomes the definition, so all the above examples now work even without the above definition. The above definition is still valid but has become an obsolete redundant declaration.

inline variables andthread_local

thread_localAn inline variable can be created per thread by using :

struct ThreadData {
    
    
    inline static thread_local std::string name;    // 每个线程都有自己的name
    ...
};

inline thread_local std::vector<std::string> cache; // 每个线程都有一份cache

As a complete example, consider the following header file:

#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 << '\n';
        std::cout << "- gName: " << gName << '\n';
        std::cout << "- tName: " << tName << '\n';
        std::cout << "- lName: " << lName << '\n';
    }
};

inline thread_local MyData myThreadData;    // 每个线程一个对象

You can main()use it inside the included compilation unit:

#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:");
}

You can also use this header file in another compilation unit that defines foo()a function that will be called from another thread:

#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:");
}

The output of the program is as follows:

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

polymer extension

C++There are many ways to initialize objects. One of them is called aggregate initialization (aggregate initialization) , which is an initialization method specific to aggregates. The initializer, introduced from Cthe language, is to initialize a class with a set of values ​​enclosed in curly braces:

struct Data {
    
    
    std::string name;
    double value;
};

Data x = {
    
    "test1", 6.778};

Since C++11, the equal sign can be ignored :

Data x{
    
    "test1", 6.778};

Since C++17, aggregates can have base classes . That is to say, subclasses derived from other classes like the following can also use this initialization method:

struct MoreData : Data {
    
    
    bool done;
}

MoreData y{
    
    {
    
    "test1", 6.778}, false};

As you can see, an aggregate initializer can use a subaggregate initializer to initialize members of the class from the base class. Alternatively, you can even omit curly braces for subaggregate initialization :

MoreData y{
    
    "test1", 6.778, false};

Preprocessing code:

  MoreData y = {
    
    {
    
    std::basic_string<char, std::char_traits<char>, std::allocator<char> >("test1", std::allocator<char>()), 6.7779999999999996}, false};

Writing this way will follow the general rules for initialization of nested aggregates, the actual parameters you pass are used to initialize which member depends on their order.

motivation

Without this feature, all derived classes cannot be initialized with aggregates, which means you would define constructors like this:

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

Now we no longer need to define any constructors to do this. We can directly use the syntax of nested curly braces to achieve initialization. If all the values ​​​​needed by the inner layer initialization are given, the inner curly braces can be omitted:

MoreData x{
    
    {
    
    "test1", 6.778}, false};    // 自从C++17起OK
MoreData y{
    
    "test1", 6.778, false};      // OK

Note that since derived classes can now also be aggregates, some other initialization methods can also be used:

MoreData u;     // OOPS:value/done未初始化
MoreData z{
    
    };   // OK: value/done初始化为0/false

If you think this is dangerous, you can use member initializers:

struct Data {
    
    
    std::string name;
    double value{
    
    0.0};
};

struct Cpp14Data : Data {
    
    
    bool done{
    
    false};
};

Alternatively, go ahead and provide a default constructor.

use

A typical use case for aggregate initialization is to initialize a class derived from a C-style struct with new members added. For example:

struct Data {
    
    
    const char* name;
    double value;
};

struct CppData : Data {
    
    
    bool critical;
    void print() const {
    
    
        std::cout << '[' << name << ',' << value << "]\n";
    }
};

CppData y{
    
    {
    
    "test1", 6.778}, false};
y.print();

Here, the parameters inside the inner curly braces are passed to the base class Data.
Note that you can skip initializing certain values. In this case, the skipped members will be default-initialized (the underlying type will be initialized to 0, falseor nullptr, the class type will be default-constructed).
For example:

CppData x1{
    
    };           // 所有成员默认初始化为0值
CppData x2{
    
    {
    
    "msg"}}     // 和{
    
    {"msg", 0.0}, false}等价
CppData x3{
    
    {
    
    }, true};   // 和{
    
    {nullptr, 0.0}, true}等价
CppData x4;             // 成员的值未定义

Note that using empty curly braces is completely different from using no curly braces:

  • x1The definition of will initialize all members to 0 by default, so the character pointer nameis initialized to nullptr, doublethe type is valueinitialized to 0.0, and boolthe type is flaginitialized to false.
  • x4The definition of does not initialize any members. The value of all members is undefined.
    You can also derive aggregates from non-aggregates. For example:
struct MyString : std::string {
    
    
    void print() const {
    
    
        if (empty()) {
    
    
            std::cout << "<undefined>\n";
        }
        else {
    
    
            std::cout << c_str() << '\n';
        }
    }
};

MyString x{
    
    {
    
    "hello"}};
MyString y{
    
    "world"};

Note that this is not usual polymorphic publicinheritance, because std::stringthere are no virtual member functions, and you need to avoid confusing the two types. You can even derive aggregates from multiple base classes and aggregates:

template<typename T>
struct D : std::string, std::complex<T>
{
    
    
    std::string data;
};

You can use and initialize like this:

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)

The inner nested initial value list will be passed to the base class in the order in which the base class is declared when inheriting. This new feature can also help us define overloads with very little code lambda.

definition

In general, C++17an object is considered an aggregate if it satisfies one of the following conditions :

  • is an array

  • Or a class type ( class, struct, union) that satisfies the following conditions:

  • explicitConstructors without user-defined and

  • usingConstructor not using declared inheritance

  • non-static data members without privateandprotected

  • no virtualfunction

  • no virtual, private, protectedbase class

However, to initialize aggregates using aggregate initialization , the following additional constraints need to be satisfied:

  • There are no members in the base class privateorprotected
  • constructor without privateorprotected

The next section has an example where compilation fails because these additional constraints are not satisfied.

C++17A new type trait has been introduced is_aggregate<>
to test whether a type is an 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)

backward incompatibility

Note that the following example no longer compiles:

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(但可能不会初始化)
}

Before C++17, Derivednot aggregates. therefore

Derived d1{
    
    };

Will call Derivedthe implicitly defined default constructor, which will call the base class Baseconstructor. Although the default constructor of the base class is privatetrue, it is also valid to call it in the constructor of the derived class, because the derived class is declared as a friend class. Since C++17, the example Derivedis an aggregate, so it does not have an implicit default constructor (constructors are not usinginherited using declarations). Therefore, d1the initialization of will be an aggregate initialization, and the following expression: std::is_aggregate<Derived>::valuewill return true. However, since the base class has a privateconstructor (see the previous section), curly braces cannot be used for initialization. This has nothing to do with whether the derived class is a friend of the base class or not.

Mandatory omission of copying or passing unmaterialized objects

  • Technically, C++17a new rule is introduced: when passing or returning a temporary object by value, the copy of the temporary object must be omitted.
  • In effect, we are actually passing an unmaterialized object .
    Next, we first introduce this feature technically, and then introduce the actual effect and term materialization .

motivation

Since the first standard, it has been allowed to elisionC++ copy operations in some cases , even if doing so may affect the program's running results (for example, a print statement in the copy constructor may not be executed again ). This can easily happen when initializing a new object with a temporary, especially if a function passes or returns a temporary by value. For example:

class MyClass
{
    
    
    ...
};

void foo(MyClass param) {
    
       // param用传递进入的实参初始化
    ...
}

MyClass bar() {
    
    
    return MyClass{
    
    };       // 返回临时对象
}

int main()
{
    
    
    foo(MyClass{
    
    });     // 传递临时对象来初始化param
    MyClass x = bar();  // 使用返回的临时对象初始化x
    foo(bar());         // 使用返回的临时对象初始化param
}

However, because this optimization is not mandatory, the situation in the example requires that the object must have an implicit or explicit copy/move constructor . That is, **Although the copy/move functions are not actually called in most cases because of optimization reasons, they must exist. **Therefore, if the class in the above example MyClassis replaced with the following definition, the code in the above example will not compile:

class MyClass
{
    
    
  public:
    ...
    // 没有拷贝/移动构造函数的定义
    MyClass(const MyClass&) = delete;
    MyClass(MyClass&&) = delete;
    ...
};

Just having no copy constructor is enough to generate an error, because a move constructor is only implicit if there is no user-declared copy constructor (or assignment operator or destructor). (In the above example, only the copy constructor needs to be defined as deleteand there will be no implicitly defined move constructor.) Omitting the copy becomes mandatory since initializing the object with a temporary variable
. C++17In fact, later on I'll see that the temporary variables we pass as parameters or return values ​​will be used to materialize a new object. MyClassThis means that the sample code compiles successfully even though copying is not allowed at all in the example above . However, note that other optional copy-omitting scenarios are still optional, in which a copy or move constructor is still required. For example:

MyClass foo()
{
    
    
    MyClass obj;
    ...
    return obj;     // 仍然需要拷贝/移动构造函数的支持
}

Here, foo()there is a named variable in (which is an lvalueobj when used ). Therefore, named return value optimization (NRVO) takes effect, however the optimization still requires copy/move support. This also happens when it is a formal parameter :obj

MyClass bar(MyClass obj)    // 传递临时变量时会省略拷贝
{
    
    
    ...
    return obj;     // 仍然需要拷贝/移动支持
}

Copy/move is no longer required when passing a temporary variable (aka prvalue ) as an argument, but copy/move support is still required if returning this parameter because the returned object is named. As part of the change, the meaning of the term value type hierarchy has been revised and clarified a lot.

effect

An obvious effect of this feature is that fewer copies lead to better performance . Although many mainstream compilers have performed this optimization before, this behavior is now guaranteed by the standard . Although move semantics can significantly reduce copy overhead, it can still bring great performance improvements if you don't copy directly (for example, move semantics still have to copy each member when the object has many basic type members). In addition, this feature can reduce the use of output parameters and return a value directly (provided that the value is created directly in the return statement).
Another effect is that you can define a factory function that will always work, because now it can even return objects that are not allowed to be copied or moved . For example, consider the following generic factory function:

#include <utility>

template <typename T, typename... Args>
T create(Args&&... args)
{
    
    
    ...
    return T{
    
    std::forward<Args>(args)...};
}

This factory function is now even available for std::atomic<>types that have neither copy nor move constructors:

#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);
}

Another effect is that classes whose move constructors have been explicitly deleted can now also return temporary objects to initialize new objects :

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

The C++17previous xinitialization is invalid, because copy-initialization (using =initialization) requires 42conversion to a temporary object, and then initializing with this temporary object xrequires the move constructor in principle, although it may not be called. (The copy constructor can be used as an alternative to the move constructor only if the move constructor is not user-defined)

More explicit value type system

A side effect of the proposal to force omission of copies of temporary variables when initializing new objects with temporary variables is that the value category system has been modified a lot to support this proposal .

value type system

C++Every expression in has a value type. This type describes what the value of the expression can be used for.

Historical Value Type Hierarchy

C++In the past, there were only lvalues ​​and rvalues ​​inherited from the C language , divided according to the assignment statement:

x = 42;

Here the expression xis an lvalue because it can appear on the left side of the assignment equals sign, 42and an rvalue because it can only appear on the right side of the expression. However, things got more complicated when ANSI-C came along, because it would not be able to appear to the left of an assignment if xdeclared as const int, but it would still be an (unmodifiable) lvalue.

Later, C++11movable objects were introduced. From a semantic analysis, movable objects can only appear on the right side of the assignment number but it can be modified, because the assignment number can remove their values. For this reason, the type expiry value (xvalue) was introduced, and the original rvalue was renamed to prvalue (prvalue) .

The value type system since C++11

We have the core value type system lvalue (lvalue) , prvalue (pure rvalue) ("pure rvalue") and xvalue (expiration value) ("eXpiring value"). Composite value type systems include glvalue (generalized lvalue) ("generalized lvalue", which is a composite of lvalue and xvalue ) and rvalue (rightvalue) ( composite of xvalue and prvalue ).
Examples of lvalue (left value) are:

  • an expression containing only a single variable, function, or member
  • Expressions containing only string literals
  • the result of the built-in *unary operator (the dereference operator)
  • An example of a prvalue (pure rvalue ) return value from a function that returns an lvalue (lvalue) reference ( type& ) is:
  • Expressions composed of literals other than string literals and user-defined literals
  • The result of the built-in unary &operator (address-of operator)
  • Results of built-in math operators
  • the return value of a function that returns a value
  • An example of a lambda expression
    xvalue (the expiry value) is:
  • The return value of a function returning an rvalue (right value) reference ( type&&
    ) (especially the std::move()return value of
  • The result of an operation that converts an object to an rvalue (rvalue) reference
    In simple terms:
  • All variable names used as expressions are lvalue (left value) .
  • All string literals used as expressions are lvalues ​​(left values) .
  • All other literals ( 4.2, true, nullptr) are prvalues .
  • All temporary objects (especially objects returned by value) are prvalues ​​(pure rvalues) .
  • std::move()The result is an xvalue (the expiry value)
    eg:
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

It is worth emphasizing that strictly speaking glvalue (generalized lvalue), prvalue (prvalue), xvalue (expiration value) are terms that describe expressions, not values ​​(which means that these terms are actually misnomers. ). For example, a variable is not itself an lvalue, only the expression containing the variable is an lvalue:

int x = 3;  // 这里,x是一个变量,不是一个左值
int y = x;  // 这里,x是一个左值

In the first statement, 3a prvalue is used to initialize the variable (not an lvalue) x. In the second statement, xis an lvalue (the expression that evaluates to an 3object containing a value). The lvalue xis converted to a prvalue, which is then used for initialization y.

C++17The value type system since

C++17The value type system is clarified again.
The key to understanding the value type system is that now broadly speaking, we only have two types of expressions:

  • glvaue: an expression describing the location of an object or function
  • prvalue: the expression used to initialize the

And xvalue can be thought of as a special location, which represents an object whose resources can be recycled (usually because the object's life cycle is about to end).

C++17 introduces a new term: ( materialization of temporary objects) , and currently prvalue is a kind of temporary object. Thus, a temporary materialization conversion is a prvalue-to-xvalue conversion.

In any case, prvalue is valid where glvalue (lvalue or xvalue) is required. At this time, a temporary object will be created and initialized with this prvalue (note that prvalue is mainly the value used for initialization). The prvalue is then replaced by a temporary object of type xvalue created temporarily. So the above example strictly speaking looks like this:

void f(const X& p); // 接受一个任何值类型体系的表达式
                    // 但实际上需要一个glvalue
f(X());             // 传递了一个prvalue,该prvalue实质化为xvalue

f()Because the formal parameter in this example is a reference, it requires an actual parameter of type glvaue. However, an expression X()is a prvalue. At this time, the "temporary variable materialization" rule will come into play, and the expression X()will be "converted" to a temporary object of type xvalue.
Note that no new/different objects are created during materialization. Lvalue references pare still bound to xvalues ​​and prvalues, although the latter are now converted to an xvalue.
Because prvalue is no longer an object but an expression that can be used to initialize an object, when using prvalue to initialize an object, it is no longer required that the prvalue be movable, and the feature of omitting the copy of the temporary variable can be perfectly realized. We now simply pass the initial value and it will be automatically materialized to initialize the new object.

Unsubstantiated return value passing

All procedures that return a temporary object (prvalue) by value are passing the return value unmaterialized:

  • When we return a literal that is not a string literal:
int f1() {
    
      // 以值返回int
    return 42;
}
  • When we use autoor typename as return type and return a temporary object:
auto f2() {
    
     // 以值返回退化的类型
    ...
    return MyType{
    
    ...};
}
  • When using decltype(auto)as return type and returning a temporary object:
decltype(auto) f3() {
    
       // 返回语句中以值返回临时对象
    ...
    return MyType{
    
    ...}
}

decltype(auto)Note that the value type is deduced when the initialization expression (here the return statement) is an expression that creates a temporary object (prvalue) . Since we are returning a prvalue by value in these scenarios, we don't need any copy/move at all.

lambdaexpression expansion

C++11Introduced lambdaand C++14introduced generics lambdais a big hit. It allows us to pass functions as arguments, which makes it easier to specify a behavior.
C++17Extended lambdaapplication scenarios of expressions:

  • used in constant expressions (that is, during compilation)
  • Used when a copy of the current object is required (for example, when called from a different thread lambda)

constexpr lambda

Since C++17then, lambdaexpressions are implicitly declared as much as possible constexpr. That is, anything that uses only valid compile-time contexts (eg, only literals, no static variables, no virtual functions, none, no try/catchcontexts new/delete) lambdacan be used at compile-time .

For example, you can use an lambdaexpression to square an argument and use the result as std::array<>the size of the argument, even though it is a compile-time argument:

auto squared = [](auto val) {
    
       // 自从C++17起隐式constexpr
    return val*val;
};
std::array<int, squared(5)> a;  // 自从C++17起OK => std::array<int, 25>

c++17The precompiled code is as follows:

  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>();

Using features that are not allowed in a compile-time context will disable lambdathe constexprability to be, but you can still use it in a runtime context 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) << '\n';   // OK

The error message is as follows:

<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:在编译期上下文中使用了静态变量
      |                     ~~~~~~~~^~~

To determine lambdawhether one is available at compile time, you can declare it as constexpr:

auto squared3 = [](auto val) constexpr {
    
      // 自从C++17起OK
    return val*val;
};

The preprocessing code is as follows:

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

If the return type is specified, the syntax looks like this:

auto squared3i = [](int val) constexpr -> int {
    
      // 自从C++17起OK
    return val*val;
};

The preprocessing code is as follows:

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

The same constexprrule applies for functions lambda: if one lambdais used in a runtime context, then the corresponding function body will also be executed at runtime. constexprHowever, using features that are not allowed in the compile-time context within the declared lambdawill result in a compilation error:

auto squared4 = [](auto val) constexpr {
    
    
    static int calls = 0;  // ERROR:在编译期上下文中使用了静态变量
    ...
    return val*val;
};

So is an implicit or explicit function constexpr lambdacaller constexpr. That is, as follows:

auto squared = [](auto val) {
    
       // 从C++17起隐式constexpr
    return val*val;
};

will be converted to the following closure type :

class CompilerSpecificName {
    
    
public:
    ...
    template<typename T>
    constexpr auto operator() (T val) const {
    
    
        return val*val;
    }
};

Note that here the function call operator of the generated closure type is automatically declared as constexpr. Since then, the generated function call operator will automatically be C++17if the lambda is explicitly or implicitly defined as .constexprconstexpr

Note the following definitions:

auto squared1 = [](auto val) constexpr {
    
      // 编译期lambda调用
    return val*val;
};

and defined as follows:

constexpr auto squared2 = [](auto val) {
    
      // 编译期初始化squared2
    return val*val;
};

is different. The following is the preprocessing code

#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;
}

In the first example if (and only) lambdathen constexprit can be used at compile time, but squared1may not be initialized until runtime, which means that it may cause problems if static initialization order is important (for example, it may cause static initialization order fiasco ). If the closure object is initialized with a lambda constexpr, then the object will be initialized at the beginning of the program, but the lambda may still only be used at runtime. Therefore, the following definitions can be considered:

constexpr auto squared = [](auto val) constexpr {
    
    
    return val*val;
};

use constexprlambdas

Here is an constexpr lambdaexample of usage. Suppose we have a hash function for a sequence of characters, this function iterates each character of the string and repeatedly updates the hash value:

auto hashed = [](const char* str) {
    
    
    std::size_t hash = 5381;        // 初始化哈希值
    while (*str != '\0') {
    
    
        hash = hash * 33 ^ *str++;  // 根据下一个字符更新哈希值
    }
    return hash;
};

Using this lambda, we can initialize a hash of different strings at compile time, defined as an enum:

enum Hashed {
    
     beer = hashed("beer"),
              wine = hashed("wine"),
              water = hashed("water"), ... };   // OK,编译期哈希

We can also compute caselabels at compile time:

switch (hashed(argv[1])) {
    
      // 运行时哈希
    case hashed("beer"):    // OK,编译期哈希
        ...
        break;
    case hashed("wine"):
        ...
        break;
    ...
}

Note that here we will call in casethe label at compile time hashed, and call in switchthe expression at runtime hashed. If we use compile time lambdato initialize a container, then the compiler is likely to calculate the initial value of the container at compile time when optimizing (the std::arrayclass template parameter deduction is used here):

std::array arr{
    
     hashed("beer"),
                hashed("wine"),
                hashed("water") };

You can even hashedcombine another constexprlambda in a function. Suppose we hasheddefine the logic of updating the hash value based on the current hash value and the next character value as a parameter:

auto hashed = [](const char* str, auto combine) {
    
    
    std::size_t hash = 5381;            // 初始化哈希值
    while (*str != '\0') {
    
    
        hash = combine(hash, *str++);   // 用下一个字符更新哈希值
    }
    return hash;
};

This lambdacan be used like this:

constexpr std::size_t hv1{
    
    hashed("wine", [](auto h, char c) {
    
    return h*33 + c;})};
constexpr std::size_t hv2{
    
    hashed("wine", [](auto h, char c) {
    
    return h*33 ^ c;})};

Here, we initialize two different "wine"hashes at compile time by changing the update logic. Both hashedare called at compile time.

copy to lambdatransferthis

When used in a non-static member function lambda, you cannot implicitly gain access to the object's members. That is, if you don't capture thisit, you won't be able to use any member of the object in the lambda (even if you this->use access):

class C {
    
    
private:
    std::string name;
public:
    ...
    void foo() {
    
    
        auto l1 = [] {
    
     std::cout << name << '\n'; };        // ERROR
        auto l2 = [] {
    
     std::cout << this->name << '\n'; };  // ERROR
        ...
    }
};

In C++11 and C++14, you can capture by value or by reference this:

class C {
    
    
private:
    std::string name;
public:
    ...
    void foo() {
    
    
        auto l1 = [this] {
    
     std::cout << name << '\n'; };    // OK
        auto l2 = [=] {
    
     std::cout << name << '\n'; };       // OK
        auto l3 = [&] {
    
     std::cout << name << '\n'; };       // OK
        ...
    }
};

However, the problem is that even capturing by copy thisis essentially a reference (since only this pointers are copied ). Calling such a function can cause problems when the lifetime of the object is longer than the lifetime of the objectlambda . For example, an extreme example is to open a new thread to complete certain tasks. The correct way to call a new thread is to pass a copy of the entire object to avoid concurrency and life cycle problems, rather than passing a reference to the object. Also sometimes you may simply want to pass a copy of the current object. Since there is a solution, but it is less readable and practical:lambdalambda
C++14

class C {
    
    
private:
    std::string name;
public:
    ...
    void foo() {
    
    
        auto l1 = [thisCopy=*this] {
    
     std::cout << thisCopy.name << '\n'; };
        ...
    }
};

For example, you might inadvertently use it when using =or capturing other objects :&this

auto l1 = [&, thisCopy=*this] {
    
    
    thisCopy.name = "new name";
    std::cout << name << '\n'; // OOPS:仍然使用了原来的name
};

Since C++17then, you can *thisexplicitly capture a copy of the current object:

class C {
    
    
private:
    std::string name;
public:
    ...
    void foo() {
    
    
        auto l1 = [*this] {
    
     std::cout << name << '\n'; };
        ...
    }
};

Here, capture *thismeans that the closure generated by the lambda will store a copy of the current object . You can still *thiscapture other objects while capturing, as long as there are no multiple thisconflicts:

auto l2 = [&, *this] {
    
     ... };       // OK
auto l3 = [this, *this] {
    
     ... };    // ERROR

Here's a complete example:

#include <iostream>
#include <string>
#include <thread>

class Data {
    
    
private:
    std::string name;
public:
    Data(const std::string& s) : name(s) {
    
    
    }
    auto startThreadWithCopyOfThis() const {
    
    
        // 开启并返回新线程,新线程将在3秒后使用this:
        using namespace std::literals;
        std::thread t([*this] {
    
    
            std::this_thread::sleep_for(3s);
            std::cout << name << '\n';
        });
        return t;
    }
};

int main()
{
    
    
    std::thread t;
    {
    
    
        Data d{
    
    "c1"};
        t = d.startThreadWithCopyOfThis();
    }   // d不再有效
    t.join();
}

lambdais captured in *this, so what is passed in lambdais a copy. So dthere is no problem to use the captured object even after being destroyed. If we use [this], [=]or [&]capture this, then the new thread will fall into undefined behavior , because when printing in the thread namewill use a member of a destroyed object.

capture by constant reference

By using a new library facility it is now also possible to capture by constant reference this.

New Attributes and Attribute Properties

Since C++11then, it is possible to specify attributes ( annotations that allow or disable certain warnings). C++17New attributes are introduced, and the usage scenarios of attributes are also expanded, which can bring some convenience.

[[nodiscard]]Attributes

The new attribute [[nodiscard]]encourages the compiler to warn when a function's return value is unused (this doesn't mean the compiler has to warn). [[nodiscard]]Usually it should be used to prevent some misbehavior caused by the return value not being used. These misconducts may be:

  • Memory leaks , such as returning dynamically allocated memory but not using it.
  • Unknown or unexpected behavior , such as some strange behavior caused by not using the return value.
  • Unnecessary overhead , such as performing some meaningless behavior because the return value is not used.
    Here are some examples of this property being used:
  • Functions that apply for resources but do not release themselves, but return resources and wait for other functions to release them should be marked as[[nodiscard]] .
    A typical example is a function that allocates memory, such as malloc()a function or allocate()member function of an allocator. Note, however, that some functions may return a value that no further processing is required. For example, a programmer might call a C function with 0 bytes realloc()to free memory, in which case the return value does not need to be free()freed by a subsequent call to the function. Therefore, it would be counterproductive realloc()to mark functions as[[nodiscard]]
  • Sometimes not using the return value will cause the function to behave differently than expected, a good example is std::async()( C++11introduction).
    std::async()It will execute a task asynchronously in the background and return a handle that can be used to wait for the end of the task execution (you can also get the return value or exception through it). However, the call becomes synchronous if the return value is not used, because the destructor for the unused return value is executed immediately after the statement that started the task ends, and the destructor blocks waiting for the task to complete. . Therefore, not using the return value leads to results that std::async()completely contradict the purpose of . Mark std::async()as [[nodiscard]]enabled to cause the compiler to give a warning.
  • Another example is a member function empty()that checks whether an object (container/string) is empty.
    Programmers often misuse this function to "empty" a container (remove all elements):
cont.empty();

This empty()misuse of the pair doesn't use the return value, so [[nodiscard]]check out this misuse:

class MyContainer {
    
    
    ...
public:
    [[nodiscard]] bool empty() const noexcept;
    ...
};

Attribute tags here can help check for such logic errors.
If for some reason you don't want to use [[nodiscard]]the return value of a function marked as , you can convert the return value to void:

(void)coll.empty(); // 禁止[[nodiscard]]警告

Note that properties marked in the base class are not inherited if the member function is overridden or hidden:

struct B {
    
    
    [[nodiscard]] int* foo();
};

struct D : B {
    
    
    int* foo();
};

B b;
b.foo();        // 警告
(void)b.foo();  // 没有警告

D d;
d.foo();        // 没有警告

So you need to re-mark the corresponding member function in the derived class [[nodiscard]](unless there is some reason why you don't want to ensure that the return value must be used in the derived class).
You can mark attributes before any modifiers before the function, or after the function name:

class C {
    
    
    ...
    [[nodiscard]] friend bool operator== (const C&, const C&);
    friend bool operator!= [[nodiscard]] (const C&, const C&);
};

It is an error to put attributes between friendand boolor between booland . Although this feature was introduced since then, it is not yet used in the standard library. Because the proposal came so late, it hasn't been used by those who need it most. In order to ensure the portability of the code, you should use rather than some non-portable solutions (such as gcc and clang's or Visual C++'s ). When defining an operator, you should mark the function with e.g. define a header file that tracks all calls.operator==
C++17std::async()
[[nodiscard]][[gnu:warn_unused_result]]_Check_return_
new()[[nodiscard]]new

[[maybe_unused]]Attributes

A new attribute [[maybe_unused]]prevents the compiler from issuing warnings when a variable is not used . The new attribute can be applied to the declaration of a class, a type used typedefor defined, a variable, a non-static data member, a function, an enumeration type, an enumeration value, etc.using

For example, one of the effects is to define a parameter that may not be used:

void foo(int val, [[maybe_unused]] std::string msg)
{
    
    
#ifdef DEBUG
    log(msg);
#endif
    ...
}

Another example is defining a member that may not be used:

class MyStruct {
    
    
    char c;
    int i;
    [[maybe_unused]] char makeLargerSize[100];
    ...
};

Note that you cannot apply to a statement [[maybe_unused]]. Therefore, you cannot directly [[maybe_unused]]use the offset [[nodiscard]]effect:

[[nodiscard]] void* foo();
int main()
{
    
    
    foo();                              // 警告:返回值没有使用
    [[maybe_unused]] foo();             // 错误:maybe_unused不允许出现在此
    [[maybe_unused]] auto x = foo();    // OK
}

[[fallthrough]]Attributes

A new attribute [[fallthrough]]prevents the compiler from issuing a warning if a statement switchis missing for a label within a statement break. For example:

void commentPlace(int place)
{
    
    
    switch (place) {
    
    
        case 1:
            std::cout << "very ";
            [[fallthrough]];
        case 2:
            std::cout << "well\n";
            break;
        default:
            std::cout << "OK\n";
            break;
    }
}

In this example, when the parameter is 1, the output will be:

very well

case 1The statements in and case 2will be executed. Note that this attribute must be used as a single statement and must be terminated with a semicolon. Also switchit cannot be used in the last branch of the statement.

Generic attribute extension

Since C++17then the following general features about properties have become available:

  • Attributes can now be used to mark namespaces. For example, you can deprecate a namespace like this:
namespace [[deprecated]] DraftAPI {
    
    
    ...
}

This also applies to inline and anonymous namespaces.

  • Properties can now mark enumerators (values ​​of enum type).
    For example, you can introduce a new enum value as a replacement for an existing (and now deprecated) enum value like this:
enum class City {
    
     Berlin = 0,
                  NewYork = 1,
                  Mumbai = 2,
                  Bombay [[deprecated]] = Mumbai,
                  ... };

Here Mumbaiis Bombaya numeric code representing the same city, but the use Bombayhas been marked as obsolete. Note that for enumeration values, the attribute is placed after the identifier .

  • User-defined attributes should generally be defined in a custom namespace. Prefixes can now be used usingto avoid re-typing the namespace for each property. That is, the following code:
[[MyLib::WebService, MyLib::RestService, MyLib::doc("html")]] void foo();

can be replaced by

[[using MyLib: WebService, RestService, doc("html")]] void foo();

usingNote that duplicating namespaces when prefixes are used will result in an error:

[[using MyLib: MyLib::doc("html")]] void foo(); // ERROR

Other language features

nested namespace

Since it was first proposed in 2003, C++the standards committee has finally agreed to define nested namespaces in the following way:

namespace A::B::C {
    
    
    ...
}

Equivalent to:

namespace A {
    
    
    namespace B {
    
    
        namespace C {
    
    
            ...
        }
    }
}

Note that there is currently no support for nested inline namespaces. This is because inlinethere is an ambiguity whether to apply to the innermost or the entire namespace (useful in both cases).

There is a defined order of expression evaluation

Many C++books contain code that seems intuitively correct, but strictly speaking they may lead to undefined behavior. A simple example is to replace multiple substrings in a string:

std::string s = "I heard it even works if you don't believe";
s.replace(0, 8, "").replace(s.find("even", 4, "sometimes")
                   .replace(s.find("you don't"), 9, "I");

The usual assumption is that the first 8 characters are replaced by the empty string, "even"replaced "sometimes"by, replaced "you don't"by "I". So the result is:

it sometimes works if I believe

However prior to C++17 the final result was not actually guaranteed. Because the functions that find substring positions find()may be called at any time before their return values ​​are needed, rather than executing expressions sequentially from left to right as intuitively. In fact, all find()calls may be executed before the first replacement is performed, so the result becomes:

it even worsometimesf youIlieve

Other outcomes are also possible:

it sometimes workIdon’t believe
it even worsometiIdon’t believe

As another example, consider using the output operator to print several interdependent values:

std::cout << f() << g() << h();

The usual assumption is that the f(), g(), and h()functions are called in sequence. However, this assumption is actually wrong. f(), g(), h()may be called in any order. When the calling order of these three functions will affect the return value, strange results may appear.

As a concrete example, until C++17, the behavior of the following code was undefined:

i = 0;
std::cout << ++i << ' ' << --i << '\n';

Before C++17, it may output 1 0, but it may also output 0 -1or 0 0, which has nothing to do with whether the variable iis inta user-defined type (though for basic types, the compiler will generally give a warning in this case).

To solve this undefined problem, the C++17 standard redefines the evaluation order of some operators, so these operators now have a fixed evaluation order:

  • For operations
e1 [ e2 ]
e1 . e2
e1 .* e2
e1 ->* e2
e1 << e2
e1 >> e2

e1 is now guaranteed to be evaluated before e2 , so the order of evaluation is left to right . Note, however, that the order of evaluation of different arguments within the same function call remains undefined. That is to say:

e1.f(a1, a2, a3);

e1.fThe guarantee in is evaluated before a1, a2, . a3However a1, the order of evaluation of a2, , a3is still undefined.

  • all assignment operations
e2 = e1
e2 += e1
e2 *= e1
...

e1 on the right is now guaranteed to be evaluated before e2 on the left.

  • Finally, operations like guaranteed memory allocation in newexpressions occur before e is evaluated. The initialization of a new object is guaranteed to complete before the object is used for the first time. All these guarantees apply to all primitive types and custom types . Therefore, sincenew Type(e)
    C++17
std::string s = "I heard it even works if you don't believe";
s.replace(0, 8, "").replace(s.find("even"), 4, "always")
                   .replace(s.find("don't believe"), 13, "use C++17");

Guaranteed to schange the value of to:

it always works if you use C++17

Because now every find()previous replace operation is guaranteed to find()complete before the expression is evaluated.

Another example, the following statement:

i = 0;
std::cout << ++i << ' ' << --i << '\n';

For any type ithe output is guaranteed to be 1 0. However, the order of operations for most other operators is still unknown. For example:

i = i++ + i;    // 仍然是未定义的行为

Here, the rightmost one imay ibe evaluated before or after the increment. Another application of the new expression evaluation order is to define a function that inserts spaces before arguments.

backward incompatibility

The new defined order of evaluation may affect the output of existing programs . For example, consider the following program:

#include <iostream>
#include <vector>

void print10elems(const std::vector<int>& v) {
    
    
    for (int i = 0; i < 10; ++i) {
    
    
        std::cout << "value: " << v.at(i) << '\n';
    }
}
int main(){
    
    
    try {
    
    
        std::vector<int> vec{
    
    7, 14, 21, 28};
        print10elems(vec);
    }
    catch (const std::exception& e) {
    
       // 处理标准异常
        std::cerr << "EXCEPTION: " << e.what() << '\n';
    }
    catch (...) {
    
                           // 处理任何其他异常
        std::cerr << "EXCEPTION of unknown type\n";
    }
}

vector<>Because there are only 4 elements in this program , an exception will be thrown when print10elems()called with an invalid index in the loop :at()

std::cout << "value: " << v.at(i) << "\n";

Before C++17, the output might be:

value: 7
value: 14
value: 21
value: 28
EXCEPTION: ...

Because at()it is allowed to "value: "be called before output, the output at the beginning can be skipped when the index is wrong "value: ".
Since C++17then, the output is guaranteed to be:

value: 7
value: 14
value: 21
value: 28
value: EXCEPTION: ...

Because now "value: "the output is guaranteed to be at()before the call.

More relaxed rules for initializing enum values ​​with integers

For an enumeration type variable with a fixed underlying type, it is from the C++17beginning to be directly list-initialized with an integer value . This works for both unscoped enums with explicit types and all scoped enums, since they all have a default underlying type:

// 指明底层类型的无作用域枚举类型
enum MyInt : char {
    
     };
MyInt i1{
    
    42};       // 自从C++17起OK(C++17以前ERROR)
MyInt i2 = 42;      // 仍然ERROR
MyInt i3(42);       // 仍然ERROR
MyInt i4 = {
    
    42};    // 仍然ERROR

// 带有默认底层类型的有作用域枚举
enum class Weekday {
    
     mon, tue, wed, thu, fri, sat, sun };
Weekday s1{
    
    0};      // 自从C++17起OK(C++17以前ERROR)
Weekday s2 = 0;     // 仍然ERROR
Weekday s3(0);      // 仍然ERROR
Weekday s4 = {
    
    0};   // 仍然ERROR

The result is exactly the same if Weekdaythere is an explicit underlying type:

// 带有明确底层类型的有作用域枚举
enum class Weekday : char {
    
     mon, tue, wed, thu, fri, sat, sun };
Weekday s1{
    
    0};      // 自从C++17起OK(C++17以前ERROR)
Weekday s2 = 0;     // 仍然ERROR
Weekday s3(0);      // 仍然ERROR
Weekday s4 = {
    
    0};   // 仍然ERROR

You still can't use list-initialization for unscoped enum types that don't have an explicit underlying type (there isn't one class) :enum

enum Flag {
    
     bit1=1, bit2=2, bit3=4 };
Flag f1{
    
    0};     // 仍然ERROR

Note that list initialization doesn't allow narrowing , so you can't pass a float:

enum MyInt : char {
    
     };
MyInt i5{
    
    42.2}; // 仍然ERROR

A technique for defining new integer types is to simply define an enum type with an existing integer type as the underlying type, as in the example above MyInt. One of the motivations for this feature was to support this trick, without which new objects would not be able to be initialized without a cast.
In fact, C++17the standard library std::bytehas used this feature directly since.

Fixed autotype list initialization

Since C++11the introduction of curly-brace uniform initialization in , whenever autoa list-initialization is used instead of an explicit type, some unintuitive results arise:

int x{
    
    42};       // 初始化一个int
int y{
    
    1, 2, 3};  // ERROR
auto a{
    
    42};      // 初始化一个std::initializer_list<int>
auto b{
    
    1, 2, 3}; // OK:初始化一个std::initializer_list<int>

These inconsistencies when using list initialization directly (without =) have now been fixed . So the behavior of the following code becomes:

int x{
    
    42};       // 初始化一个int
int y{
    
    1, 2, 3};  // ERROR
auto a{
    
    42};      // 现在初始化一个int
auto b{
    
    1, 2, 3}; // 现在ERROR

Note that this is a breaking change (breaking change) , because it may cause the behavior of many codes to change silently.
As a result, compilers that support this change C++11will now enable this change even in mode. For mainstream compilers, the versions that accept this change are Visual Studio 2015, g++5, , respectively clang3.8.
Note : c++11mode has been fixed
Note that when using autocopy list initialization (with = ) it is still initializing one std::initializer_list<>:

auto c = {
    
    42};      // 仍然初始化一个std::initializer_list<int>
auto d = {
    
    1, 2, 3}; // 仍然OK:初始化一个std::initializer_list<int>

So now again there is a significant difference between direct initialization (without = ) and copy initialization (with = ):

auto a{
    
    42};     // 现在初始化一个int
auto c = {
    
    42};  // 仍然初始化一个std::initializer_list<int>

The preprocessing code is as follows:

  int a = {
    
    42};
  std::initializer_list<int> b = std::initializer_list<int>{
    
    3};

This is one of the reasons why direct list initialization (brace initialization without =) is preferred.

hexadecimal floating point literal

C++17Allows specifying hexadecimal floating point literals (some compilers even C++17supported this before). This feature is very useful when an accurate floating-point representation is required (if the decimal floating-point literal is used directly, the actual precise value stored is not guaranteed).

For example:

#include <iostream>
#include <iomanip>

int main()
{
    
    
    // 初始化浮点数
    std::initializer_list<double> values {
    
    
            0x1p4,          // 16
            0xA,            // 10
            0xAp2,          // 40
            5e0,            // 5
            0x1.4p+2,       // 5
            1e5,            // 100000
            0x1.86Ap+16,    // 100000
            0xC.68p+2,      // 49.625
    };

    // 分别以十进制和十六进制打印出值:
    for (double d : values) {
    
    
        std::cout << "dec: " << std::setw(6) << std::defaultfloat << d
                  << "  hex: " << std::hexfloat << d << '\n';
    }
}

The program defines different floating-point values ​​by using existing and newly added hexadecimal floating-point notations. The new notation is a base-2 scientific notation notation:

  • Significant digits/mantissa are written in hexadecimal
  • The exponent part is written in decimal, which means multiplying by 2 to the nth power

For example 0xAp2the value is 40 (10 *2 2). This value can also be written `0x1.4p+5`, which is 1.25`*`32 (0.4 is a hexadecimal fraction, which is equal to 0.25 in decimal, 2 5 = 32).
The output of the program is as follows:

dec:     16  hex: 0x1p+4
dec:     10  hex: 0x1.4p+3
dec:     40  hex: 0x1.4p+5
dec:      5  hex: 0x1.4p+2
dec:      5  hex: 0x1.4p+2
dec: 100000  hex: 0x1.86ap+16
dec: 100000  hex: 0x1.86ap+16
dec: 49.625  hex: 0x1.8dp+5

As the above example shows, the notation for hexadecimal floating point numbers has been around since the beginning, because the std::hexfloatoperators used by output streams C++11have been around since.

UTF-8 character literals

String literals prefixed with C++11. However, this prefix cannot be used with character literals. This fixes this, so now it's possible to write:C++u8UTF-8C++17

auto c = u8'6'; // UTF-8编码的字符6

In C++17, u8'6'the type of is char, which C++20may become in char8_t, so autoit would be better to use it here.

By using this prefix it is now guaranteed that character values ​​are UTF-8encoded. You can use all 7-bit US-ASCIIcharacters that are UTF-8represented and US-ASCIIrepresented exactly the same. That is, the character '6' represented u8'6'by 7 bits is also valid US-ASCII(also a valid ISO Latin-1, ISO-8859-15, character in the basic Windowscharacter set).

Usually your source code characters are interpreted as US-ASCIIor UTF-8and the result is the same, so this prefix is ​​not necessary. cThe value of is always 54(hexadecimal 36).

Here's some background to why this prefix is ​​necessary: ​​For characters and string literals in source code, C++normalizes the characters you can use rather than the values ​​of those characters. These values ​​depend on the source charset . When a compiler generates an executable program from source code it uses the runtime character set . The source charset is almost always a 7-bit US-ASCIIencoding, and the runtime charset is usually the same. This means that in any C++ program, all identical character and string literals (whether u8prefixed or not) always have the same value.

However, in some particularly rare scenarios this is not the case. For example, on older IBM machines using the EBCDIC character set, the character '6' would have the value 246 (F6 in hexadecimal). In a program using the EBCDIC character set, cthe value of the above character will be 246 instead of 54. If this program is run on a UTF-8 encoded platform, it may print the character ö, which is encoded in ISO/IEC 8859-x The value in is 246. In this case, this prefix is ​​necessary.

Note that u8it can only be used for a single character, and the UTF-8 encoding of the character must occupy only one byte. One initializes as follows:

char c = u8'ö';

is not allowed, because the UTF-8 encoding of the German accent character ö is a sequence of two bytes, 195 and 182 (C3 B6 in hexadecimal).
Therefore, character and string literals now accept the following prefixes:

  • u8 for single-byte US-ASCII and UTF-8 encodings
  • u for two-byte UTF-16 encoding
  • U for UTF-32 encoding of four bytes
  • L is used for wide characters that are not explicitly encoded, and may be two or four bytes

Exception declaration as part of type

Since C++17then, exception handling declarations have become part of the function type . That is, the types of the following two functions are different:

void fMightThrow();
void fNoexcept() noexcept;  // 不同类型

Previously C++17the types of these two functions were the same. One such problem is that if a function that may throw an exception is assigned to a function pointer that is guaranteed not to throw an exception , then an exception may be thrown when called:

void (*fp)() noexcept;  // 指向不抛异常的函数的指针
fp = fNoexcept;         // OK
fp = fMightThrow;       // 自从C++17起ERROR

It is still valid to assign a non-throwing function to a throwing function pointer :

void (*fp2)();      // 指向可能抛出异常的函数的指针
fp2 = fNoexcept;    // OK
fp2 = fMightThrow;  // OK

Therefore, if only undeclared function pointers are used in the program noexcept, they will not be affected by this feature. But note that declarations in function pointers can no longer be violated noexcept(this could break existing programs with good intentions).

Overloading a function with the same signature but differing exception declarations is not allowed (just like overloading a function with a different return value is not allowed):

void f3();
void f3() noexcept;        // ERROR

Note that other rules are not affected. noexceptFor example, you still cannot ignore declarations in base classes :

class Base {
    
    
public:
    virtual void foo() noexcept;
    ...
};

class Derived : public Base {
    
    
public:
    void foo() override;    // ERROR:不能重载
    ...
};

Here, the member function in the derived class foo()is of a different type than the base class foo()so cannot be overloaded. This code doesn't compile, even without overridethe modifier code, because we can't overload with a more relaxed exception declaration.

Use traditional exception declarations

When using traditional noexceptdeclarations, whether a function throws an exception depends on whether the condition is trueeither false:

void f1();
void f2() noexcept;
void f3() noexcept(sizeof(int)<4);  // 和f1()或f2()的类型相同
void f4() noexcept(sizeof(int)>=4); // 和f3()的类型不同

Here f3()the type depends on the value of the condition:

  • If sizeof(int)4 or greater is returned, the signature is equivalent to
void f3() noexcept(false); // 和f1()类型相同
  • If sizeof(int)the returned value is less than 4, the signature is equivalent to
void f3() noexcept(true);  // 和f2()类型相同

Because f4()the exception conditions of and f3()are exactly the opposite, the types of f3()and f4()are always different (that is, they must be one that may throw an exception and the other that cannot).

The old-style not-throwing declaration still works but has been deprecated since C++11:

void f5() throw();  // 和void f5() noexcept等价但已经被废弃

Dynamic exception declarations with parameters are no longer supported (deprecated since C++11):

void f6() throw(std::bad_alloc); // ERROR:自从C++17起无效

Impact on generic libraries

Will noexceptbe part of the type means there will be some impact on the generics library. For example, the following code was valid until C++14, but no longer compiles from C++17 onwards:

#include <iostream>

template<typename T>
void call(T op1, T op2)
{
    
    
    op1();
    op2();
}

void f1() {
    
    
    std::cout << "f1()\n";
}
void f2() noexcept {
    
    
    std::cout << "f2()\n";
}

int main()
{
    
    
    call(f1, f2);   // 自从C++17起ERROR
}

The problem is that the type of and C++17is no longer the same, so when instantiating the template function the compiler cannot deduce the type , and since , you need to specify two different template parameters to pass compilation:f1()f2()call()时T
C++17

template<typename T1, typename T2>
void call(T1 op1, T2 op2)
{
    
    
    op1();
    op2();
}

Now if you wanted to overload all possible function types, you would need twice as many overloads. For example, for std::is_function<>the definition of a standard library type trait, the main template is defined as follows, which is used to match Tthe case that is not a function:

// 主模板(匹配泛型类型T不是函数的情况):
template<typename T> struct is_function : std::false_type {
    
     };

The template is std::false_typederived from , so is_function<T>::valueany type Twill be returned false.

For any type that is a function, there is std::true_typea partial specialization of derived from, so members valueare always returned true:

// 对所有函数类型的部分特化版
template<typename Ret, typename... Params>
struct is_function<Ret (Params...)> : std::true_type {
    
     };

template<typename Ret, typename... Params>
struct is_function<Ret (Params...) const> : std::true_type {
    
     };

template<typename Ret, typename... Params>
struct is_function<Ret (Params...) &> : std::true_type {
    
     };

template<typename Ret, typename... Params>
struct is_function<Ret (Params...) const &> : std::true_type {
    
     };
...

C++17There were a total of 24 partial specialization versions of this feature before: because the function type can be decorated with and constmodifiers volatile, there may also be lvalue reference (&) or rvalue reference (&&) modifiers, and variable parameters need to be overloaded version of the list. The number of partial specializations is
now doubled in , since a modified version needs to be added to all of them:C++17noexcept

...
// 对所有带有noexcept声明的函数类型的部分特化版本
template<typename Ret, typename... Params>
struct is_function<Ret (Params...) noexcept> : std::true_type {
    
     };

template<typename Ret, typename... Params>
struct is_function<Ret (Params...) const noexcept> : std::true_type {
    
     };

template<typename Ret, typename... Params>
struct is_function<Ret (Params...) & noexcept> : std::true_type {
    
     };

template<typename Ret, typename... Params>
struct is_function<Ret (Params...) const & noexcept> : std::true_type {
    
     };
...

Libraries that do not implement noexceptoverloading may noexceptnot compile in scenarios that require the use of functions with .

single parameterstatic_assert

Since C++17then, the previously static_assert()required parameter for error messages has become optional. That is to say, the diagnostic information output when the assertion fails is completely dependent on the implementation of the platform. For example:

#include <type_traits>

template<typename t>
class C {
    
    
    // 自从C++11起OK
    static_assert(std::is_default_constructible<T>::value,
                  "class C: elements must be default-constructible");

    // 自从C++17起OK
    static_assert(std::is_default_constructible_v<T>);
    ...
};

An example of a new version of a static assertion without an error message parameter also uses the type trait suffix _v.

Preconditioning__has_include

C++17Extended preprocessing, adding a macro that checks whether a header file can be included. For example:

#if __has_include(<filesystem>)
#  include <filesystem>
#  define HAS_FILESYSTEM 1
#elif __has_include(<experimental/filesystem>)
#  include <experimental/filesystem>
#  define HAS_FILESYSTEM 1
#  define FILESYSTEM_IS_EXPERIMENTAL 1
#elif __has_include("filesystem.hpp")
#  include "filesystem.hpp"
#  define HAS_FILESYSTEM 1
#  define FILESYSTEM_IS_EXPERIMENTAL 1
#else
#  define HAS_FILESYSTEM 0
#endif

Evaluates to 1 when the corresponding #includecommand is valid . __has_include(...)No other factors affect the result (for example, whether the corresponding header file has been included does not affect the result). Also, while evaluating to true may indicate that the corresponding header file does exist, it does not guarantee that its contents are as expected. Its content may be empty or invalid.

__has_includeis a pure preprocessing directive. So it cannot be used as a conditional expression in the source code:

if (__has_include(<filesystem>)) {
    
      // ERROR
}

reference

C++17 - The Complete Guide

Guess you like

Origin blog.csdn.net/MMTS_yang/article/details/130391926