Introduction to C++11\14\17\20 Features (reproduced)

This article introduces in sequence according to the list of features listed by cppreference .
This article is long, and it is all about the language features of "discrete" content, so it is necessary to add a table of contents, but when it was published to "Jianshu", it was discovered that "Jianshu" cannot automatically generate a table of contents.
But fortunately, I found a very, very good open source plug-in on github some time ago: Jane Yue .
It can perform secondary processing on the page to provide a page with a unified style and consistent experience, and it also supports automatic generation of directories . The plug-in also has a sense of product design. I feel that the author has practiced a sentence: "A product manager who can't design is not a good developer." Therefore, readers are highly recommended to install this plugin (you can install it according to the official tutorial linked above), and I believe it will give you a different reading experience.

C++11 new features

#01 auto and decltype

auto : For a variable, specifies that its type is to be automatically deduced from its initializer. Example:

auto a = 10;    // 自动推导 a 为 int
auto b = 10.2;  // 自动推导 b 为 double
auto c = &a;    // 自动推导 c 为 int*
auto d = "xxx"; // 自动推导 d 为 const char*

decltype : The declared type of the deduced entity, or the type of the expression. In order to solve the defect that the auto keyword can only deduce the type of variables, it appears. Example:

int a = 0;

decltype(a) b = 1;        // b 被推导为 int 类型
decltype(10.8) c = 5.5;   // c 被推导为 double 类型
decltype(c + 100) d;      // d 被推导为 double

struct { double x; } aa;
decltype(aa.x) y;         // y 被推导为 double 类型
decltype(aa) bb;          // 推断匿名结构体类型

In C++11, the combination of auto and decltype can also be used to deduce the return type of the function with the help of "tail return type". Example:

// 利⽤ auto 关键字将返回类型后置
template<typename T, typename U>
auto add1(T x, U y) -> decltype(x + y) {
  return x + y;
}

Starting from C++14 , only auto and return type deduction are supported, see the C++14 section below.

#02 defaulted and deleted functions

In C++, if the programmer does not customize, the compiler will generate " constructor ", " copy constructor ", " copy assignment function " and so on for the programmer by default .

However, if the programmer defines the above functions, the compiler will not automatically generate these functions.

In the actual development process, we sometimes need to prohibit some default functions while retaining some default functions .

For example, when creating a " class that does not allow copying ", in traditional C++, we often have the following conventional code:

// 除非特别熟悉编译器自动生成特殊成员函数的所有规则,否则意图是不明确的
class noncopyable   {
public:
  // 由于下⽅有⾃定义的构造函数(拷⻉构造函数)
  // 编译器不再⽣成默认构造函数,所以这⾥需要⼿动定义构造函数
  // 但这种⼿动声明的构造函数没有编译器⾃动⽣成的默认构造函数执⾏效率⾼
  noncopyable() {};
private: 
  // 将拷⻉构造函数和拷⻉赋值函数设置为 private
  // 但却⽆法阻⽌友元函数以及类成员函数的调⽤
  noncopyable(const noncopyable&);
  noncopyable& operator=(const noncopyable&);
};

The traditional C++ routine processing method has the following defects:

  1. Since the "copy constructor" is customized, the compiler no longer generates a "default constructor", and the "no-argument constructor" needs to be manually and explicitly defined
  2. Manually explicitly defined "no-argument constructor" is less efficient than "default constructor"
  3. Although the "copy constructor" and "copy assignment function" are private, they are hidden from the outside. But it cannot prevent the call of friend functions and class member functions
  4. Intention is ambiguous unless you are very familiar with all the rules for automatic compiler-generated special member functions

To this end, C++11 introduces the default and delete keywords to explicitly retain or prohibit special member functions:

class noncopyable {
 public:
  noncopyable() = default;
  noncopyable(const noncopyable&) = delete;
  noncopyable& operator=(const noncopyable&) = delete;
};

#03 final 与 override

In traditional C++, override parent class virtual functions as follows:

struct Base {
  virtual void foo();
};
struct SubClass: Base {
  void foo();
};

There are certain hidden dangers in the above code:

  • The programmer does not want to override the virtual function of the parent class, but defines a member function with the same name . Accidental overwrites due to no compiler checks and hard to spot
  • After the virtual function of the parent class is deleted, the compiler will not check and warn, which may cause serious errors

For this reason, C++11 introduces override to explicitly declare to override the virtual function of the base class . If there is no such virtual function, it will not pass compilation:

class Parent {
  virtual void watchTv(int);
};
class Child : Parent {
  virtual void watchTv(int) override;    // 合法
  virtual void watchTv(double) override; // 非法,父类没有此虚函数
};

And final terminates the virtual class being inherited or the virtual function being overwritten:

class Parent2 {
  virtual void eat() final;
};

class Child2 final : Parent2 {};  // 合法

class Grandson : Child2 {};       // 非法,Child2 已经 Final,不可被继承

class Child3 : Parent2 {
  void eat() override; // 非法,foo 已 final
};

#04 trailing return type

Look at a more complex function definition:

// func1(int arr[][3], int n) 为函数名和参数
// (* func1(int arr[][3], int n)) 表示对返回值进⾏解引⽤操作
// (* func1(int arr[][3], int n))[3] 表示返回值解引⽤后为⼀个⻓度为 3 的数组
// int (* func1(int arr[][3], int n))[3] 表示返回值解引⽤后为⼀个⻓度为 3 的 int 数组
int (* func1(int arr[][3], int n))[3] {
  return &arr[n];
}

C++11 introduces "Trailing Return Type", connects "Function Return Type" to the back of the function through the -> symbol , and cooperates with auto to simplify the definition of the above complex function:

// 返回指向数组的指针
auto fun1(int arr[][3], int n) -> int(*)[3] {
  return &arr[n];
}

Trailing return types are often used in " lambda expressions " and " template function returns ":

// 使⽤尾置返回类型来声明 lambda 表达式的返回类型
[capture list] (params list) mutable exception->return_type { function body }

// 在模板函数返回中结合 auto\decltype 声明模板函数返回值类型
template<typename T, typename U>
auto add(T x, U y) -> decltype(x + y) {
  return x + y;
}

#05 Rvalue references

What are lvalues ​​and rvalues

  • lvalue : the value of an expression that has an object in memory whose address is determined
  • rvalue : the value of any expression that is not an lvalue. Rvalues ​​can be divided into " traditional prvalues " and " perishable values "

What are the above-mentioned "traditional prvalues" and "perishable values"?

  • A prvalue : an rvalue prior to C++11. include:

    1. Common literals such as 0, "123", or expressions are literals
    2. Unnamed temporary objects, such as functions returning temporary objects
  • xvalue : A concept that comes with rvalue references introduced in C++11. include:

    1. The return value of a "function returning an rvalue reference". For example, the return value of a function whose return type is T&&
    2. The return value of the "conversion function converted to an rvalue reference", such as the return value of the std::move() function

At the same time, lvalue + xvalue is also called "panlvalue". These concepts may be confusing for newcomers, let's sort them out, as shown in the following figure:

 Whether an lvalue or an rvalue can be determined by taking the address operator & , the address that can be obtained correctly through & is an lvalue, otherwise it is an rvalue.

int i = 0;
int* p_i = &i;            // 可通过 & 取出地址,固 i 为左值
cout << p_i << endl;

int* p_i_plus = &(i + 1); // 非法,i + 1 为右值
int* p_i_const = &(0);    // 非法,0 为右值

What are lvalue references and rvalue references

Before C++11, we often used references to lvalues, that is, lvalue references, declared with the & symbol:

int j = 0;
int& ref_j = j;           // ref_j 为左值引⽤
int& ref_ret = getVal();  // ref_ret 为左值引用

int& ref_j_plus = j + 1;  // ⾮法,左值引⽤不能作⽤于右值
int& ref_const = 0;       // 非法,左值引用不能作用于右值

As shown in the above sample code, ref_j_plus and ref_const are lvalue references often used in traditional C++, and cannot be applied to rvalues ​​such as j+1 or 0 .

C++11 introduces references to rvalues, that is, rvalue references, which are declared using the && symbol:

int&& ref_k_plus = (i + 1); // ref_k_plus 为右值引用,它绑定了右值 i + 1
int&& ref_k = 0;            // ref_k 为右值引用,它绑定了右值 0 

Features of rvalue references

Take the following code as an example:

int getVal() {
  return 1;
}

int main() {
  // 这里存在两个值:
  //    1. val(左值)
  //    2. getVal() 返回的临时变量(右值)
  // 其中 getVal() 返回的临时变量赋值给 val 后会被销毁
  int val = getVal();
  return 0;
}

In the above code, the "temporary variable" generated by the getVal function needs to be copied to the lvalue val before being destroyed.

But if you use an rvalue reference:

// 使用 && 来表明 val 的类型为右值引用
// 这样 getVal() 返回的临时对象(右值) 将被「续命」
// 拥有与 val 一样长的生命周期
int&& val = getVal();

The above code embodies the first feature of rvalue references :

Through the declaration of rvalue reference, the rvalue can be "reborn", and the life cycle is as long as the variable life cycle of the rvalue reference type.

