龙空技术网

C++ 标准(from 11 to 20)主要新增特性

小智雅汇 1432

前言:

此时小伙伴们对“c语言0b1010”可能比较关切,我们都想要知道一些“c语言0b1010”的相关知识。那么小编也在网摘上收集了一些有关“c语言0b1010””的相关资讯,希望咱们能喜欢,同学们快快来了解一下吧!

C++ is an ISO standardized programing language. There are different C++ standards:

-    C++ 98-    C++ 03-    C++ 11    \    modern C++-    C++ 14    |-    C++ 17    |-    C++ 20    /

Everything starting with C++11 is referred to as “Modern C++”. These standards define the language in great technical detail. They also serve as manuals for C++ compiler writers. It is a mind-boggling set of rules and specifications. The C++ standards can be bought, or a draft version can be downloaded for free. These drafts closely resemble the final C++ standard. When C++ code can be successfully transferred and compiled on different platforms (machines or compilers), and when C++ implementation closely follows the standard, we say that the code is portable. This is often referred to as portable C++ .

The standards surrounded by braces represent the so-called “Modern C++.” Each standard describes the language and introduces new language and library features. It may also introduce changes to the existing rules. We will describe notable features in each of these standards.

标准和新特性

解释或描述

C++11

11.1 Automatic Type Deduction

auto

11.2 Range-based Loops

for(auto el:range)

11.​3 Initializer Lists

{}

11.​4 Move Semantics

move

11.​5 Lambda Expressions

[](){}

11.​6 The constexpr Specifier

constexpr

11.​7 Scoped Enumerators

enum class

11.​8 Smart Pointers

unique_ptr

11.​9 std:​:​unordered_​set

unordered_​set

11.​10 std:​:​unordered_​map

​unordered_​map

11.​11 std:​:​tuple

​pair<>->tuple<>

11.​12 static_​assert

constexpr

11.​13 Concurrency

thread(func)

11.​14 Deleted and Defaulted Functions

default, delete

11.​15 Type Aliases

using id = type

C++14

14.​1 Binary Literals

0b1010

14.​2 Digits Separators

123'456

14.​3 Auto for Functions

auto func();

14.​4 Generic Lambdas

auto lambdas

14.​5 std:​:​make_​unique

make_unique()

C++17

17.​1 Nested Namespaces

n::m::p declaration

17.​2 Constexpr Lambdas

Constexpr Lambdas

17.​3 Structured Bindings

auto[] = arr

17.​4 std:​:​filesystem

filesystem

17.​5 std:​:​string_​view

non-owning view

17.​6 std:​:​any

any c = 123;

17.​7 std:​:​variant

union->variant

C++20

20.​1 Modules

export and import

20.​2 Concepts

template type requires

20.​3 Lambda Templates

[]<>(){}

20.​4 [likely] and [unlikely] Attributes

if (choice) [[likely]]

20.​5 Ranges

ranges::sort(vec);

20.​6 Coroutines

co_await

20.​7 std:​:​span

span<int> is = vec;

20.​8 Mathematical Constants

numbers::log2e

1 C++11

C++11 is an ISO C++ standard, published in 2011. To compile for this standard, add the -std=c++11 flag to a command-line compilation string if compiling with g++ or clang. If using Visual Studio, choose Project / Options / Configuration Properties / C/C++ / Language / C++ Language Standard and choose C++11. New Visual Studio versions already support this standard out of the box.

11.1 Automatic Type Deduction

This standard introduces the auto keyword which deduces the type of the variable based on the variable’s initializer:

int main(){    auto mychar = 'A';    auto myint = 123 + 456;    auto mydouble = 456.789;}
11.2 Range-based Loops

The range-based loops allow us to iterate over the range, such as C++ standard-library containers:

#include <iostream>#include <vector>int main(){    std::vector<int> v = { 10, 20, 40, 5, -20, 75 };    for (auto el : v)    {        std::cout << el << '\n';    }}

The range-based for loop is of the following form: for (type element : container). This is read as for each element in a container (do something).

11.3 Initializer Lists

Initializer lists, represented by braces { } allow us to initialize objects in a uniform way. We can initialize single objects:

int main(){    int x{ 123 };    int y = { 456 };    double d{ 3.14 };}

And containers:

#include <vector>int main(){    std::vector<int> v = { 1, 2, 3, 4, 5 };}

List initialization also prevents narrowing conversions. If we tried to initialize our integer object with a double value inside the initializer list, the compilation would fail:

int main(){    int x = { 123.45 }; // Error, does not allowing narrowing}

When initializing our objects, we should prefer initializer lists {} to old-style parentheses ().

11.4 Move Semantics

C++ 11 standard introduces the move semantics for classes. We can initialize our objects by moving the data from other objects. This is achieved through move constructors and move assignment operators. Both accept the so-called rvalue reference as an argument. Lvalue is an expression that can be used on the left-hand side of the assignment operation. rvalues are expressions that can be used on the right-hand side of an assignment. The rvalue reference has the signature of some_type&&. To cast an expression to an rvalue reference, we use the std::move function. A simple move constructor and move assignment signature are:

class MyClass{public:    MyClass(MyClass&& otherobject) // move constructor    {        //implement the move logic here    }    MyClass& operator=(MyClass&& otherobject) // move assignment operator    {        // implement the copy logic here        return *this;    }};
11.5 Lambda Expressions

Lambda expressions are anonymous function objects. They allow us to write a short code snippet to be used as a standard-library function predicate. Lambdas have a capture list, marked by [ ] where we can capture local variables by reference or copy, parameter list with optional parameters marked with ( ), and a lambda body, marked with { }. An empty lambda looks like [] () {};. A simple example of counting only the even numbers in a set using the lambda as a predicate:

#include <iostream>#include <vector>#include <algorithm>int main(){    std::vector<int> v = { 1, 2, 3, 4, 5 };    auto counteven = std::count_if(std::begin(v), std::end(v),        [](int x) {return x % 2 == 0; }); // lambda expression    std::cout << "The number of even vector elements is: " << counteven;}
11.6 The constexpr Specifier

The constexpr specifier promises the variable or a function can be evaluated during compile-time. If the expression can not be evaluated during compile-time, the compiler emits an error:

int main(){    constexpr int n = 123;          //OK, 123 is a compile-time constant // expression    constexpr double d = 456.78;    //OK, 456.78 is a compile-time constant // expression    constexpr double d2 = d;        //OK, d is a constant expression    int x = 123;    constexpr int n2 = x;           //compile-time error                                    // the value of x is not known during // compile-time}
11.7 Scoped Enumerators

Enumerator使用时(右值)需显式声明类作用域。

The C++11 standard introduces the scoped enumerators . Unlike the old enumerators, the scoped enumerators do not leak their names into the surrounding scope. Scoped enums have the following signature: enum class Enumerator_Name {value1, value2 etc} signature. A simple example of a scoped enum is:

enum class MyEnum{    myfirstvalue,    mysecondvalue,    mythirdvalue};int main(){    MyEnum myenum = MyEnum::myfirstvalue;}
11.8 Smart Pointers

智能指针使用类模板封装原生指针及类指针操作及在自定义析构函数中在适当的时机调用delete或delete[](包括申请的堆内存的所有权控制或引用计数)。

Smart pointers point to objects, and when the pointer goes out of scope, the object gets destroyed. This makes them smart in the sense that we do not have to worry about manual deallocation of allocated memory. The smart pointers do all the heavy lifting for us.

There are two kinds of smart pointers, the unique pointer with an std::unique_ptr signature and a shared pointer with an std::shared_ptr signature. The difference between the two is that we can have only one unique pointer pointing at the object. In contrast, we can have multiple shared pointers pointing at an object. When the unique pointer goes out of scope, the object gets destroyed, and the memory is deallocated. When the last of the shared pointers pointing at our object goes out of scope, the object gets destroyed. The memory gets deallocated.

A unique pointer example:

#include <iostream>#include <memory>int main(){    std::unique_ptr<int> p(new int{ 123 });    std::cout << *p;} // p goes out of scope here, the memory gets deallocated, the object gets // destroyed39

A unique pointer can not be copied, only moved. To have multiple shared pointers pointing at the same object, we would write:

#include <iostream>#include <memory>int main(){    std::shared_ptr<int> p1(new int{ 123 });    std::shared_ptr<int> p2 = p1;    std::shared_ptr<int> p3 = p1;} // when the last shared pointer goes out of scope, the memory gets // deallocated39

Shared pointers can be copied. It is said they share ownership of the object. When the last shared pointer gets out of scope, the pointed-to object gets destroyed, and the memory gets deallocated.

11.9 std::unordered_set

std::set使用红黑树为底层数据结构,std::unordered_set以哈希映射实现物理存储。

The std::unordered_set is a container that allows for constant time insertion, searching, and removal of elements. This container is implemented as an array of buckets of linked lists. The hash value of each element is calculated (hashed), and the object is placed into an appropriate bucket based on the hash value. The object themselves are not sorted in any particular order. To define an unordered set, we need to include the header. Example:

#include <iostream>#include <unordered_set>int main(){    std::unordered_set<int> myunorderedset = { 1, 2, 5, -4, 7, 10 };    for (auto el : myunorderedset)    {        std::cout << el << '\n';    }}

The values are not sorted but are unique. To insert single or multiple values into an unordered_set, we use the .insert() member function:

#include <iostream>#include <unordered_set>int main(){    std::unordered_set<int> myunorderedset = { 1, 2, 5, -4, 7, 10 };    myunorderedset.insert(6); // insert a single value    myunorderedset.insert({ 8, 15, 20 }); // insert multiple values    for (auto el : myunorderedset)    {        std::cout << el << '\n';    }}

To delete a value from an unordered set, we use the .erase() member function:

#include <iostream>#include <unordered_set>int main(){    std::unordered_set<int> myunorderedset = { 1, 2, 5, -4, 7, 10 };    myunorderedset.erase(-4); // erase a single value    for (auto el : myunorderedset)    {        std::cout << el << '\n';    }}
11.10 std::unordered_map

std::map使用红黑树为底层数据结构,std::unordered_map以哈希映射实现物理存储。

Similar to std::unordered_set, there is also an std::unordered_map , an unordered container of key-value pairs with unique keys. This container also allows for fast insertion, searching, and removal of elements. The container is also data is also implemented as buckets. What element goes into what bucket depends on the element’s key hash value. To define an unordered map, we include the header. Example:

#include <iostream>#include <unordered_map>int main(){    std::unordered_map<char, int> myunorderedmap = { {'a', 1}, {'b', 2}, {'c', 5} };    for (auto el : myunorderedmap)    {        std::cout << el.first << ' '<< el.second << '\n';    }}

Here we initialize an unordered map with key-value pairs. In the range-based for loop, we print both the key and the value. Map elements are pairs. Pairs have member functions .first for accessing a key and .second for accessing a value. To insert an element into a map we can use the member function .insert() member function:

#include <iostream>#include <unordered_map>int main(){    std::unordered_map<char, int> myunorderedmap = { {'a', 1}, {'b', 2}, {'c', 5} };    myunorderedmap.insert({ 'd', 10 });    for (auto el : myunorderedmap)    {        std::cout << el.first << ' '<< el.second << '\n';    }}

We can also use the map’s operator [] to insert an element. Normally, this operator is used to access an element value by key. However, if the key does not exist, the operator inserts a new element into the map:

#include <iostream>#include <unordered_map>int main(){    std::unordered_map<char, int> myunorderedmap = { {'a', 1}, {'b', 2}, {'c', 5} };    myunorderedmap['b'] = 4; // key exists, change the value    myunorderedmap['d'] = 10; // key does not exist, insert the new element    for (auto el : myunorderedmap)    {        std::cout << el.first << ' ' << el.second << '\n';    }}
11.11 std::tuple

std::pair是一个两个成员特殊命名(first, second)的类模板。

std::tuple是一个有多个数据成员,可以由get<>()访问的类模板。

While std::pair can hold only two values, the std::tuple wrapper can hold more than two values. To use tuples, we need to include the header. To access a certain tuple element , we use the std::get(tuple_name) function:

#include <iostream>#include <utility>#include <tuple>int main(){    std::tuple<char, int, double> mytuple = { 'a', 123, 3.14 };    std::cout << "The first element is: " << std::get<0>(mytuple) << '\n';    std::cout << "The second element is: " << std::get<1>(mytuple) << '\n';    std::cout << "The third element is: " << std::get<2>(mytuple) << '\n';}

We can create a tuple using the std::make_tuple function:

#include <iostream>#include <tuple>#include <string>int main(){    auto mytuple = std::make_tuple<int, double, std::string>(123, 3.14, "Hello World.");    std::cout << "The first tuple element is: " << std::get<0>(mytuple) << '\n';    std::cout << "The second tuple element is: " << std::get<1>(mytuple) << '\n';    std::cout << "The third tuple element is: " << std::get<2>(mytuple) << '\n';}

Instead of typing a lengthy tuple type, which is std::tuple, we used the auto specifier to deduce the type name for us.

40.1.12 static_assert

The static_assert directive checks a static (constexpr) condition during compile time. If the condition is false, the directive fails the compilation and displays an error message. Example:

int main(){    constexpr int x = 123;    static_assert(x == 456, "The constexpr value is not 456.");}

Here the static_assert checks if the value of x is equal to 456 during compile time. Since it is not, the compilation will fail with a "The constexpr value is not 456." message. We can think of the static_assert as a way of testing our code during compile time. It is also a neat way of testing if the value of a constexpr expression is what we expect it to be.

40.1.13 Introduction to Concurrency

C++11 standard introduces facilities for working with threads. To enable threading, we need to add the -pthreads flag when compiling with g++ and clang on the command line. Example:

g++ -std=c++11 -Wall -pthread source.cpp

With clang it will be:

clang++ -std=c++11 -Wall -pthread source.cpp

When we compile and link our source code program, an executable file is produced. When we start the executable, the program gets loaded into memory and starts running. This running program is called a process. When we start multiple executable files, we can have multiple processes. Each process has its memory, its own address space. Within a process, there can be multiple threads. What are the threads? Threads or threads of execution are an OS mechanism that allows us to execute multiple pieces of code concurrently/simultaneously.

For example, we can execute multiple functions concurrently using threads. In a broader sense, concurrently can also mean in parallel. A thread is part of the process. A process can spawn one or more threads. Threads share the same memory and thus can communicate with each other using this shared memory.

To create a thread object, we use the std::thread class template from a header file. Once defined, the thread starts executing. To create a thread that executes a code inside a function, we supply the function name to the thread constructor as a parameter. Example:

#include <iostream>#include <thread>void function1(){    for (int i = 0; i < 10; i++)    {        std::cout << "Executing function1." << '\n';    }}int main(){    std::thread t1{ function1 }; // create and start a thread    t1.join(); // wait for the t1 thread to finish}

Here we have defined a thread called t1 that executes a function function1. We supply the function name to the std::thread constructor as a first parameter. In a way, our program now has a main thread, which is the main() function itself, and the t1 thread, which was created from the main thread. The .join() member function says: “hey, main thread, please wait for me to finish my work before continuing with yours.” If we left out the .join() function, the main thread would finish executing before the t1 thread has finished its work. We avoid this by joining the child thread to the main thread.

If our function accepts parameters, we can pass those parameters when constructing the std::thread object:

#include <iostream>#include <thread>#include <string>void function1(const std::string& param){    for (int i = 0; i < 10; i++)    {        std::cout << "Executing function1, " << param << '\n';    }}int main(){    std::thread t1{ function1, "Hello World from a thread." };    t1.join();}

We can spawn multiple threads in our program/process by constructing multiple std::thread objects. An example where we have two threads executing two different functions concurrently/simultaneously:

#include <iostream>#include <thread>void function1(){    for (int i = 0; i < 10; i++)    {        std::cout << "Executing function1." << '\n';    }}void function2(){    for (int i = 0; i < 10; i++)    {        std::cout << "Executing function2." << '\n';    }}int main(){    std::thread t1{ function1 };    std::thread t2{ function2 };    t1.join();    t2.join();}

This example creates two threads executing two different functions concurrently.

The function1 code executes in a thread t1, and the function2 code executes in a separate thread called t2.

We can also have multiple threads executing code from the same function concurrently:

#include <iostream>#include <thread>#include <string>void myfunction(const std::string& param){    for (int i = 0; i < 10; i++)    {        std::cout << "Executing function from a " << param << '\n';    }}int main(){    std::thread t1{ myfunction, "Thread 1" };    std::thread t2{ myfunction, "Thread 2" };    t1.join();    t2.join();}

Threads sometimes need to access the same object. In our example, both threads are accessing the global std::cout object in order to output the data. This can be a problem. Accessing the std::cout object from two different threads at the same time allows one thread to write a little to it, then another thread jumps in and writes a little to it, and we can end up with some strange text in the console window:

Executi.Executingng function1.Executing function2.

This means we need to synchronize the access to a shared std::cout object somehow. While one thread is writing to it, we need to ensure that the thread does not write to it.

We do so by locking and unlocking mutex-es. A mutex is represented by std::mutex class template from a header. A mutex is a way to synchronize access to shared objects between multiple threads. A thread owns a mutex once it locks the mutex, then performs access to shared data and unlocks the mutex when access to shared data is no longer needed. This ensures only one thread at the time can have access to a shared object, which is std::cout in our case.

Here is an example where two threads execute the same function and guard access to std::cout object by locking and unlocking mutexes:

#include <iostream>#include <thread>#include <string>#include <mutex>std::mutex m; // will guard std::coutvoid myfunction(const std::string& param){    for (int i = 0; i < 10; i++)    {        m.lock();        std::cout << "Executing function from a " << param << '\n';        m.unlock();    }}int main(){    std::thread t1{ myfunction, "Thread 1" };    std::thread t2{ myfunctiosn, "Thread 2" };    t1.join();    t2.join();}

We can forget to unlock the mutex manually. A better approach is to use the std::lock_guard function instead. It locks the mutex, and once it goes out of scope, it automatically unlocks the mutex. Example:

#include <iostream>#include <thread>#include <string>#include <mutex>std::mutex m; // will guard std::coutvoid myfunction(const std::string& param){    for (int i = 0; i < 10; i++)    {        std::lock_guard<std::mutex> lg(m);        std::cout << "Executing function from a " << param << '\n';    } // lock_guard goes out of scope here and unlocks the mutex}int main(){    std::thread t1{ myfunction, "Thread 1" };    std::thread t2{ myfunction, "Thread 2" };    t1.join();    t2.join();}
11.14 Deleted and Defaulted Functions

If we do not supply a default constructor, the compiler will generate one for us so that we can write:

class MyClass{};int main(){    MyClass o; // OK, there is an implicitly defined default constructor}

However, in certain situations, the default constructor will not be implicitly generated. For example, when we define a copy constructor for our class, the default constructor is implicitly deleted. Example:

#include <iostream>class MyClass{public:    MyClass(const MyClass& other)    {        std::cout << "Copy constructor invoked.";    }};int main(){    MyClass o; // Error, there is no default constructor}

To force the instantiation of a default, compiler-generated constructor, we provide the =default specifier in its declaration. Example:

#include <iostream>class MyClass{public:    MyClass() = default; // defaulted member function    MyClass(const MyClass& other)    {        std::cout << "Copy constructor invoked.";    }};int main(){    MyClass o; // Now OK, the defaulted default constructor is there    MyClass o2 = o; // Invoking the copy constructor}

The =default specifier, when used on a member function, means: whatever the language rules, I want this default member function to be there. I do not want it to be implicitly disabled.

Similarly, if we want to disable a member function from appearing, we use the =delete specifier. To disable the copy constructor and copy assignment, we would write:

#include <iostream>class MyClass{public:    MyClass()    {        std::cout << "Default constructor invoked.";    }    MyClass(const MyClass& other) = delete; // delete the copy constructor    MyClass& operator=(const MyClass& other) = delete; // delete the copy // assignment operator};int main(){    MyClass o; // OK    MyClass o2 = o; // Error, a call to deleted copy constructor    MyClass o3;    o3 = o; // Error, a call to deleted copy assignment operator}

These specifiers are mostly used in situations where we want to:

a. force or the instantiation of implicitly defined member functions such as constructors and assignment operators, when we use the =default; expression

b. disable the instantiation of implicitly defined member functions using the =delete; expression

These expressions can also be used on other functions as well.

115 Type Aliases

A type alias is a user-provided name for the existing type. If we want to use a different name for the existing type, we write: using my_type_name = existing_type_name; Example:

#include <iostream>#include <string>#include <vector>using MyInt = int;using MyString = std::string;using MyVector = std::vector<int>;int main(){    MyInt x = 123;    MyString s = "Hello World";    MyVector v = { 1, 2, 3, 4, 5 };}
2 C++14

C++14 is an ISO C++ standard published in 2014. It brings some additions to the language and the standard library, but mainly complements and fixes the C++11 standard. When we say we want to use the C++11 standard, what we actually want is the C++14 standard. Below are some of the new features for C++14.

To compile for C++14, add the -std=c++14 flag to a command-line compilation string if using g++ or clang compiler. In Visual Studio, choose Project / Options / Configuration Properties / C/C++ / Language / C++ Language Standard and choose C++14.

14.1 Binary Literals

Values are represented by literals. So far, we have mentioned three different kinds of binary literals: decimal, hexadecimal, and octal as in the example below:

int main(){    int x = 10;    int y = 0xA;    int z = 012;}

These three variables have the same value of 10, represented by different number literals. C++14 standard introduces the fourth kind of integral literals called binary literals. Using binary literals, we can represent the value in its binary form. The literal has a 0b prefix, followed by a sequence of ones and zeros representing a value. To represent the number 10 as a binary literal, we write:

int main(){    int x = 0b101010;}

The famous number 42 in binary form would be:

int main(){    int x = 0b1010;}

Important to rememberValues are values; they are some sequence of bits and bytes in memory. What can be different is the value representation. There are decimal, hexadecimal, octal, and binary representations of the value. These different forms of the same thing can be relevant to us humans. To a machine, it is all bits and bytes, transistors, and electrical current.

14.2 Digits Separators

In C++14, we can separate digits with a single quote to make it more readable:

int main(){    int x =100'000'000;}

The compiler ignores the quotes. The separators are only here for our benefit, for example, to split a large number into more readable sections.

14.3 Auto for Functions

We can deduce the function type based on the return statement value:

auto myintfn() // integer{    return 123;}auto mydoublefn() // double{    return 3.14;}int main(){    auto x = myintfn(); // int    auto d = mydoublefn(); // double}
14.4 Generic Lambdas

We can use auto parameters in lambda functions now. The type of the parameter will be deduced from the value supplied to a lambda function. This is also called a generic lambda :

#include <iostream>int main(){    auto mylambda = [](auto p) {std::cout << "Lambda parameter: " << p << '\n'; };    mylambda(123);    mylambda(3.14);}
14.5 std::make_unique

C++14 introduces a std::make_unique function for creating unique pointers. It is declared inside a header. Prefer this function to raw new operator when creating unique pointers:

#include <iostream>#include <memory>class MyClass{private:    int x;    double d;public:    MyClass(int xx, double dd)        : x{ xx }, d{ dd }    {}    void printdata() { std::cout << "x: " << x << ", d: " << d; }};int main(){    auto p = std::make_unique<MyClass>(123, 456.789);    p->printdata();}
3 C++17

The C++17 standard introduces new language and library features and changes some of the language rules.

17.1 Nested Namespaces

Remember how we said we could have nested namespaces ? We can put a namespace into another namespace. We used the following the nest namespaces:

namespace MyNameSpace1{    namespace MyNameSpace2    {        namespace MyNameSpace3        {            // some code        }    }}

The C++17 standard allows us to nest namespaces using the namespace resolution operator. The above example can now be rewritten as:

namespace MyNameSpace1::MyNameSpace2::MyNameSpace3{    // some code}
17.2 Constexpr Lambdas

Lambdas can now be a constant expression, meaning they can be evaluated during compile-time:

int main(){    constexpr auto mylambda = [](int x, int y) { return x + y; };    static_assert(mylambda(10, 20) == 30, "The lambda condition is not true.");}

An equivalent example where we put the constexpr specifier in the lambda itself, would be:

int main(){    auto mylambda = [](int x, int y) constexpr { return x + y; };    static_assert(mylambda(10, 20) == 30, "The lambda condition is not true.");}

This was not the case in earlier C++ standards.

17.3 Structured Bindings

Structured binding binds the variable names to elements of compile-time known expressions, such as arrays or maps. If we want to have multiple variables taking values of expression elements, we use the structured bindings. The syntax is:

auto [myvar1, myvar2, myvar3] = some_expression;

A simple example where we bound three variables to be aliases for three array elements would be:

int main(){    int arr[] = { 1, 2, 3 };    auto [myvar1, myvar2, myvar3] = arr;}

Now we have defined three integer variables. These variables have array elements values of 1, 2, 3, respectively. These variables are copies of array elements. Making changes to variables does not affect the array elements themselves:

#include <iostream>int main(){    int arr[] = { 1, 2, 3 };    auto [myvar1, myvar2, myvar3] = arr;    myvar1 = 10;    myvar2 = 20;    myvar3 = 30;    for (auto el : arr)    {        std::cout << el << ' ';    }}

We can make structured bindings of reference type by using the auto& syntax. This means the variables are now references to array elements and making changes to variables also changes the array elements:

#include <iostream>int main(){    int arr[] = { 1, 2, 3 };    auto& [myvar1, myvar2, myvar3] = arr;    myvar1 = 10;    myvar2 = 20;    myvar3 = 30;    for (auto el : arr)    {        std::cout << el << ' ';    }}

It is an excellent way of introducing and binding multiple variables to some container-like expression elements.

17.4 std::filesystem

The std::filesystem library allows us to work with files, paths, and folders on our system. The library is declared through a header. Paths can represent paths to files and paths to folders. To check if a given folder exists, we use:

#include <iostream>#include <filesystem>int main(){    std::filesystem::path folderpath = "C:\\MyFolder\\";    if (std::filesystem::exists(folderpath))    {        std::cout << "The path: " << folderpath << " exists.";    }    else    {        std::cout << "The path: " << folderpath << " does not exist.";    }}

Similarly, we can use the std::filesystem::path object to check if a file exists:

#include <iostream>#include <filesystem>int main(){    std::filesystem::path folderpath = "C:\\MyFolder\\myfile.txt";    if (std::filesystem::exists(folderpath))    {        std::cout << "The file: " << folderpath << " exists.";    }    else    {        std::cout << "The file: " << folderpath << " does not exist.";    }}

To iterate over folder elements, we use the std::filesystem::directory_iterator iterator:

#include <iostream>#include <filesystem>int main(){    auto myfolder = "C:\\MyFolder\\";    for (auto el : std::filesystem::directory_iterator(myfolder))    {        std::cout << el.path() << '\n';    }}

Here we iterate over the directory entries and print each of the elements full path using the .path() member function.

For Linux, we need to adjust the path and use the following instead:

#include <iostream>#include <filesystem>int main(){    auto myfolder = "MyFolder/";    for (auto el : std::filesystem::recursive_directory_iterator(myfolder))    {        std::cout << el.path() << '\n';    }}

To iterate over folder elements recursively, we use the std::filesystem::recursive_directory_iterator. This allows us to iterate recursively over all subfolders in a folder. On Windows, we would use:

#include <iostream>#include <filesystem>int main(){    auto myfolder = "C:\\MyFolder\\";    for (auto el : std::filesystem::recursive_directory_iterator(myfolder))    {        std::cout << el.path() << '\n';    }}

On Linux and similar OS-es, we would use the following path:

#include <iostream>#include <filesystem>int main(){    auto myfolder = "MyFolder/";    for (auto el : std::filesystem::directory_iterator(myfolder))    {        std::cout << el.path() << '\n';    }}

Below are some useful utility functions inside the std::filesystem namespace:

std::filesystem::create_directory for creating a directory

std::filesystem::copy for copying files and directories

std::filesystem::remove for removing a file or an empty folder

std::filesystem::remove_all for removing folders and subfolders

17.5 std::string_view

引用一段字符串并保留长度信息。

Copying data can be an expensive operation in terms of CPU usage. Passing substrings as function parameters would require making a copy of substrings. This is a costly operation. The string_view class template is an attempt to rectify that.

The string_view is a non-owning view of a string or a substring. It is a reference to something that is already there in the memory. It is implemented as a pointer to some character sequence plus the size of that sequence. With this kind of structure, we can parse strings efficiently.

The std::string_view is declared inside the header file. To create a string_view from an existing string, we write:

#include <iostream>#include <string>#include <string_view>int main(){    std::string s = "Hello World.";    std::string_view sw(s);    std::cout << sw;}

To create a string_view for a substring of the first five characters, we use the different constructor overload. This string_view constructor takes a pointer to the first string element and the length of the substring:

#include <iostream>#include <string>#include <string_view>int main(){    std::string s = "Hello World.";    std::string_view sw(s.c_str() , 5);    std::cout << sw;}

Once we create a string_view, we can use its member functions. To create a substring out of a string_view, we use the .substr() member function. To create a substring, we supply the starting position index and length. To create a substring of the first five characters, we use:

#include <iostream>#include <string>#include <string_view>int main(){    std::string s = "Hello World";    std::string_view sw(s);    std::cout << sw.substr(0, 5);}

A string_view allows us to parse (not change) the data that is already in the memory, without having to make copies of the data. This data is owned by another string or character array object.

17.6 std::any

The std::any container can hold a single value of any type. This container is declared inside the header file. Example:

可以理解为一种特殊的void*(void*在解引用前也需要类型显式转换为具体特定的类型)。

#include <any>int main(){    std::any a = 345.678;    std::any b = true;    std::any c = 123;}

To access the value of an std::any object in a safe manner, we cast it to a type of our choice using the std::any_cast function:

#include <iostream>#include <any>int main(){    std::any a = 123;    std::cout << "Any accessed as an integer: " << std::any_cast<int>(a) << '\n';    a = 456.789;    std::cout << "Any accessed as a double: " << std::any_cast<double>(a) << '\n';    a = true;    std::cout << "Any accessed as a boolean: " << std::any_cast<bool>(a) << '\n';}

Important, the std::any_cast will throw an exception if we try to convert, for example, 123 to type double. This function performs only the type-safe conversions.Another std::any member function is .has_value() which checks if the std::any object holds a value:

#include <iostream>#include <any>int main(){    std::any a = 123;    if (a.has_value())    {        std::cout << "Object a contains a value." << '\n';    }    std::any b{};    if (b.has_value())    {        std::cout << "Object b contains a value." << '\n';    }    else    {        std::cout << "Object b does not contain a value." << '\n';    }}
17.7 std::variant

There is another type of data in C++ called union. A union is a type whose data members of different types occupy the same memory. Only one data member can be accessed at a time. The size of a union in memory is the size of its largest data member. The data members overlap in a sense. To define a union type in C++, we write:

union MyUnion{    char c;        // one byte    int x;         // four bytes    double d;      // eight bytes};

Here we declared a union type that can hold characters or integers or doubles. The size of this union is the size of its largest data member double, which is probably eight bytes, depending on the implementation. Although the union declares multiple data members, it can only hold a value of one member at any given time. This is because all the data members share the same memory location. And we can only access the member that was the last written-to. Example:

#include <iostream>union MyUnion{    char c;        // one byte    int x;         // four bytes    double d;      // eight bytes};int main(){    MyUnion o;    o.c = 'A';    std::cout << o.c << '\n';    // accessing o.x or o.d is undefined behavior at this point    o.x = 123;    std::cout << o.c;    // accessing o.c or o.d is undefined behavior at this point    o.d = 456.789;    std::cout << o.c;    // accessing o.c or o.x is undefined behavior at this point}

C++17 introduces a new way of working with unions using the std::variant class template from a header. This class template offers a type-safe way of storing and accessing a union. To declare a variant using a std::variant, we would write:

#include <variant>int main(){    std::variant<char, int, double> myvariant;}

This example defines a variant that can hold three types. When we initialize or assign a value to a variant, an appropriate type is chosen. For example, if we initialize a variant with a character value, the variant will currently hold a char data member. Accessing other members at this point will throw an exception. Example:

#include <iostream>#include <variant>int main(){    std::variant<char, int, double> myvariant{ 'a' }; // variant now holds // a char    std::cout << std::get<0>(myvariant) << '\n'; // obtain a data member by // index    std::cout << std::get<char>(myvariant) << '\n'; // obtain a data member // by type    myvariant = 1024; // variant now holds an int    std::cout << std::get<1>(myvariant) << '\n'; // by index    std::cout << std::get<int>(myvariant) << '\n'; // by type    myvariant = 123.456; // variant now holds a double}

We can access a variant value by index using the std::get(variant_name) function. Or we can access the variant value by a type name using: std::get(variant_name). If we tried to access a wrong type or wrong index member, an exception of type const std::bad_variant_access& would be raised. Example:

#include <iostream>#include <variant>int main(){    std::variant<int, double> myvariant{ 123 }; // variant now holds an int    std::cout << "Current variant: " << std::get<int>(myvariant) << '\n';    try    {        std::cout << std::get<double>(myvariant) << '\n'; // exception is // raised    }    catch (const std::bad_variant_access& ex)    {        std::cout << "Exception raised. Description: " << ex.what();    }}

We define a variant that can hold either int or double. We initialize the variant with a 123 literal of type int. So now our variant holds an int data member. We can access that member using the index of 0 or a type name which we supply to the std::get function. Then we try to access the wrong data member of type double. An exception is raised. And the particular type of that exception is std::bad_variant_access. In the catch block, we handle the exception by parsing the parameter we named ex. A parameter is of type std::bad_variant_access, which has a .what() member function that provides a short description of the exception.

20.4 C++20

The C++ 20 standard promises to bring some big additions to the language. Its impact on the existing standards is said to be as big as the C++11 was to a C++98/C++03 standard. At the time of writing, the C++20 standard is to be ratified around May 2020. The full implementation and the support in the compilers should follow. Some of the following things may, at first glance, seem intimidating, especially when beginning C++. However, do not worry. At the time of writing, none of the compilers fully support the C++20 standard, but that is about to change. Once the compilers fully support the C++20 standard, trying out the examples will be much easier. With that in mind, let us go through some of the most exciting C++20 features.

20.1 Modules

Modules are the new C++20 feature, which aims to eliminate the need for the separation of code into header and source files. So far, in traditional C++, we have organized our source code using headers files and source files. We keep our declarations/interfaces in header files. We put our definitions/implementations in source files. For example, we have a header file with a function declaration:

mylibrary.h

#ifndef MYLIBRARY_H#define MYLIBRARY_Hint myfunction();#endif // !MYLIBRARY_H39

Here we declare a function called myfunction(). We surround the code with header guards, which ensures the header file is not included multiple times during the compilation. And we have a source file with the function definition. This source file includes our header file:

// mylibrary.cpp#include "mylibrary.h"int myfunction(){    return 123;}

In our main.cpp file we also include the above header file and call the function:

// main.cpp#include "mylibrary.h"int main(){    int x = myfunction();}

We include the same header multiple times. This increases compilation time. Modules are included only once, and we do not have to separate the code into interface and implementation. One way is to have a single module file, for example, mymodule.cpp where we provide the entire implementation and export of this function.

To create a simple module file which implements and exports the above function, we write:

// mymodule.cppexport module mymodule;export int myfunction() { return 123; }

Explanation: the export module mymodule; line says there is a module called mymodule in this file. In the second line, the export specifier on the function means the function will be visible once the module is imported into the main program.

We include the module in our main program by writing the import mymodule; statement.

// main.cppimport mymodule;int main(){    int x = myfunction();}

In our main program, we import the module and call the exported myfunction() function.

A module can also provide an implementation but does need to export it. If we do not want our function to be visible to the main program, we will omit the export specifier in the module. This makes the implementation private to the module:

export module mymodule;export int myfunction() { return 123; }int myprivatefunction() { return 456; }

If we have a module with a namespace in it, and a declaration inside that namespace is exported, the entire namespace is exported. Within that namespace, only the exported functions are visible Example:

// mymodule2.cppexport module mymodule2;namespace MyModule{    export int myfunction() { return 123; }}

main2.cpp:

import mymodule2;int main(){    int x = MyModule::myfunction();}
20.2 Concepts

类型总是定义了一组特定的操作。模板实现的泛型有些不能满足特定类型的需要,一种方法是类型特化,concepts的方法是显式声明类型的特定要求。

Remember the class templates and function templates providing generic types T? If we want our template argument T to satisfy certain requirements, then we use concepts. In other words, we want our T to satisfy certain compile-time criteria. The signature for a concept is:

templateconcept concept_name = requires (T var_name) { reqirement_expression; };

The second line defines a concept name followed by a reserved word requires, followed by an optional template argument T and a local var_name, followed by a requirement_expression which is a constexpr of type bool.

In a nutshell, the concept predicate specifies the requirements a template argument must satisfy in order to be used in a template. Some of the requirements we can write ourselves, some are already pre-made.

We can say that concepts constrain types to certain requirements. They can also be seen as a sort of compile-time assertions for our template types.

For example, if we want a template argument to be incrementable by one, we will specify the concept for it:

template <typename T>concept MustBeIncrementable = requires (T x) { x += 1; };

To use this concept in a template, we write:

template<MustBeIncrementable T>void myfunction(T x){    // code goes in here}

Another way to include the concept into our template is:

template<typename T> requires MustBeIncrementable <T>void myfunction(T x){    // code goes in here}

A full working example would be:

#include <iostream>#include <concepts>template <typename T>concept MustBeIncrementable = requires (T x) { x ++; };template<MustBeIncrementable T>void myfunction(T x){    x += 1;    std::cout << x << '\n';}int main(){    myfunction<char>(42); // OK    myfunction<int>(123); // OK    myfunction<double>(345.678); // OK}

This concept ensures our argument x of type T must be able to accept operator ++, and the argument must be able to be incremented by one. This check is performed during the compile-time. The requirement is indeed true for types char, int, and double. If we used a type for which the requirement is not fulfilled, the compiler would issue a compile-time error.

We can combine multiple concepts. Let us, for example, have a concept that requires the T argument to be an even or an odd number.

template <typename T>concept MustBeEvenOrOdd = requires (T x) { x % 2; };

Now our template can include both the MustBeIncrementable and MustBeEvenOrOdd concepts:

template<typename T> requires MustBeIncrementable<T> && MustBeEvenNumber<T>;void myfunction(T x){    // code goes in here}

The keyword requires is used both for the expression in the concept and when including the concept into our template class/function.

The complete program, which includes both concept requirements, would be:

#include <iostream>#include <concepts>template <typename T>concept MustBeIncrementable = requires (T x) { x++; };template <typename T>concept MustBeEvenOrOdd = requires (T x) { x % 2; };template<typename T> requires MustBeIncrementable<T> && MustBeEvenOrOdd<T>void myfunction(T x){    std::cout << "The value conforms to both conditions: " << x << '\n';}int main(){    myfunction<char>(123); // OK    myfunction<int>(124); // OK    myfunction<double>(345); // Error, a floating point number is not even // nor odd}

In this example, the template will be instantiated if both concept requirements are evaluated to true during compile time. Only the myfunction(123); and myfunction(124); functions can be instantiated and pass the compilation. The arguments of types char and int are indeed incrementable and can be either even or odd. However, the statement myfunction(345); does not pass a compilation. The reason is that the second requirement MustBeEvenOrOdd is not fulfilled as floating-point numbers are neither odd nor even.

Important! Both concepts say: for every x of type T, the statement inside the code-block { } compiles and nothing more. It just compiles. If it compiles, the requirement for that type is fulfilled.

If we want our type T to have a member function, for example, .empty() and we want the result of that function to be convertible to type bool, we write:

template <typename T>concept HasMemberFunction requires (T x){    { x.empty() } -> std::convertible_to(bool);};

There are multiple predefined concepts in the C++20 standard. They check if the type fulfills certain requirements. These predefined concepts are located inside the header. Some of them are:

a. std::integral – specifies the type should be an integral type

b. std::boolean – specifies the type can be used as a boolean type

c. std::move_constructible – specifies that the object of a particular type can be constructed using the move semantics

d. std::movable – specifies that the object of a certain type T can be moved

e. std::signed_integral – says the type is both integral and is a signed integral

20.3 Lambda Templates

We can now use template syntax in our lambda functions . Example:

auto mylambda = []<typename T>(T param){    // code};

For example, to printout the generic type name, using a templated lambda expression, we would write:

#include <iostream>#include <vector>#include <typeinfo>int main(){    auto mylambda = []<typename T>(T param)    {        std::cout << typeid(T).name() << '\n';    };    std::vector<int> v = { 1, 2, 3, 4, 5 };    mylambda(v); // integer    std::vector<double> v2 = { 3.14, 123.456, 7.13 };    mylambda(v2); // double}
20.4 [likely] and [unlikely] Attributes

If we know that some paths of execution are more likely to be executed than others, we can help the compiler optimize the code by placing attributes. We use the [[likely]] attribute before the statement that is more likely to be executed. We can also put the [[unlikely]] attribute before the statement that is unlikely to be executed. For example, the attributes can be used on case branches inside the switch statement:

#include <iostream>void mychoice(int i){    switch (i)    {    [[likely]] case 1:        std::cout << "Likely to be executed.";        break;    [[unlikely]] case 2:        std::cout << "Unlikely to be executed.";        break;    default:        break;    }}int main(){    mychoice(1);}

If we want to use these attributes on the if-else branches, we write:

#include <iostream>int main(){    bool choice = true;    if (choice) [[likely]]    {        std::cout << "This statement is likely to be executed.";    }    else [[unlikely]]    {        std::cout << "This statement is unlikely to be executed.";    }}
20.5 Ranges

A range, in general, is an object that refers to a range of elements. The new C++20 ranges feature is declared inside a header. The ranges themselves are accessed via the std::ranges name. With classic containers such as an std::vector, if we want to sort the data, we would use:

#include <iostream>#include <vector>#include <algorithm>int main(){    std::vector<int> v = { 1, 2, 3, 4, 5 };    std::sort(v.begin(), v.end());    for (auto el : v)    {        std::cout << el << '\n';    }}

The std::sort function accepts vector’s .begin() and end() iterators. With ranges, it is much simpler, we just provide the name of the range, without iterators:

#include <iostream>#include <ranges>#include <vector>#include <algorithm>int main(){    std::vector<int> v = { 3, 5, 2, 1, 4 };    std::ranges::sort(v);    for (auto el : v)    {        std::cout << el << '\n';    }}

Ranges have a feature called adaptors. One of the range adaptors is views. The views adaptors are accessed via std::ranges::views. Views are not owning. They cannot change the values of the underlying elements. It is also said they are lazily executed. This means the code from the views adaptors will not be executed until we iterate over the result of such views.

Let us create an example which uses range views to filter-out even numbers and print only the odd numbers from a vector by creating a range view:

#include <iostream>#include <ranges>#include <vector>#include <algorithm>int main(){    std::vector<int> v = { 1, 2, 3, 4, 5 };    auto oddnumbersview = v | std::views::filter([](int x) { return x % 2 == 1; });    for (auto el : oddnumbersview)    {        std::cout << el << '\n';    }}

Explanation: we have a simple vector with some elements. Then we create a view range adaptor on that vector, which filters the numbers in the range. For this, we use the pipe operator |. Only the numbers for which the predicate is true are included. In our case, this means the even numbers are excluded. Then we iterate over the filtered view and print out the elements.

Important to note, the underlying vector’s elements are unaffected as we are operating on a view, not on a vector.

Let us create an example which creates a view that returns only numbers greater than 2:

#include <iostream>#include <ranges>#include <vector>#include <algorithm>int main(){    std::vector<int> v = { 1, 2, 3, 4, 5 };    auto greaterthan2view = v | std::views::filter([](int x) { return x > 2; });    for (auto el : greaterthan2view)    {        std::cout << el << '\n';    }}

Now, let us combine the two views into one big view by separating them with multiple pipe | operators:

#include <iostream>#include <ranges>#include <vector>#include <algorithm>int main(){    std::vector<int> v = { 1, 2, 3, 4, 5 };    auto oddandgreaterthan2 = v | std::views::filter([](int x) { return x % 2 == 1; })                                | std::views::filter([](int x) { return x > 2; });    for (auto el : oddandgreaterthan2)    {        std::cout << el << '\n';    }}

This example creates a view range adaptor containing odd numbers greater than two. We create this view by combining two different range views into one.

Another ranges adaptors are algorithms. The idea is to have the algorithms overload for ranges. To call an algorithm adaptor we use: std::ranges::algorithm_name(parameters). Example using the std::ranges::reverse() algorithm:

#include <iostream>#include <ranges>#include <vector>#include <algorithm>int main(){    std::vector<int> v = { 1, 2, 3, 4, 5 };    std::ranges::reverse(v);    for (auto el : v)    {        std::cout << el << '\n';    }}

Unlike views, the ranges algorithms modify the actual vector content.

40.4.6 Coroutines

A coroutine is a function that can be suspended and be resumed. The ordinary function is a coroutine if it uses any of the following operators in its function body:

a. co_await – suspends the execution of the coroutine until some other computation is performed, that is until the coroutine itself resumes

b. co_yield – suspends a coroutine and return a value to the caller

c. co_return – returns from a coroutine and stops its execution

20.7 std::span

std::string_view定义的是特定的类型(string),std::span是与std:string_view相同概念的全部类型的应用。

Some containers and types store their elements in a sequence, one next to the other. This is the case for arrays and vectors. We can represent such containers with a pointer to their first element plus the length of the container. A std::span class template from a header is just that. A reference to a span of contiguous container elements. One reason to use the std::span, is that it is cheap to construct and copy. Span does not own a vector or an array it references. However, it can change the value of the elements. To create a span from a vector we use:

#include <iostream>#include <vector>#include <span>int main(){    std::vector<int> v = { 1, 2, 3 };    std::span<int> myintspan = v;    myintspan[2] = 256;    for (auto el : v)    {        std::cout << el << '\n';    }}

Here, we created a span that references vector elements. Then we used the span to change the vector’s third element. With span, we do not have to worry about passing a pointer and a length around, and we just use the neat syntax of a span wrapper. Since the size of the vector can change, we say our span has a dynamic extent . We can create a fixed-size span from a fixed-sized array. We say our span now has a static extent. Example:

#include <iostream>#include <span>int main(){    int arr[] = { 1, 2, 3, 4, 5 };    std::span<int, 5> myintspan = arr;    myintspan[4] = 10;    for (auto el : arr)    {        std::cout << el << '\n';    }}
20.8 Mathematical Constants

C++20 standard introduces a way to represent some of the mathematical constants. To use them, we need to include the header. The constants themselves are inside the std::numbers namespace. The following example shows how to use numbers pi and e, results of logarithmic functions and square roots of numbers 2 and 3:

#include <iostream>#include <numbers>int main(){    std::cout << "Pi: " << std::numbers::pi << '\n';    std::cout << "e: " << std::numbers::e << '\n';    std::cout << "log2(e): " << std::numbers::log2e << '\n';    std::cout << "log10(e): " << std::numbers::log10e << '\n';    std::cout << "ln(2): " << std::numbers::ln2 << '\n';    std::cout << "ln(10): " << std::numbers::ln10 << '\n';    std::cout << "sqrt(2): " << std::numbers::sqrt2 << '\n';    std::cout << "sqrt(3): " << std::numbers::sqrt3 << '\n';}

ref:

Slobodan Dmitrović 《Modern C++ for Absolute Beginners》

-End-

标签: #c语言0b1010