"Modern C++ Tutorial" Notes (3)

3 Enhancement of the language runtime

3.1 lambda expressions

One of the most important features of modern C++. Similar to anonymous class functions.
The basic syntax of a Lambda expression is as follows:

[捕获列表](参数列表) mutable(可选) 异常属性 -> 返回类型 {
// 函数体
}

The so-called capture list can actually be understood as a type of parameter. By default, the internal function body of a Lambda expression cannot use variables outside the function body. At this time, the capture list can play the role of passing external data. According to the behavior passed, the capture list is also divided into the following types:

  1. Value capture
    Similar to parameter passing by value, the premise of value capture is that the variable can be copied, the difference is that the captured variable is copied when the Lambda expression is created, not when it is called.
void lambda_value_capture() {
    
    
    int value = 1;
    auto copy_value = [value] {
    
    
        return value;
    };
    value = 100;
    auto stored_value = copy_value();
    std::cout << "stored_value = " << stored_value << std::endl;
    // 这时, stored_value == 1, 而 value == 100.
    // 因为 copy_value 在创建时就保存了一份 value 的拷贝
}
  1. Reference capture
    Similar to parameter passing by reference, reference capture saves references, and the value will change.
void lambda_reference_capture() {
    
    
    int value = 1;
    auto copy_value = [&value] {
    
    
        return value;
    };
    value = 100;
    auto stored_value = copy_value();
    std::cout << "stored_value = " << stored_value << std::endl;
    // 这时, stored_value == 100, value == 100.
    // 因为 copy_value 保存的是引用
}
  1. Implicit capture
    Manually writing the capture list is sometimes very complicated. This mechanical work can be handed over to the compiler. At this time, you can write an & or = in the capture list to declare to the compiler that reference capture or value capture is used .

  2. Expression capture
    The value capture and reference capture mentioned above are variables that have been declared in the outer scope, so these capture methods capture lvalues ​​instead of rvalues.

C++14 allows captured members to be initialized with arbitrary expressions, which allows the capture of rvalues. The declared capture variable type will be judged according to the expression, and the judgment method is essentially the same as using auto:

#include <iostream>
#include <memory>  // std::make_unique
#include <utility> // std::move

void lambda_expression_capture() {
    
    
    auto important = std::make_unique<int>(1);
    auto add = [v1 = 1, v2 = std::move(important)](int x, int y) -> int {
    
    
        return x+y+v1+(*v2);
    };
    std::cout << add(3,4) << std::endl;
}

In the above code, important is an exclusive pointer, which cannot be captured by the "=" value. At this time, we can transfer it to an rvalue and initialize it in the expression.

We mentioned that the auto keyword cannot be used in the parameter list, because this way of writing will conflict with the function of the template. But Lambda expressions are not ordinary functions, so Lambda expressions cannot be templated without explicitly specifying the type of the parameter list. Fortunately, this kind of trouble only exists in C++11. Starting from C++14, the formal parameters of Lambda functions can use the auto keyword to generate meaningful generics:

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

add(1, 2);
add(1.1, 2.2);

3.2 Function object wrappers

std::function

The essence of a Lambda expression is an object (called a closure object) of a class type (called a closure type) similar to the function object type. When the capture list of a Lambda expression is empty, the closure object can also be converted to function pointer value to pass, for example:

#include <iostream>

using foo = void(int); // 定义函数类型, using 的使用见上一节中的别名语法
void functional(foo f) {
    
     // 参数列表中定义的函数类型 foo 被视为退化后的函数指针类型 foo*
    f(1); // 通过函数指针调用函数
}

int main() {
    
    
    auto f = [](int value) {
    
    
        std::cout << value << std::endl;
    };
    functional(f); // 传递闭包对象,隐式转换为 foo* 类型的函数指针值
    f(1); // lambda 表达式调用
    return 0;
}

The above code gives two different calling forms, one is to call Lambda as a function type, and the other is to call Lambda expression directly. In C++11, these concepts are unified, and the The types of objects that can be called are collectively called callable types. And this type is introduced through std::function.
C++11 std::function is a general-purpose, polymorphic function package. Its instances can store, copy, and call any callable target entity. It is also an existing callable entity in C++. A type-safe package (relatively speaking, function pointer calls are not type-safe), in other words, a container of functions. When we have a container of functions, we can more conveniently handle functions and function pointers as objects. For example:

#include <functional>
#include <iostream>

int foo(int para) {
    
    
    return para;
}

int main() {
    
    
    // std::function 包装了一个返回值为 int, 参数为 int 的函数
    std::function<int(int)> func = foo;

    int important = 10;
    std::function<int(int)> func2 = [&](int value) -> int {
    
    
        return 1+value+important;
    };
    std::cout << func(10) << std::endl;
    std::cout << func2(10) << std::endl;
}

std::bind 和 std::placeholder

And std::bind is used to bind the parameters of function calls. It solves the demand that sometimes we may not be able to obtain all the parameters of a function at one time. Through this function, we can partially call The parameters are bound to the function in advance to become a new object, and then the call is completed after the parameters are complete. For example:

int foo(int a, int b, int c) {
    
    
    ;
}
int main() {
    
    
    // 将参数1,2绑定到函数 foo 上,
    // 但使用 std::placeholders::_1 来对第一个参数进行占位
    auto bindFoo = std::bind(foo, std::placeholders::_1, 1,2);
    // 这时调用 bindFoo 时,只需要提供第一个参数即可
    bindFoo(1);
}