Look at the following example:

template<typename T>
void f(T&& t) {}

f(10);      // t 为右值

int x = 10;
f(x);       // t 为左值

The above example embodies the second feature of rvalue references :

In the case of automatic type inference (such as template functions, etc.) , T&& t is an undetermined reference type , that is, t is not necessarily an rvalue. If it is lvalue-initialized, then t is an lvalue. It is an rvalue if it was initialized by an rvalue.

It is precisely because of the above characteristics that C++11 introduces rvalue references to achieve the following goals:

  • Implement move semantics . Solve the problem of inefficient copying of temporary objects
  • Achieve perfect forwarding . Solve the problem of function forwarding rvalue feature loss

Move semantics brought by rvalue references

Prior to C++11, the assignment of temporary objects used inefficient copying.

For example, the whole process is like moving an elephant from one refrigerator to another. The traditional C++ approach is to copy an identical elephant in the second refrigerator, and then move the elephant from the first refrigerator to another. Elephants are destroyed, which is obviously not a natural way of operation.

See the following example:

class HasPtrMem1 {
 public:
  HasPtrMem1() : d(new int(0)) {}
  ~HasPtrMem1() { delete d; }
  int* d;
};

int main() {
  HasPtrMem1 a1;
  HasPtrMem1 b1(a1);

  cout << *a1.d << endl;
  cout << *b1.d << endl;

  return 0;
}

In the above code, HasPtrMem1 b(a)the default "copy constructor" generated by the compiler will be called to make a copy, and the bitwise copy (shallow copy) will be performed, which will lead to a dangling pointer problem [1] .

Dangling pointer problem [1] : After executing the main function, the above code will destroy the a and b objects, and then call the corresponding destructor to execute the delete d operation. However, since the member d in the objects a and b points to the same memory, after one of the objects is destroyed, the pointer d in the other object no longer points to valid memory, and the d of this object becomes a dangling memory. pointer.

Freeing memory on a dangling pointer will cause serious bugs. Therefore, a deep copy must be performed for the above scenarios:

class HasPtrMem2 {
 public:
  HasPtrMem2() : d(new int(0)) {}
  HasPtrMem2(const HasPtrMem2& h) :
      d(new int(*h.d)) {}
  ~HasPtrMem2() { delete d; }
  int* d;
};

int main() {
  HasPtrMem2 a2;
  HasPtrMem2 b2(a2);

  cout << *a2.d << endl;
  cout << *b2.d << endl;

  return 0 ;
}

In the above code, we have customized the implementation of the copy constructor. We have implemented deep copy by allocating new memory through new, which avoids the problem of "dangling pointer", but also introduces new problems.

It is very common in traditional C++ programming that the copy constructor allocates new memory for pointer members and copies them. But sometimes we don't need such a copy:

HasPtrMem2 GetTemp() {
  return HasPtrMem2();
}

int main() {
  HasPtrMem2 a = GetTemp();
}

In the above code, the temporary object returned by GetTemp is deeply copied and then destroyed. As shown in the figure below:

If the pointer member in HasPtrMem2 is a complex and huge data type, it will cause a lot of performance consumption.

Going back to the elephant moving analogy, it is actually more efficient to take the elephant out of the first refrigerator and put it in the second refrigerator. Similarly, can we not use the copy constructor when assigning a temporary object to a variable? The answer is yes, as shown in the figure below:

In C++11, a constructor that "steals" resources like this is called a " move constructor ", and this "stealing" behavior is called " move semantics ", which can be understood as "move for your own use". ".

Of course, the implementation needs to define the corresponding "move constructor" in the code:

class HasPtrMem3 {
  public:
    HasPtrMem3() : d(new int(0)) {}
    HasPtrMem3(const HasPtrMem3& h) : 
        d(new int(*h.d)) {}
    HasPtrMem3(HasPtrMem3&& h) : d(h.d) {
      h.d = nullptr;
    }
    ~HasPtrMem3() { delete d; }
    int* d;
};

Note that the "move constructor" still has the problem of dangling pointers, so after "stealing" resources through the move constructor, the hd pointer of the temporary object should be set to null to prevent two pointers from pointing to the same memory. is destructed twice.

The parameter in the "move constructor" is HasPtrMem3&& h is an rvalue type [2] , and the temporary object of the return value is an rvalue type, which is why the temporary object of the return value can match the "move constructor".

Rvalue type[2] : Note that it is different from the second feature of rvalue reference mentioned above. This is not a type deduction scenario. HasPtrMem3 is a definite type, so HasPtrMem3&& h is a definite rvalue type.

The above move semantics match temporary values ​​through rvalue references, so can lvalues ​​use move semantics to optimize performance? C++11 provides us with std::move function to achieve this goal:

{
  std::list<std::string> tokens;              // tokens 为左值
  // 省略初始化...
  std::list<std::string> t = tokens;          // 这里存在拷贝
}

std::list<std::string> tokens;
std::list<std::string> t = std::move(tokens); // 这里不存在拷贝

The std::move function does not actually move any resources. The only thing it does is to cast an lvalue into an rvalue reference , so as to match the "move constructor" or "move assignment operator" and apply move semantics Implement resource movement. However, all containers in C++11 implement move semantics, so the above code using the list container can avoid copying and improve performance.

Perfect forwarding brought by rvalue references

In traditional C++, an rvalue parameter is converted to an lvalue, that is, it cannot be forwarded according to the original type of the parameter, as shown below:

template<typename T>
void forwardValue1(T& val) {
  // 右值参数变为左值
  processValue(val);
}

template<typename T>
void forwardValue1(const T& val) {
  processValue(val); // 参数都变成常量左值引用了
}

How to maintain the lvalue and rvalue characteristics of parameters, C++11 introduces std::forward, which will forward according to the actual type of the parameter:

void processValue(int& a) {
  cout << "lvalue" << endl;
}

void processValue(int&& a) {
  cout << "rvalue" << endl;
}

template<typename T>
void forwardValue2(T&& val) {
  // 照参数本来的类型进⾏转发
  processValue(std::forward<T>(val));
}

int main() {
  int i = 0;

  forwardValue2(i); // 传入左值,函数执行输出 lvalue
  forwardValue2(0); // 传入右值,函数执行输出 rvalue

  return 0;
}

#06 Move constructor and move assignment operator

It has already been mentioned in rule #05 , so I won't repeat it.

#07 Scoped enums

The enumeration types of traditional C++ have the following problems:

  • Each enumeration value is visible within its scope, which can easily cause naming conflicts
// Color 下的 BLUE 和 Feeling 下的 BLUE 命名冲突
enum Color { RED, BLUE };
enum Feeling { EXCITED, BLUE };
  • Will be implicitly converted to int, which may cause errors in scenarios where the conversion to int should not be
  • The data type of the enumeration cannot be specified, which makes the code difficult to understand, and forward declaration cannot be performed, etc.

 There are also some indirect solutions in traditional C++ that can properly solve or alleviate the above problems, such as using namespaces :

namespace Color { enum Type { RED, YELLOW, BLUE }; };

Or use classes, structures:

struct Color { enum Type { RED, YELLOW, BLUE }; };

However, the above solution usually solves the scope problem , but cannot solve the problem of implicit conversion and data type.

C++11 introduces enumeration classes to solve the above problems:

// 定义枚举值为 char 类型的枚举类
enum class Color:char { RED, BLACK };

// 使⽤
Color c = Color::RED;

#08 constexpr and literal types

constexpr : compile an expression or function to a constant result at compile time

constexpr modifies variables and functions:

// 修饰变量
constexpr int a = 1 + 2 + 3;
char arr[a]; // 合法,a 是编译期常量

// 修饰函数,使函数在编译期会成为常量表达式(如果可以)
// 如果 constexpr 函数返回的值不能在编译器确定,则 constexpr 函数就会退化为运行期函数(这样做的初衷是避免在为编译期和运行期写两份相同代码)
// constexpr 函数的设计其实不够严谨,所以 C++20 引入了 consteval (详见下文 C++20 部分)
// C++11 中,constexpr 修饰的函数只能包含 using 指令、typedef 语句以及 static_assert 
// C++14 实现了对其他语句的支持
constexpr int len_foo_constexpr() {
  return 5;
}

#09 Initialization List - Expand the scope of "initialization list"

In C++98/03, ordinary arrays or POD types can be initialized through initialization lists, for example:

POD type see #18 below

int arr1[3] = { 1, 2, 3 };

long arr2[] = { 1, 3, 2, 4 };
struct A { 
  int x;
  int y;
} a = { 1, 2 };

C++11 expands the applicable scope of "initialization list" so that it can be applied to the initialization of all types of objects:

class Dog {
 public:
  Dog(string name, int age) {
    cout << name << " "; cout << age << endl;
  }
};

Dog dog1 = {"cat1", 1};
Dog dog2 {"cat2", 2};

A more powerful "initialization list" can also be implemented through std::initializer_list , for example:

class Dog {
 public:
  Dog(initializer_list<int> list) {
   for (initializer_list<int>::iterator it = list.begin();
          it != list.end(); ++it) {
      cout << *it << " ";
    } 
    cout << endl;
  }
};

Dog dog3 = {1, 2, 3, 4, 5};

At the same time, initialization lists can also be used as formal parameters or return values ​​of ordinary functions :

// 形参
void watch(Dog dog) {
  cout << "watch" << endl;
}

watch({"watch_dog", 4});

// Dog 作为返回值

getDefaultDog() {
  return {"default", 3};
}

getDefaultDog();

#10 Delegating vs Inherited Constructors

Delegating construction : calling another constructor of the same class in one constructor Inheritance
construction : In C++ before C++11, subclasses need to declare the constructors owned by the parent class in turn, and pass the corresponding Initialization parameters. C++11 uses the keyword using to introduce inherited constructors, and uses a one-line statement to allow the compiler to automatically complete the above work.

class Parent {
  public:
    int value1;
    int value2;

  Parent() {
    value1 = 1;
  }

  Parent(int value) : Parent() { // 委托 Parent() 构造函数
    value2 = value;
  }
}

class Child : public Parent {
  public: 
    using Parent::Parent;         // 继承构造
}

#11 Brace-or-equal initializers

It has been mentioned above and will not be repeated

#12 nullptr

There are many defects in the definition of NULL in traditional C++, and the compiler often defines it as 0 when implementing it, which will cause overloading confusion. Consider the following code;

void foo(char*);
void foo(int);

Calling foo(NULL) will match the foo(int) function, which is obviously confusing.

C++11 introduces the keyword nullptr (type nullptr_t) to distinguish null pointers from 0, and nullptr can be implicitly converted to any pointer or member pointer type.

#13 long long

long : the target type will have a width of at least 32 bits
long long : the target type will have a width of at least 64 bits

Just as long type suffix needs "l" or "L", long long type suffix needs to add "ll" or "LL".

#14 char16_t and char32_t

In order to express Unicode strings in C++98, the wchar_t type is introduced to solve the problem that 1-byte char can only contain 256 characters.

However, since the length of the wchar_t type is different on different platforms, it has a certain impact on code porting. So C++11 introduces char16_t and char32_t , which have fixed lengths of 2 bytes and 4 bytes respectively .

char16_t : The type represented by UTF-16 characters, which is required to be large enough to represent any UTF-16 code unit (16 bits). It std::uint_least16_thas the same size, signedness, and alignment as , but is a separate type.

char32_t : - UTF-32 character representation type, required to be large enough to represent any UTF-32 code unit (32 bits). It std::uint_least32_thas the same size, signedness, and alignment as , but is a separate type.

At the same time, C++11 also defines 3 constant string prefixes:

  • u8 stands for UTF-8 encoding
  • u stands for UTF-16 encoding
  • U stands for UTF-32 encoding
char16_t UTF16[] = u"中国"; // 使用 UTF-16 编码存储

char32_t UTF16[] = U"中国"; // 使用 UTF-32 编码存储

#15 Type Aliases

In traditional C++, typedef is used to define a new name for a type. In C++11, we can use using to achieve the same effect, as shown below:

typedef std::ios_base::fmtflags flags;
using flags = std::ios_base::fmtflags;

Now that we have typedef, why introduce using? Of course because using can do more than typedef.

typedef can only define new names for "types", while templates are " used to generate types ", so the following code is illegal:

template<typename T, typename U>
class DogTemplate {
  public: 
    T attr1;
    U aatr2;
};

// 不合法
template<typename T>
typedef DogTemplate<std::vector<T>, std::string> DogT;

But with using it is possible to define an alias for the template:

template<typename T>
using DogT = DogTemplate<std::vector<T>, std::string>;

#16 Variadic templates

In traditional C++, a class template or function template can only accept a fixed number of template parameters.

However, C++11 allows any number of template parameters of any type, and does not need to fix the number of parameters when defining . As shown below:

template<typename... T> class DogT;

// 传⼊多个不同类型的模板参数
class DogT<int, 
            std::vector<int>,
            std::map<std::string,
            std::vector<int>>> dogT;

// 不传⼊参数( 0 个参数)
class DogT<> nothing;

// 第⼀个参数必传,之后为变⻓参数
template<typename require, typename... Args> class CatT;

The same can support template functions:

template<typename... Args>
void my_print(const std::string& str, Args... args) {
  // 使⽤ sizeof... 计算参数个数
  std::cout << sizeof...(args) << std::endl;
}

#17 Generalized (non-trivial) unions

Union Union provides us with the ability to define members of different types in a structure, but in traditional C++, not all data types can become data members of unions. For example:

struct Dog {
  Dog(int a, int b) : age(a), size(b) {}
  int age;
  int size;
}

union T {
  // C++11 之前为非法(d 不是 POD 类型)
  // C++11 之后合法
  Dog d;
  int id;
}

Refer to #18 below for POD types

C++11 removes the above-mentioned restrictions on the union [3] , and the standard stipulates that any non-reference type can become a data member of the union .

[3] The reason for the removal is that the long-term practice has proved that the restrictions made for compatibility with C are unnecessary.

#18 Promoted POD (trivial vs standard layout types)

POD is the abbreviation of Plain Old Data, Plain highlights it as a common data type, Old reflects its compatibility with C, for example, it can be copied using the memcpy() function, initialized using the memset() function, etc. .

Specifically, C++11 divides POD into a collection of two concepts: trivial (trival) and standard layout (standard layout).

Among them, ordinary classes or structures should meet the following requirements:

  1. Has trivial default constructor and destructor. That is, do not customize any constructor, or specify to use the default constructor through =default
  2. Has trivial copy constructor and move constructor
  3. Has trivial copy assignment operator and move assignment operator
  4. Does not contain virtual functions and virtual base classes

C++11 also provides an auxiliary class template is_trivial to realize whether it is trivial:

cout << is_trivial<DogT>::value << endl;

Another concept included in POD is "standard layout". A class or structure with a standard layout needs to meet the following requirements:

  1. All non-static members have the same access rights (public, private, protected)
  2. When a class or structure is inherited, one of the following two conditions is met:
    2.1 There are non-static members in the subclass, and there is only one base class containing only static members
    struct B1 { static int a; };
    struct B2 { static int b; };
    
    2.2 If the base class has non-static members, the subclass has no non-static members
    struct B2 { int a; } ;
    struct D2 : B2 { static int d; };
    
    From the above conditions, 1. As long as both the subclass and the base class have non-static members. 2. The subclass inherits multiple base classes, and multiple base classes have non-static members at the same time. Neither of these cases is part of the standard layout.
  3. The type of the first non-static member in a class is different from its base class
struct A : B { B b; };        // 非标准布局,第一个非静态成员 b 就是基本类型
struct A : B { int a; B b; }; // 标准布局,第一个非静态成员 a 不是基类 B 类型
  1. no virtual functions or virtual base classes
  2. All non-static data members conform to standard-layout types whose base classes also conform to standard-layout (recursively defined)

Similarly, C++11 provides an auxiliary class template is_standard_layout to help us judge:

cout << is_standard_layout<Dog>::value << endl;

Finally, C++11 also provides an auxiliary class template is_pod for one-time judgment of whether it is a POD:

cout << is_pod<Dog>::value << endl;

Understand the basic concept of POD, what function or benefit does POD have? POD can bring us the following advantages:

  1. Byte assignment. Safely use memset and memcpy to initialize and copy POD types
  2. Compatible with C memory layout. for interoperability with C functions
  3. Safeguard static initialization. Static initialization can effectively improve program performance

#19 Unicode String Literals

As mentioned in #14, C++11 defines 3 constant string prefixes:

  • u8 stands for UTF-8 encoding
  • u stands for UTF-16 encoding
  • U stands for UTF-32 encoding

In addition, C++11 also introduces a string prefix R to indicate " native string literal ". The so-called "native string literal" means that the string does not need to process special characters through escaping. , what you see is what you get:

// ⽤法: R"分隔符 (原始字符 )分隔符"
string path = R"(D:\workspace\vscode\java_demo)";

// - 作为分隔符,
// 因为原始字符串含有 )",如果不添加 - 作为分隔符,则会导致字符串错误标示结束位置
// 分隔符应该尽量使用原始字符串中未出现的字符,以便正确标示开始与结尾
string path2 = R"-(a\b\c)"\daaa\e)-";

#20 User Defined Literals

User-defined literals are literals that support user-defined types.

Traditional C++ provides various literals, for example, "12.5" is a double literal. "12.5f" is a float type literal. These literals are defined and specified in the C++ standard, and programs and users cannot define new literal types or suffixes .

C++11 introduces the ability of user-defined literals. It is mainly realized by defining "literal operator function" or function template. The operator name is preceded by a pair of adjacent double quotes. Literal operators are usually invoked implicitly where user-defined literals are used. For example:

struct S {
  int value;
};

// 用户定义字面量运算符的实现
S operator ""_mysuffix(unsigned long long v) {
  S s_;
  S_.value = (int) v;
  return s_;
}

// 使用
S sv;
// 101 为类型为 S 的字面量
// _mysuffix 是我们自定义的后缀,如同 float 的 f 一般
sv = 101_mysuffix;