3.3 Rvalue references

Rvalue references are one of the important features introduced by C++11 with the same name as lambda expressions. Its introduction solves a large number of historical problems in C++, eliminates additional overheads such as std::vector and std::string, and makes the function object container std::function possible.

First figure out what is an rvalue

prvalues ​​of lvalues, rvalues, perithvalues, rvalues

Lvalue (lvalue, left value), as the name implies, is the value to the left of the assignment symbol. To be precise, an lvalue is a persistent object that survives an expression (not necessarily an assignment expression).

Rvalue (rvalue, right value), the value on the right, refers to a temporary object that no longer exists after the expression ends.

In C++11, in order to introduce powerful rvalue references, the concept of rvalue is further divided into: prvalue and xvalue.

Pure rvalue (prvalue, pure rvalue), a pure rvalue, is either a pure literal, such as 10, true; or the evaluation result is equivalent to a literal or an anonymous temporary object, such as 1+2. Temporary variables returned by non-references, temporary variables generated by operation expressions, primitive literals, and Lambda expressions are all prvalues.

It should be noted that, except for string literals, literals are all prvalues. A string literal, on the other hand, is an lvalue of type const char array. For example:

#include <type_traits>

int main() {
    
    
    // 正确,"01234" 类型为 const char [6],因此是左值
    const char (&left)[6] = "01234";

    // 断言正确,确实是 const char [6] 类型,注意 decltype(expr) 在 expr 是左值
    // 且非无括号包裹的 id 表达式与类成员表达式时,会返回左值引用
    static_assert(std::is_same<decltype("01234"), const char(&)[6]>::value, "");

    // 错误,"01234" 是左值,不可被右值引用
    // const char (&&right)[6] = "01234";
}

But note that an array can be implicitly converted to the corresponding pointer type, and the result of the conversion expression (if it is not an lvalue reference) must be an rvalue (an rvalue reference is an xvalue, otherwise it is a prvalue). For example:

const char*   p   = "01234";  // 正确,"01234" 被隐式转换为 const char*
const char*&& pr  = "01234";  // 正确,"01234" 被隐式转换为 const char*,该转换的结果是纯右值
// const char*& pl = "01234"; // 错误,此处不存在 const char* 类型的左值

Xvalue (xvalue, expiring value) is a concept proposed by C++11 to introduce rvalue references (so in traditional C++, prvalue and rvalue are the same concept), that is, to be destroyed, but A value that can be moved.

It may be a little difficult to understand the x value, let's look at this code:

std::vector<int> foo() {
    
    
    std::vector<int> temp = {
    
    1, 2, 3, 4};
    return temp;
}

std::vector<int> v = foo();

In such code, according to the traditional understanding, the return value temp of the function foo is created internally and then assigned to v. However, when v obtains this object, it will copy the entire temp and then destroy it. If this temp is very large, which will cause a lot of additional overhead (this is the problem that traditional C++ has been criticized for). In the last line, v is an lvalue, and the value returned by foo() is an rvalue (also a prvalue). However, v can be captured by other variables, and the return value generated by foo() is used as a temporary value. Once copied by v, it will be destroyed immediately, and cannot be obtained or modified. The dying value defines such a behavior: the temporary value can be identified and can be moved at the same time.

After C++11, the compiler has done some work for us. The lvalue temp here will be subjected to this implicit rvalue conversion, which is equivalent to static_cast<std::vector &&>(temp), and here The v will shift the value locally returned by foo. That is the move semantics we will mention later.

rvalue reference and lvalue reference

To get a dying value, you need to use an rvalue reference: T &&, where T is the type. The declaration of an rvalue reference extends the life cycle of this temporary value. As long as the variable is still alive, the xvalue will continue to survive.

C++11 provides the std::move method to unconditionally convert lvalue parameters to rvalues. With it, we can easily obtain an rvalue temporary object, for example:

#include <iostream>
#include <string>

void reference(std::string& str) {
    
    
    std::cout << "左值" << std::endl;
}
void reference(std::string&& str) {
    
    
    std::cout << "右值" << std::endl;
}

int main()
{
    
    
    std::string lv1 = "string,"; // lv1 是一个左值
    // std::string&& r1 = lv1; // 非法, 右值引用不能引用左值
    std::string&& rv1 = std::move(lv1); // 合法, std::move可以将左值转移为右值
    std::cout << rv1 << std::endl; // string,

    const std::string& lv2 = lv1 + lv1; // 合法, 常量左值引用能够延长临时变量的生命周期
    // lv2 += "Test"; // 非法, 常量引用无法被修改
    std::cout << lv2 << std::endl; // string,string,

    std::string&& rv2 = lv1 + lv2; // 合法, 右值引用延长临时对象生命周期
    rv2 += "Test"; // 合法, 非常量引用能够修改临时变量
    std::cout << rv2 << std::endl; // string,string,string,Test

    reference(rv2); // 输出左值

    return 0;
}

Although rv2 refers to an rvalue, since it is a reference, rv2 is still an lvalue.

Guess you like

Origin blog.csdn.net/YuhsiHu/article/details/131971835