User-defined literals usually consist of the following types:

  1. Numeric literals
    1.1 Integer literals
    1.2 Floating point literals
OutputType operator "" _suffix(unsigned long long);
OutputType operator "" _suffix(long double);
 
// Uses the 'unsigned long long' overload.
OutputType some_variable = 1234_suffix;
// Uses the 'long double' overload.
OutputType another_variable = 3.1416_suffix; 
  1. string literal
OutputType operator "" _ssuffix(const char     * string_values, size_t num_chars);
OutputType operator "" _ssuffix(const wchar_t  * string_values, size_t num_chars);
OutputType operator "" _ssuffix(const char16_t * string_values, size_t num_chars);
OutputType operator "" _ssuffix(const char32_t * string_values, size_t num_chars);

// Uses the 'const char *' overload.
OutputType some_variable =   "1234"_ssuffix; 
// Uses the 'const char *' overload.
OutputType some_variable = u8"1234"_ssuffix;
// Uses the 'const wchar_t *'  overload. 
OutputType some_variable =  L"1234"_ssuffix; 
// Uses the 'const char16_t *' overload.
OutputType some_variable =  u"1234"_ssuffix; 
// Uses the 'const char32_t *' overload.
OutputType some_variable =  U"1234"_ssuffix; 
  1. character literal
S operator "" _mysuffix(char value) {
  const char cv[] {value,'\0'};
  S sv_ (cv);
  return sv_;
}

S cv {'h'_mysuffix};

All the bells and whistles

#21 Properties

C++11 introduced so-called " attributes " to allow programmers to provide additional information in the code, such as:

// f() 永不返回
void f [[ noreturn ]] () {
  throw "error";  // 虽然不能返回,但可以抛出异常
}

The above example shows the basic form of the attribute, and noreturn means that the function will never return.

C++11 introduces two attributes:

Attributes Version retouch target effect
noreturn C++11 function Indicates that the function does not return, there is no return statement, and the execution is not completed normally, but it can exit through an exception or the exit() function
carries_dependency C++11 function, variable Indicates that the dependency chain in the release consumption std::memory_order is passed in and out of this function

The concept and function are somewhat similar to annotations in Java

#22 Lambda expressions

Lambda expression basic syntax:

// [捕获列表]:捕获外部变量,详见下文
// (参数列表): 函数参数列表
// mutable: 是否可以修改值捕获的外部变量
// 异常属性:exception 异常声明
[捕获列表](参数列表) mutable( 可选 ) 异常属性 -> 返回类型 {
  // 函数体
}

For example:

bool cmp(int a, int b) {
  return a < b;
}

int main() {
  int x = 0;
  // 传统做法
  sort(vec.begin(), vec.end(), cmp);

  // 使用 lambda
  sort(vec.begin(), vec.end(), [x](int a, int b) -> bool { return a < b; });
  return 0;
}

The "capture list" in a lambda expression allows the lambda expression to use external variables of its visible scope internally, such as in the above example x. Capture lists generally have the following types:
1. Value capture
Similar to value passing in parameter passing, the captured variable is passed in as a value copy:

int a = 1;
auto f1 = [a] () { a+= 1; cout << a << endl;};

a = 3;
f1();

cout << a << endl;

2. Reference capture
Add & symbol to capture external variables by reference:

int a = 1;
// 引用捕获
auto f2 = [&a] () { cout << a << endl; };

a = 3;
f2();

3. Implicit capture
There is no need to explicitly list all external variables that need to be captured, [=]all external variables can be captured by "value capture", and [&]all external variables can be captured by "reference capture":

int a = 1;
auto f3 = [=] { cout << a << endl; };    // 值捕获
f3(); // 输出:1

auto f4 = [&] { cout << a << endl; };    // 引用捕获
a = 2;
f4(); // 输出:2

4. Mixed way The mixed way
above [=, &x]means that the variable x is captured by reference, and other variables are captured by value.

The final summary of lambda capturing external variables is shown in the following table:

capture form illustrate
[] does not capture any external variables
[variable name, …] By default, the specified multiple external variables are captured in the form of values ​​(separated by commas). If they are captured by reference, the declaration needs to be displayed (using the & specifier)
[this] captures the this pointer as a value
[=] captures all external variables as values
[&] Capture all external variables by reference
[=, &x] The variable x is captured by reference, and the rest are captured by value
[&, x] The variable x is captured as a value and the rest of the variables are captured as references

#23 noexcept specifier and noexcept operator

C++11 simplifies the declaration of exceptions to the following two cases:

  1. function may throw any exception
void func(); // 可能抛出异常
  1. It is not possible for the function to throw any exceptions
void func() noexcept; // 不可能抛出异常

Using noexcept allows the compiler to better optimize the code, and at the same time, if the function modified by noexceptstd::terminate() throws an exception , the call will immediately terminate the program.

noexcept can also be used as an operator to determine whether an expression produces an exception:

cout << noexcept(func()) << endl;

#24 alignof and alignas

C++11 introduces alignof and alignas to control memory alignment.

alignof : can get the alignment
alignas : the alignment of the custom structure:

struct A {
  char a;
  int b;
};

struct alignas(std::max_align_t) B {
  char a;
  int b;
  float c;
};

cout << alignof(A) << endl;
cout << alignof(B) << endl;

#25 Multi-threaded memory model

See the section called " atomic and memory_order in C++ " in Jump table implementation in LevelDB .

#26 Thread-local storage

In a multi-threaded program, global and static variables are shared by multiple threads, which is expected and required in some scenarios.

But in other scenarios, we hope to have thread-level variables, which are exclusive to the thread and will not be affected by other threads. We call it thread local storage (TLS, thread local storage) .

C++11 introduces thread_local to declare thread-local storage, as follows:

int thread_local num;

#27 GC interface

As we all know, C++ is a language with explicit heap memory management, and programmers need to pay attention to the allocation and destruction of memory space at all times. And if the programmer does not manage the heap memory correctly, it will cause program exceptions, errors, crashes, etc. From the language level, these incorrect memory management mainly include:

  • Wild pointer : the memory has been destroyed, but the pointer to it is still in use
  • Repeated release : release the memory that has been released, or release the memory that has been reallocated, resulting in repeated release errors
  • Memory leak : The memory space that is no longer needed in the program is not released in time, resulting in unnecessary memory consumption as the program continues to run

Explicit memory management can provide programmers with great programming flexibility, but it also increases the probability of errors. To this end, C++11 further transforms smart pointers, and also provides a "minimum garbage collection" standard.

At present, many modern languages ​​fully support "garbage collection", such as Java, Python, C#, Ruby, PHP, etc. all support "garbage collection". In order to realize "garbage collection", the most important point is to judge when objects or memory can be recycled . The main methods for judging whether an object or memory is recyclable are:

  1. reference count
  2. Trace processing (trace object diagram) . Such as " object reachability analysis " in Java .

After it is determined that the object or memory can be recycled, it needs to be recycled, and there are different recycling strategies and recycling algorithms (simple description):

  1. Mark-clear
    The first step is to mark whether the object and memory can be recycled, and the second step is to recycle the marked memory. Obviously this approach will lead to a lot of memory fragmentation
  2. Marking-Organizing
    The first step is also marking. But the second step is not to clean up directly, but to align the "live objects" to the left (sorting). However, moving a large number of objects will cause the references in the program to be updated. If more objects die, more moving operations are required. Therefore, it is suitable for "longevity" objects.
  3. copy algorithm . Divide the heap space into two parts: from and to . After the from space is full, scan the mark to find out the living objects, copy them to the to space, and clear the from space. Afterwards, the original to becomes the from space for the program to allocate memory, and the original from becomes the to, waiting for the next garbage collection to accommodate those "survivors". If there are a large number of survivors, then the copy will cause a large performance consumption. Therefore, it is suitable for short-lived objects who "live in the morning and die in the evening ".

In the implementation, the "generational collection" algorithm is usually used, that is, the heap space is divided into "new generation " and "old generation". ” or “Tag Organize”.

The above introduces the relevant algorithms of garbage collection, and C++11 has formulated the standard of "minimum garbage collection". The so-called "minimum" actually means that it is not a complete GC at all, but for subsequent GC preparations currently only provide some library functions to assist GC, such as:
declare_reachable (declare that an object cannot be recycled), undeclare_reachable (declare that an object can be recycled).

Since pointers in C++ are very flexible, this flexibility will cause GC to misjudgment and reclaim memory, so these two function protection objects are provided:

int* p1 = new int(1);
p1 += 10;             // 将导致 GC 回收内存空间
p1 -= 10;             // 指针的灵活性:又移动回来了
*p1 = 10;             // 内存已被回收,导致程序错误

// 使用 declare_reachable 保护对象不被 GC
int* p2 = new int(2);
declare_reachable(p2); // p2 不可回收

p2 += 10;              // GC 不会回收
p2 -= 10;

*p2 = 10;              // 程序正常

It can be seen from the above that these two functions are designed to be compatible with the upcoming [4] C++ GC for old programs.

[4] Looks like it won't be coming.

The above has introduced so much, and finally let me introduce the most embarrassing point: no compiler has yet implemented the C++11 standard about GC .

This GC feature can be ignored temporarily. In fact, many features of C++ can be ignored

#28 range for

Similar to the foreach loop in Java

std::vector<int> vec = {1, 2, 3, 4};
for (auto element : vec) {
  std::cout << element << std::endl;  // read only
}

#29 static_assert

We often use assert, which is a runtime assertion. But many things should not be judged and checked at runtime, but should be strictly asserted at compile time, such as the length of the array.

C++11 introduces static_assert to implement compile-time assertions:

static_assert(sizeof(void *) == 4,"64位系统不支持");

#30 Smart Pointers

C++98 provides a template type "auto_ptr" to implement smart pointers. auto_ptr manages the allocated memory as an object, and releases the memory at an appropriate time. Programmers only need to use the pointer returned by the new operation as the initial value of auto_ptr, as shown below:

auto_ptr(new int);

However, auto_ptr has defects such as "setting the original pointer to NULL when copying", so C++11 introduces three smart pointers: unique_ptr, shared_ptr, and weak_ptr .

  • unique_ptr : unique_ptr is tightly bound to the memory space of the specified object, and it is not allowed to share the same object memory with other unique_ptr pointers. That is, memory ownership is unique at the same time , but ownership can be transferred through the move and move semantics mentioned in #05. As shown below:
unique_ptr<int> p1(new int(111));

unique_ptr<int> p2 = p1;        // ⾮法,不可共享内存所有权
unique_ptr<int> p3 = move(p1);  // 合法,移交所有权。p1 将丧失所有权

p3.reset();                     // 显式释放内存
  • shared_ptr : Compared with unique_ptr, memory ownership can be shared, that is, multiple shared_ptr can point to the memory of the same object. At the same time, shared_ptr uses the reference counting method to determine whether the memory is still needed, so as to determine whether it needs to be recycled.
shared_ptr<int> p4(new int(222));
shared_ptr<int> p5 = p4;  // 合法

p4.reset();               // 「释放」内存

// 由于采⽤引⽤计数法,p4.reset() 仅仅使得引⽤数减⼀
// 所指向的内存由于仍有 p5 所指向,所以不会被回收
// 访问 *p5 是合法且有效的
cout << *p5 << endl;      // 输出 222
  • weak_ptr : weak_ptr can point to the memory pointed to by shared_ptr, and if necessary, the member lock can be used to return a shared_ptr pointer pointing to the current memory. If the current memory has been released, then lock() will return nullptr. Another important point is that weak_ptr does not participate in reference counting . Like a "virtual pointer", it points to the object memory pointed to by shared_ptr. On the one hand, it does not hinder the release of memory. On the other hand, weak_ptr can be used to determine whether the memory is valid and has been released:
shared_ptr<int> p6(new int(333));
shared_ptr<int> p7 = p6;
weak_ptr<int> weak_p8 = p7;

shared_ptr<int> p9_from_weak_p8 = weak_p8.lock();

if (p9_from_weak_p8 != nullptr) {
  cout << "内存有效" << endl;
} else {
  cout << "内存已被释放" << endl;
}

p6.reset();
p7.reset(); // weak_p8

// 内存已被释放,即使 weak_p8 还「指向」该内存

weak_ptr also has a very important application and is to solve the "circular reference" problem caused by the shared_ptr reference counting method. The so-called "circular reference" is shown in the following figure:

Since the member variables of ObjA and ObjB refer to each other, even if the references of P1 and P2 are removed, the reference counts of these two objects are still not 0. But in fact, the two objects are no longer accessible and should be recycled.

Using weak_ptr to achieve mutual reference between the above two objects can solve this problem, as shown in the following figure:

Remove the references of P1 and P2. At this time, ObjA and ObjB internally refer to each other through weak_ptr. Since weak_ptr does not participate in reference counting , the reference counts of ObjA and ObjB are judged to be 0, and ObjA and ObjB will be recycled correctly.

C++14 new features

#01 Variable template

We already have class templates, function templates, and now C++14 brings us variable templates:

template<class T>
constexpr T pi = T(3.1415926535897932385);

int main() {
  cout << pi<int> << endl;

  cout << pi<float> << endl;

  cout << pi<double> << endl;

  return 0;
}

// 当然在以前也可以通过函数模板来模拟
// 函数模板
template<class T>
constexpr T pi_fn() {
  return T(3.1415926535897932385);
}

#02 Generic lambdas

The so-called "generic lambda" is a lambda that uses the auto type specifier in the formal parameter declaration. For example:

auto lambda = [](auto x, auto y) { return x + y; };

#03 lambda initialization capture

C++11 lambda has provided us with value capture and reference capture, but they are actually for lvalues, and rvalue objects cannot be captured . This problem has been solved in C++14:

int a = 1;
auto lambda1 = [value = 1 + a] {return value;};

std::unique_ptr ptr(new int(10));

// 移动捕获
auto lambda2 = [value = std::move(ptr)] {return *value;};

#04 new/delete elision

I don't know how to translate it well, new/delete eliminated? new/delete omitted? This is listed
in cppreference c++14 , but not elaborated.

Since C++14 newly provides the make_unique function, unique_ptr can be automatically deleted when it is destructed, and together with make_shared and shared_ptr, it can basically cover most scenarios and requirements. So starting from C++14, the use of new/delete should be greatly reduced.

#05 Relaxed restrictions on constexpr functions

It has been mentioned in #08 of C++11 that constexpr-modified functions can only contain one return statement in addition to using directives, typedef statements, and static_assert assertions.

However, C++14 releases this restriction. The function modified by constexpr can contain conditional statements such as if/switch, and can also contain for loops .

#06 Binary literals

Numbers in C++14 can be expressed in binary form, prefixed with 0bor 0B.

int a = 0b101010; // C++14

#07 Number Separator

Use single quotes 'to improve readability of numbers:

auto integer_literal = 100'0000;

Major features such as GC, modules, and coroutines are submissive, and dispensable features C++ strikes hard!

#08 Function return type deduction

As mentioned above, the use of auto/decltype in C++11 with the trailing return value realizes the derivation of the return value of the function. C++14 implements an auto and automatically deduces the return value type:

auto Func(); // 返回类型由编译器推断

#09 Aggregate classes with default member initializers

C++11 adds a default member initializer. If the constructor does not initialize a member and the member has a default member initializer, the default member initializer will be used to initialize the member.

In C++11, the definition of an aggregate type is changed to "explicitly exclude any type with a default member initializer".

Therefore, in C++11 aggregate initialization is not allowed if a class has a default member initializer. C++14 relaxes this restriction:

struct CXX14_aggregate {
  int x;
  int y = 42;  // 带有默认成员初始化器
};

// C++11 中不允许
// 但 C++14允许 且 a.y 将被初始化为42
CXX14_aggregate a = { 1 }; 

#10 decltype(auto)

Allow auto type declarations to use the rules of decltype. That is, it is allowed to use decltype's inference rules for a given expression without explicitly specifying an expression as an argument to decltype.
—— From Wikipedia C++14

See an example:

// 在另一个函数中对下面两个函数进行转发调用
std::string  lookup1();
std::string& lookup2();

// 在 C++11 中,需要这么实现
std::string look_up_a_string_1() {
    returnlookup1();
}
std::string& look_up_a_string_2() {
    returnlookup2();
}

// 在 C++14 中,可以通过 decltype(auto) 实现
decltype(auto) look_up_a_string_1() {
    return lookup1();
}
decltype(auto) look_up_a_string_2() {
    return lookup2();
}

C++17 new features

#01 Fold expressions

As mentioned above, "variable parameter templates" were introduced in C++11 (C++11 #16). It is cumbersome to expand variable-length parameters in C++11, and usually uses a recursive function to expand:

void print() {  // 递归终止函数
   cout << "last" << endl;
}

template <class T, class ...Args>
void print(T head, Args... rest) {
   cout << "parameter " << head << endl;
   print(rest...); // 递归展开 rest 变长参数
}

C++17 introduces "folding expressions" to further support the expansion of variable-length parameters:

// ⼀元左折叠
// 只有一个操作符 「-」,且展开符 ... 位于参数包 args 的左侧,固为一元左折叠
template<typename... Args>
auto sub_val_left(Args&&... args) {
  return (... - args);
}

auto t = sub_val_left(2, 3, 4);   // ((2 - 3) - 4) = -5;

// 一元右折叠
// 只有一个操作符 「-」,且展开符 ... 位于参数包 args 的右侧,固为一元右折叠
template<typename... Args>
auto sub_val_right(Args&&... args) {
  return (args - ...);
}
auto t = sub_val_right(2, 3, 4);  // (2 - (3 - 4)) = 3;

// 二元左折叠
// 左右有两个操作符 ,且展开符 ... 位于参数包 args 的左侧,固为二元左折叠
template<typename... Args>
auto sub_one_left(Args&&... args) {
  return (1 - ... - args);
}
auto t  = sub_one_left(2, 3, 4);  // ((1 - 2) - 3) - 4 = -8

// 二元右折叠
// 左右有两个操作符,且展开符 ... 位于参数包 args 的右侧,固为二元右折叠
template<typename... Args>
auto sub_one_right(Args&&... args) {
  return (args - ... - 1);        
}
auto t  = sub_one_right(2, 3, 4); //  2 - (3 - (4 - 1)) = 2

#02 Class Template Argument Deduction

Class templates before C++17 cannot perform parameter deduction:

std::pair<int, string> a{ 1, "a"s }; // 需要指明 int, string 类型

C++17 implements argument type deduction for class templates:

std::pair a{ 1, "a"s }; // C++17,类模板可自行推导实参类型

#03 auto placeholder non-type template parameter

template<auto n> struct B { /* ... */ }

B<5> b1;    // OK: 非类型模板形参类型为 int
B<'a'> b2;  // OK: 非类型模板形参类型为 char
B<2.5> b3;  // 错误(C++20前):非类型模板形参类型不能是 double

#04 constexpr if statement at compile time

C++17 introduces the keyword constexpr into the if statement, allowing the judgment condition of the constant expression to be declared in the code

template<typename T>
auto print_info(const T& t) {
  if constexpr (std::is_integral<T>::value) {
    return t + 1;
  } else {
    return t + 1.1;
  }
}

The above code will judge the if statement during compilation , so as to select one of the branches during compilation .

#05 Inline variables (inline variables)

See an example:

// student.h
extern int age;  // 全局变量

struct  Student {
   static int age;  // 静态成员变量
};

// student.cpp
int age = 18;
int Student::foo = 18;

Before C++17, if you want to use a global variable or a static member variable of a class, you need to declare it in the header file and then define it in each cpp file.

C++17 supports declaring inline variables to the same effect:

// student.h
inline int age = 18;

struct Student {
   static inline int age = 18;
};

#06 Structured Binding

Similar to destructuring assignment in JavaScript

Illustrative example:

tuple<int, double, string> f() {
  return make_tuple(1, 2.3, "456");
}

int main() {
  int arr[2] = {1,2};
  // 创建 e[2]
  // 复制 arr 到 e, 然后 a1 指代 e[0], b1 指代 e[1]
  auto [a1, b1] = arr;
  cout << a1 << ", " << b1 << endl;

  // a2 指代 arr[0], b2 指代 arr[1]
  auto& [a2, b2] = arr;
  cout << a2 << "," << b2<< endl;

  // 结构化绑定 tuple
  auto [x, y, z] = f();
  cout << x << ", " << y << ", " << z << endl;

  return 0;
}

#07 Variable initialization for if/switch statements

The if/switch statement declares and initializes variables in the form: if (init; condition) and switch (init; condition). Example:

for (int i = 0; i < 10; i++) {
  // int count = 5; 这条初始化语句直接写在 if 语句中
  if (int count = 5; i > count) {
    cout << i << endl;
  }
}

// char c(getchar()); 这条初始化语句直接写在 switch 语句中
switch (char c(getchar()); c) {
  case 'a': left(); break;
  case 'd': right(); break;
  default: break;
}

#08 u8-char

Character prefix:

u8'c-字符' // UTF-8 字符字面量

Note that it is different from the "string prefix" above. The u8 introduced by C++11 is a string prefix, and C++17 supplements u8 as a character prefix.

#09 Simplified nested namespaces

namespace X { namespace Y { … }}  // 传统
namespace X::Y { … }              // C++17 简化命名空间

#10 using statement can declare multiple names

struct A {
    void f(int) {cout << "A::f(int)" << endl;}
};
struct B {
    void f(double) {cout << "B::f(double)" << endl;}
};
struct S : A, B {
    using A::f, B::f; // C++17
};

#11 Make noexcept part of the type system

Like the return type, the exception specification becomes part of the function type, but not the function signature :

// 下面函数是不同类型函数,但拥有相同的函数签名
void g() noexcept(false);
void g() noexcept(true);

#12 New order of evaluation rules

Before C++17, in order to satisfy each compiler to optimize correspondingly on different platforms, C++ did not strictly stipulate some evaluation order. The most typical examples are as follows:

cout << i << i++;  // C++17 之前,未定义行为
a[i] = i++;              // C++17 之前,未定义行为
f(++i, ++i);           // C++17 之前,未定义行为

Specifically, C++17 specifies the following order of evaluation:

  • a.b
  • a->b
  • a->*b
  • a(b1, b2, b3)
  • b @= a
  • a[b]
  • a << b
  • a >> b

The ordering rules are: the evaluation of a and all side effects are ordered before b, but the order of the same letter is indeterminate

#13 Guaranteed copy elision

C++17 introduces "forced copy elision" to ensure that objects are not copied if certain conditions are met .

Before C++11, the so-called copy elimination technology (copy elision), that is, the compiler's return value optimization RVO/NRVO, already existed.

RVO(return value optimization): return value optimization
NRVO(named return value optimization): named return value optimization

See the example below:

T Func() {
  return T();
}

Under the traditional copy elimination (copy elision) rules, the above code will generate a temporary object and copy it to the "return value". This process may be optimized away, that is, the copy/move functions are not called at all. But the program must still provide the corresponding copy function.

Look at the following code again:

T t = Func();

The above code will copy the return value to t. This copy operation may still be optimized, but in the same way, the program still needs to provide the corresponding copy function.

From the above, we can see that the following code is illegal under the traditional copy elimination rules:

// 传统的复制消除即使优化了拷贝函数的调用
// 但还是会检查是否定义了拷贝函数等
struct T {
    T() noexcept = default;
    T(const T&) = delete; // C++11 中如果不提供相应的拷贝函数将会导致 return 与 赋值错误
    T(T&&) = delete;
};

T Func() {
  return T();
}

int main() {
  T t = Func();
}

And "forced copy elimination" for the prvalue [5] will actually eliminate the above copy process [6] , and will not check whether the copy/move function is provided, so the above code is legal in C++17 .

[5] Before C++17, prvalues ​​were temporary objects, and C++17 expanded the definition of prvalues: expressions that can generate temporary objects but have not yet generated temporary objects, such as the code in the above example Func() in
[6] eliminates the principle: when the condition of "purrvalue assignment to glvalue" is met, T t = Func(); will be optimized to be similar to T t = T(); No temporary objects are created in between.

But on the other hand, for "named temporary objects", no "forced copy elimination" will be performed:

T Func() {
   T t = ...;
   ...
   return t;
}

T still has to provide copy/move functions, so C++17 has no changes to NRVO (named return value optimization).

Regarding mandatory copy elimination, you can refer to the first answer in the link below, the answer is very clear:
How does guaranteed copy elision work?

Does all this come from the original design problem of C++: the default overloading of the = operator, which gives the = operator object copy semantics.

#14 lambda expressions capture *this

#include <iostream>
 
struct Baz {
  auto foo() {
    // 通过 this 捕获对象,之后在 lambda 即可访问对象的成员变量 s
    return[this]{ std::cout << s << std::endl; };
  }
 
  std::string s;
};
 
int main() {
  auto f1 = Baz{ "ala" }.foo();
  auto f2 = Baz{ "ula" }.foo();
  f1();
  f2();
}

But there is a flaw in the above code: what is captured is the current object, if the lambda expression's access to the member variable exceeds the lifetime of the current object, it will cause problems.

C++17 provides the ability to capture a copy*this of the current object :

auto foo() {
  return[*this]{ std::cout << s << std::endl; };
}

#15 constexpr lambda expressions

C++17's lambda declarations are of type constexpr, and such lambda expressions can be used in other contexts that require a constexpr type.

int y = 32;

auto func = [y]() constexpr {
  int x = 10;

  return y + x;
};

#16 Attribute namespaces don’t have to be repeated

The concept of attributes has been introduced in C++11 #21 above, and may have namespaces for non-standard attributes with implementation-defined behavior:

[[gnu::always_inline, gnu::const, gnu::hot, nodiscard]]
inline int f(); // 声明 f 带四个属性

[[gnu::always_inline, gnu::const, gnu::hot, nodiscard]]
int f(); // 同上,但使用含有四个属性的单个属性说明符

The namespaces of the above attributes in C++11 need to be declared repeatedly, and C++17 simplifies the definition of attribute namespaces:

[[using gnu : const, always_inline, hot]] [[nodiscard]]
int f[[gnu::always_inline]](); // 属性可出现于多个说明符中

#17 New attributes [[fallthrough]] [[nodiscard]] and [[maybe_unused]]

C++11 only comes with two standard attributes, and C++17 continues to expand several standard attributes.

fallthrough

// 以下代码因为没有 case 中没有 break;
// 所以将会发生 case 穿透
// 编译时编译器将会发出警告
int x = 2;
switch (x) {
  case 2:
    result++;
  case 0:
    result++;
  default:
    result++;
}

// 有时候我们需要 case 穿透,如匹配到 2 就一直执行后续的 case
// 此时可以使用属性 [[fallthrough]],使用后,编译器将不会发出警告
switch (x) {
    case 2:
      result++;
      [[fallthrough]];  // Added
    case 0:
      result++;
      [[fallthrough]];  // Added
    default:
      result++;
  }

Nodiscard
often needs to check the return value of the function during the development process. This step is necessary in many business scenarios, for example:

// 许多人会遗漏对返回值进行检查的步骤
// 导致了很多业务层面潜在的缺陷
if (CallService() != ret) {
  // ... 
}

// C++17 引入 [[nodiscard]] 属性来「提醒」调用者检查函数的返回值
[[nodiscard]] int CallService() {
  return CallServiceRemote();
}

CallService();              // 如果只调用而不检查,编译器将发出警告
if (CallService() != ret) { // pass
  // ...
}

maybe_unused
If we compile the following code with -Wunused and -Wunused-parameter, the compiler may report a warning:

int test(int a, int b, int c) {
  int result = a + b;

#ifdef ENABLE_FEATURE_C
  result += c;
#endif
  return result;
}

The reason is that the compiler considers c to be an unused variable, but it is not. [[maybe_unused]] can be used in C++17 to suppress warnings for "unused entities":

int test(int a, int b, [[maybe_unused]] int c) {
  int result = a + b;

#ifdef ENABLE_FEATURE_C
  result += c;
#endif
  return result;
}

#18 __has_include

Indicates whether a header or source file with the specified name exists:

#if __has_include("has_include.h")
  #define NUM 1
#else
  #define NUM 0   
#endif

New features in C++20

#01 Feature Test Macro

A set of preprocessor macros is defined for features of the C++ language and libraries introduced in C++11 and later. Making this an easy and portable way of detecting the presence of these features. For example:

__has_cpp_attribute(fallthrough)     // 判断是否支持 fallthrough 属性
#ifdef __cpp_binary_literals              // 检查「二进制字面量」特性是否存在 
#ifdef __cpp_char8_t                          // char8 t
#ifdef __cpp_coroutines                     // 协程
// ...

#02 Three-way comparison operator <=>

// 若 lhs < rhs 则 (a <=> b) < 0
// 若 lhs > rhs 则 (a <=> b) > 0
// 而若 lhs 和 rhs 相等/等价则 (a <=> b) == 0

lhs <=> rhs

#04 Initialization statements and initializers in scoped for

C++17 introduces the initialization statement of if/switch, and C++20 introduces the initialization of the range for:

// 将 auto list = getList(); 初始化语句直接放在了范围 for 语句中
for (auto list = getList(); auto& ele : list) {
    // ele = ....
}

In addition, the scope of C++20 for can also support certain functional programming styles, such as introducing the pipe symbol | to realize function composition:

// 范围库
auto even = [](int i){ return 0 == i % 2; };
auto square = [](int i) { return i * i; };
// ints 输出到 std::view::filter(even) ,处理后得到所有偶数
// 上一个结果输出到 std::view::transform(square),将所有偶数求平方
// 循环遍历所有偶数的平方
for (int i : ints | std::view::filter(even) | 
                      std::view::transform(square)) {
 // ...
}

#05 char8_t

C++20 newly added char8_t type.

char8_t is used to represent UTF-8 characters and is required to be large enough to represent any UTF-8 code unit (8 bits).

#06 [[no_unique_address]]

[[no_unique_address]] attribute modified data members can be optimized to not take up space :

struct Empty {}; // 空类
struct X {
  int i;
  Empty e;
};
struct Y {
  int i;
  [[no_unique_address]] Empty e;
};
struct Z {
  char c;
  [[no_unique_address]] Empty e1, e2;
};
struct W {
  char c[2];
  [[no_unique_address]] Empty e1, e2;
};

int main() {
  // 任何空类类型对象的大小至少为 1
  static_assert(sizeof(Empty) >= 1);

  // 至少需要多一个字节以给 e 唯一地址
  static_assert(sizeof(X) >= sizeof(int) + 1);

  // 优化掉空成员
  std::cout << "sizeof(Y) == sizeof(int) is " << std::boolalpha << (sizeof(Y) == sizeof(int)) << '\n';

  // e1 与 e2 不能共享同一地址,因为它们拥有相同类型,尽管它们标记有 [[no_unique_address]]。
  // 然而,其中一者可以与 c 共享地址。
  static_assert(sizeof(Z) >= 2);

  // e1 与 e2 不能拥有同一地址,但它们之一能与 c[0] 共享,而另一者与 c[1] 共享
  std::cout << "sizeof(W) == 2 is " << (sizeof(W) == 2) << '\n';
}

#07 [[likely]]

The [[likely]] attribute is used to tell the compiler which branch is more likely to be executed, thereby helping the compiler optimize code compilation

if (a > b) [[likely]] {
  // ...
}

The first intuition is really a wonderful feature. I am curious to what extent it can be optimized so that language features are specially added to require programmers to cooperate with this optimization,
including the header files below. It makes me think that C++ is often not a compiler serving programmers, but It is the programmer who serves the compiler

#08 [[unlikely]]

Corresponds to [[likely]]:

if (a>b) [[unlikely]] {
  // ...
}

#09 Packet expansion in lambda initialization capture

Before C++20, lambda expressions and pack expansion cannot be initialized and captured. If you want to initialize and capture pack expansion, you need to implement make_tuple and apply, as follows:

template <class... Args>
auto delay_invoke_foo(Args... args) {
    // 对 args 进行 make_tuple,然后再用 apply 恢复
    return [tup=std::make_tuple(std::move(args)...)]() -> decltype(auto) {
        return std::apply([](auto const&... args) -> decltype(auto) {
            return foo(args...);
        }, tup);
    };
}

C++20 will directly support lambdas for initialization capture of pack expansions, as follows:

template <class... Args>
auto delay_invoke_foo(Args... args) {
    // 直接 ...args = xxxxx
    return [...args=std::move(args)]() -> decltype(auto) {
        return foo(args...);
        
    };
}

#10 Removed the requirement to use the typename keyword to disambiguate types in multiple contexts

Before P0634R3
C++20, typename was required to disambiguate where template types were used, as follows:

template<typename T>
typename std::vector<T>::iterator // std::vector<T>::iterator 之前必须使用 typename 关键字

C++20 allows typename to be omitted in some contexts, as follows:

template<typename T>
std::vector<T>::iterator // 省略 typename 关键字

#11 consteval、constinit

consteval
mentioned above that constexpr functions can be run at compile time or at run time. In order to clarify the scene and semantics more clearly, C++20 provides a consteval that can only be executed at compile time. If the value returned by a function modified by consteval cannot be determined by the compiler, the compilation will fail.

constinit
In C++, there are usually two situations for the initialization of variables with static storage duration:

  • initialized at compile time
  • Initialized when declared on first load

The second case has hidden risks due to the initialization order of static variables.

So C++20 provides constinit so that certain variables that should be initialized at compile time are guaranteed to be initialized at compile time.

#12 More relaxed constexpr requirements

From C++11 to C++20, constexpr has been "patched", can't it expand its capabilities at one time?

Quoting from C++20 New Features
The ability to constexpr extensions in C++20:

  • constexpr virtual function
    • A constexpr virtual function can override a non-constexpr virtual function
    • Non-constexpr virtual functions will overload constexpr virtual functions
  • constexpr functions support:
    • Using dynamic_cast() and typeid
    • dynamic memory allocation
    • Change the value of a union member
    • contains try/catch
      • But the throw statement is not allowed
      • try/catch does not work when triggering constant evaluation
      • Need to enable constexpr std::vector
  • constexpr supports string & vector types

#13 specifies that signed integers are implemented in two's complement

Before C++20, the implementation of signed integers was not clearly specified in the standard form (although the two's complement was basically used in the implementation). C++20 explicitly stipulates that signed integers are implemented using two's complement.

#14 Aggregate initialization with parentheses

C++20 introduces some new forms of aggregate initialization, as follows:

T object = { .designator = arg1 , .designator { arg2 } ... };  //(since C++20)
T object { .designator = arg1 , .designator { arg2 } ... };     // (since C++20)
T object (arg1, arg2, ...);                                                           // (since C++20)

Among them, the third form that has not been used before is: T object (arg1, arg2, ...), using parentheses for initialization.

#15 Coroutines

Process : The basic unit of operating system resource allocation. Scheduling involves switching between user space and kernel space, which consumes a lot of resources.
Thread : The basic unit of operating system operation. Under the framework of the same process resource, preemptive multitasking is realized, and compared with the process, the resource consumption of execution unit switching is reduced.
Coroutines : Very similar to threads. But change a way of thinking to realize cooperative multitasking, and the user can realize cooperative scheduling (voluntarily hand over the control right)

Donald Knuth:

A subroutine is a special case of a coroutine

Coroutine is a generalized function (subroutine), but its process is switched and controlled by the user to a certain extent.

As an example:

# 协程实现的生产者和消费者
def consumer():
  r = ''
  while True:
    n = yield r
    if not n:
      return
    print('[CONSUMER] Consuming %s...' % n)
    time.sleep(1)
    r = '200 OK'

def produce(c):
  c.next()
  n = 0
  while n < 5:
    n = n + 1
    print('[PRODUCER] Producing %s...' % n)
    r = c.send(n)
    print('[PRODUCER] Consumer return: %s' % r)
  c.close()

if __name__=='__main__':
  c = consumer()
  produce(c)

The producer produces the message, and after the consumer completes the execution, it passes the yield control and switches back to the producer to continue production.

yield : Execute here to actively give up control, return a value, and wait for the previous context to further schedule itself

The above is the pure concept of coroutines , but many languages ​​have different implementations and packages for coroutines, which leads to the further expansion and extension of the concept of coroutines.

For example, Goroutines in golang is not a pure coroutine concept, but the encapsulation and implementation of coroutines and threads. It can be said that the execution unit scheduling in the user state solves the problem that traditional coroutines cannot take advantage of multi-core capabilities. defect. So many materials call it " lightweight thread " or " user state thread ".

In addition, coroutines have a special advantage when it comes to asynchronous programming:
expressing asynchronous logic through sequential execution that is more in line with human intuition .

In the JS ecosystem (especially represented by Node.js), we write asynchronous logic and often use callbacks to return results. And if it is a multi-level asynchronous call scenario, it is easy to fall into " callback hell callback hell ".

As follows:

fs.readFile(fileA, function (err, data) {
  fs.readFile(fileB, function (err, data) {
    // ...
  });
});

JS subsequently introduced Promise to simplify the form of callback calls, as follows:

readFile(fileA)
.then(function(data){
  console.log(data.toString());
})
.then(function(){
  return readFile(fileB);
})
.then(function(data){
  console.log(data.toString());
})
.catch(function(err) {
  console.log(err);
});

Later, an implementation of the coroutine was introduced - the Generator generator :

var fetch = require('node-fetch');

function* gen(){
  var url = 'https://api.github.com/users/github';
  var result = yield fetch(url);
  console.log(result.bio);
}

var g = gen();
var result = g.next();

result.value.then(function(data){
  return data.json();
}).then(function(data){
  g.next(data);
});

The Generator function can suspend execution (yield) and resume execution (next), which is the fundamental reason why it can be used to implement asynchronous programming

And the async & await asynchronous programming experience implemented by the underlying layer of JS through yield/generator will also make JS programmers intuitively feel the coroutine as a "callback scheduler".

C++20 introduces relatively pure coroutines, for example, a generator function or generator can be implemented:

experimental::generator<int> GetSequenceGenerator( 
    int startValue, 
    size_t numberOfValues) { 
    for (int i = 0 startValue; i < startValue + numberOfValues; ++i){ 
        time_t t = system_clock::to_time_t(system_clock::now()); 
        cout << std:: ctime(&t); co_yield i; 
    } 
} 
int main() {
    auto gen = GetSequenceGenerator(10, 5); 
    for (const auto& value : gen) { 
        cout << value << "(Press enter for next value)" << endl; 
        cin.ignore(); 
    } 
}

#16 module

Historical Baggage - Header Files

Please see the following code:

// person.cpp
int rest() {
  Play();
  return 0;
}

// game.cpp
int play() {
  LaunchSteam();
  return 0;
}
  1. Result files such as .obj may be from other languages ​​due to the C/C++ era. It is guaranteed that each source file is not associated with other source files and needs to be compiled independently. In this context, if we try to compile person.cpp from the perspective of the compiler, we will find that the compilation will not work. The reason is that Play's return type, parameter type and other meta information cannot be obtained. So is it possible to generate external symbols to wait for the linking phase?
  2. the answer is negative. i.e. cannot be deferred until the linking phase. The reason is that when compiling C++, the meta-information such as the return value and parameters of the function will not be compiled into the .obj and other results, so the meta-information related to the Play function cannot be obtained at the linking stage . The reason why the meta-information is not written into the compilation result like Java/C# and other modern languages ​​is that memory and other resources are scarce in the C/C++ era, so we try to save various resources.

Due to the above historical reasons, C++ finally handed over this inconvenience to programmers. Programmers need to declare the function prototype in advance when calling a function in another source file , and it is too low-level to repeat the declaration once in each source file that uses the corresponding function, so the so-called header file appears to simplify the declaration work .

On the other hand, header files play the role of interface description to a certain extent, but it is very far-fetched for some people to regard header files as the result of "the design idea of ​​separation of implementation and interface".

Header files are essentially a concept around the compilation period. Due to historical reasons, C/C++ has to be compiled by programmers using header files to assist the compiler.

The concept of interface revolves around the business development or programming stage, which is another level of matter.

If it is not easy to understand, you can think about it, how does the Java/C# language without header files realize the so-called "header file provides interface" function?

If it needs to be implemented, the compiler can directly extract the interface information from the source code file to generate an interface file, and it can also decide which ones should be exposed to the outside world and which ones should not be exposed according to the access rights. You can even use the .h suffix to make life easier for programmers who think " the header file acts as an interface ".

C++20 introduces modules, and one of the functions of modules is to unify and header.编译单元

// example 模块
export module example; //声明一个模块名字为example
export int add(int first, int second) { //可以导出的函数
  return first + second;
}

// 使用 example 模块
import example; //导入上述定义的模块
int main() {
  add(1, 2); //调用example模块中的函数
}

#17 Limitations and concepts

Concepts is one of the important updates of C++20, which is an extension of template capabilities. Before C++20, our template parameters were undefined, as follows:

template<class L, class T>
void find(const L& list, const T& t); // 从 list 列表中查找 t

The above parameter types L and T do not have any restrictions, but in fact there are implicit restrictions :

  • L should be an iterable
  • The elements in L should be of the same type as T
  • elements in L should be equality-comparable with type T

Programmers should be aware of the above implicit conditions, otherwise the compiler will output a bunch of errors. But now you can inform the compiler of the above restrictions through concepts, and you will get an intuitive error reason when you use it wrong.

For example, use concepts to limit parameters to hash:

// 定义概念
template<typename T>
concept Hashable = requires(T a) {
  // 下面语句的限定含义为:
  // 限定  std::hash(a) 返回值可转换成 std::size_t
  { std::hash<T>{}(a) } -> std::convertible_to<std::size_t>;
};

// 使用概念对模板参数进行限定
template<typename T>
auto my_hash(T) requires Hashable<T> {
  // ....
}

The above my_hashfunction can also be performed in a simplified way:

// 简化
template<Hashable T>
auto my_hash(T) {
  // ....
}

#18 Shortcut function templates

The usual form of declaring a function template is as follows:

template<class T> void f(T);
template<C1 T> void f2(T); // C1 如果是一个 concept 概念
// ...

C++20 can use autoor concept autoto achieve a shorter form of function template declaration:

void f1(auto);       // 等同于 template<class T> void f(T);
void f2(C1 auto); // template<C1 T> void f2(T);
// ...

#19 Array length derivation

C++20 will allow new int[]{1, 2, 3}the writing method, and the compiler can automatically deduce the length of the array.

summary

The most fundamental design concept of C++ is to serve for operational efficiency , and even specifically adding new features requires programmers to cooperate with compilers to optimize. But on the other hand, C++ has been borrowing features from languages ​​​​such as Java/JavaScript/Go/Python in the later stage, and many of them are irrelevant syntactic sugar, but the really important features have been delayed until 2020. The standard was not introduced.

When will C++ 20 really take root in the industry? As for the formation of a complete and unified ecology like other modern languages, it is even more distant

This leads to the already complicated syntax of C++ becoming more confusing over time, which further increases the cost of learning and using C++. The only benefit is that it increases the pride of some existing C++ programmers, after all, some programmers are proud of the difficulty of the tools they have mastered. These people not only link the "difficulty of the tool" with the "technical level", but sometimes even flaunt their IQ. It is recommended that people who have this idea read and recite the complete Xinhua Dictionary or use compilation to complete all the work .

C++ has its corresponding application scenarios. In the development of some basic components that require extremely high operating efficiency, in most game development scenarios, C++ is irreplaceable. However, the use of C++ in some upper-level application scenarios, especially in Internet services that are closer to users, is basically due to historical debt [7] .

[7] : For example, my current department. Even though a large number of components, tools and platforms have been accumulated around C++, many people think that the development efficiency is not low, but in fact there is at least 2 to 3 years behind the industry in terms of development efficiency [8], and the current It is foreseeable that this gap will only increase.

[8] : Except for games and basic component scenarios, here refers to those scenarios and businesses that should not use C++ but use C++ for historical reasons and are not determined to change now. Of course, this is only from a technical point of view. The actual situation may be that it is difficult to change the business, or the risk of change is greater than the benefit. But this should not be a reason for peace of mind. It is still an objective fact that development efficiency lags behind the industry.

related code

I have written some sample codes (updating) for the above features, please click here to view [9]

[9] : The file name is cpp version number_feature serial number_feature name

References

Modern C++ Tutorial: Quick Start C++11/14/17/20
cppreference
cppreference Chinese
C++17 STL Cook Book
Changes between C++14 and C++17
In-depth understanding of C++11 new feature analysis and application
C++ +20 - The next major version function is confirmed



Author: 401
Link: https://www.jianshu.com/p/8c4952e9edec
Source: Jianshu
The copyright belongs to the author. For commercial reprint, please contact the author for authorization, for non-commercial reprint, please indicate the source.

Guess you like

Origin blog.csdn.net/bodybo/article/details/124901297