C++11 new feature learning - "Modern C++ Tutorial" reading notes: Chapter 3 - Lambda expressions, function object classes and rvalue references

Online reading link: https://changkun.de/modern-cpp/zh-cn/00-preface/
I am currently reading the "Modern C++ Tutorial" on the link, and post it as a study note. If there is any infringement, it will be deleted immediately .

Chapter 3 Language Runtime Enhancements

​ This chapter introduces Lambda expressions, function object wrappers and rvalue references, among which Lambda expressions and rvalue references are important features introduced by C++11.

​Lambda expression , suitable for the situation of "need a function, but don't want to bother to name a function", ① can be roughly understood as a class function defined within the function, including capture list, parameter list, return type definition and internal function body In the other part, the required parameters are directly passed in when calling , and the capture list allows multiple methods such as value passing, reference passing, and rvalue passing through expressions; and ② Generic Lambda can further omit the parameter list parameters The definition of type and return type allows the compiler to implement type deduction independently .

​Function object wrapper , ① introduces the lambda expression that std::functionunifies the empty capture list, function type and other callable types , and provides a type-safe container for the storage, copying and calling of these functions and function pointers ; ② use and processing In the case of "not being able to obtain all parameters of a function at one time", realize the binding and placeholder of some parameters , and complete the call after the parameters are complete.std::bindstd::placeholder

​Rvalue references , ① Use T&&declarations to extend the declaration period of temporary variables, provide std::movemethods to unconditionally convert lvalues ​​​​to rvalues, and use rvalue conversion and move operations to replace the operations of creating, copying, and destroying original objects in traditional C++, greatly improving Reduce additional overhead ; ② For the reference collapse rules after the introduction of rvalue references, use std::forwardmethods to ensure that the original parameter types are maintained when passing functions , which is static_cast<T&&>equivalent to .

3.1 Lambda expression

Basic Lambda

The Lambda function actually provides a feature similar to an anonymous function, which is used for "the situation where a function is needed, but you don't want to bother naming a function". Its basic syntax is as follows

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

The " capture " change is difficult to understand. By default, the internal function body of a Lambda expression cannot use variables outside the function body, so the capture list plays the role of passing external parameters, but only parameters need to be passed in when the expression is subsequently called. , without passing in a capture.

(1) Value capture is similar to parameter passing, provided that the variable can be copied, and the captured variable is copied when the Lambda expression is created, not when it is called

int value = 1;
// 定义lambda表达式(值捕获)
auto copy_value = [value] {
    
    
    return value;
};
value = 100;
// 使用lambda表达式
auto stored_value = copy_value();

(2) Reference capture , similar to reference passing parameters, save the reference, and the value will change

int value = 1;
// 定义lambda表达式(引用捕获)
auto copy_value = [&value] {
    
    
    return value;
};
value = 100;
// 使用lambda表达式
auto stored_value = copy_value();

(3) Implicit capture . Compared with the first two capture methods, the lambda expression can also not []write the variable name in , but specify the capture type in **[], and directly use the corresponding variable name in the function body. Let the compiler deduce the capture list by itself, specifically: [&]capture by reference, [=]capture by value.

(4) [Expression capture] However, the above capture methods capture lvalues, not rvalues. Starting from C++14, the captured member is allowed to be initialized with any expression. For example, the following code importantis an exclusive pointer that cannot be captured by the "=" value. At this time, it will be transferred to an rvalue. In the expression initialization.

#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);
    };
    // lambda表达式的使用,这里的3,4对应了x,y
    std::cout << add(3,4) << std::endl;
}

Application: Use lambda expressions for sorting operations , creating possibilities for some complex functions

std::vector<int> v = {
    
    3, 1, 5, 4, 2};
sort(v.begin(), v.end(), [](int a, int b){
    
    
    return b < a;
});

Generic Lambda

Starting from C++14, the formal parameters of Lambda functions can use autokeywords to generate generic types:

auto add = [](auto x, auto y) {
    
    
    return x+y;
};
add(1, 2);
add(1.1, 2.2);

3.2 Function object wrappers

std::function callable type

The essence of a lambda expression is an object (called a closure object) of a class type (called a closure type) similar to a function object type. When the capture list of a lambda expression is empty, it can also be called as a function pointer pass, as follows:

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

As you can see, there are two different ways of invoking: ① pass as a function type to call; ② directly call the lambda expression. C++ introduces std::functiontypes that unify the types of these objects that can be called, which are collectively called callable types .

std::functionThe instance of can store, copy and call any target entity that can be called. Compared with the call of function pointer, it is a type-safe package. In fact, it provides a container for functions , making it more convenient to treat functions and function pointers as objects.

#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

std::bindThe parameters used to bind function calls are applicable to the situation that we " cannot obtain all the parameters of calling a function at one time ". Through this function, some parameters are bound to the function in advance, and std::placeholder the unobtained parameters are used Make a placeholder, and complete the call after the parameters are complete. like:

#include <functional>
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

The introduction of rvalue references solves a large number of historical problems and eliminates additional overhead such as std::vector, etc., which makes the function object class possible.std::stringstd::function

Rvalue references introduced by C++11 are &&denoted by and can be modified.

int && a = 10;
a = 100;

Conceptual understanding: lvalue, rvalue, prvalue of rvalue, dying value

  • Lvalue : lvalue, left value, the value to the left of the assignment symbol, more precisely an object that persists after the expression .
  • Rvalue : rvalue, right value, the value on the right, a temporary object that no longer exists after the expression ends. (It is the same concept as prvalue in traditional C++)
  • Pure rvalues : prvalue, pure rvalue, pure literals (such as 10, true) or evaluation results are equivalent to literals/anonymous temporary objects (such as 1+2), temporary variables returned by non-references, temporary variables produced by operation expressions, primitive literals, lambdas Expressions are prvalues. (Note that the string literal is a sit value, the type is const charan array)
  • Xvalue : xvalue, expiring value, the concept proposed by C++11 to introduce rvalue references, that is, values ​​that are about to be destroyed but can be moved . It defines such a behavior that temporary values ​​can be recognized and moved at the same time. You can see the analysis of the following piece of code:
std::vector<int> foo() {
    
    
    std::vector<int> temp = {
    
    1, 2, 3, 4};
    return temp;
}

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

in the code above

  • In traditional C++ , foothe return value of the function tempis assigned to the object after it is created internally. vAfter vobtaining the object, the entire object is copied andtemp then destroyed . additional overhead .temptemp
  • After C++11 , the compiler has done some work to tempperform implicit rvalue conversion on the lvalue here , and then movev the locally returned value , which is the move semantics mentioned later.foo

Here we also need to distinguish between the concepts of constant and non-constant . When defining a constant, it is constmodified , thus forming four types of (non-)constant left/right values. Their possible references and usage scenarios are as follows:

value type →
reference type ↓
non-const lvalue const lvalue non-const rvalue const rvalue scenes to be used
non-const lvalue reference Y N N N none
const lvalue reference Y Y Y Y Commonly used in classes to construct copy constructors
non-const rvalue reference N N Y N Mobile Semantics, Perfect Forwarding
const rvalue reference N N Y Y no practical use

Reference: http://c.biancheng.net/view/7829.html

[rvalue reference and lvalue reference]

T &&The declaration of rvalue reference can extend the lifetime of the temporary value, and C++11 provides std::movethis method to unconditionally convert lvalue parameters to rvalues.

Non-consts are not allowed to bind to non-lvalues; consts are allowed to bind to non-lvalues.

move semantics

In traditional C++ , if you want to move resources, you need the caller to copy and then destruct , or implement the interface of the moving object by yourself. It is unreasonable to throw away or destroy all the things in the old house. The problem with traditional C++ here is that there is no distinction between the concept of moving and copying, resulting in a large amount of unnecessary data copying , wasting time and space, and the std::moveemergence header#include <utility> file) or ② function return value implementation.

std::moveWhat it does is actually convert the lvalue to an rvalue .

Look at the following two pieces of code as examples of using move semantics (by means of ① and ② respectively):

#include <iostream> // std::cout
#include <utility> // std::move
#include <vector> // std::vector
#include <string> // std::string

int main() {
    
    

    std::string str = "Hello world.";
    std::vector<std::string> v;

    // 将使用 push_back(const T&), 即产生拷贝行为
    v.push_back(str);
    // 将输出 "str: Hello world."
    std::cout << "str: " << str << std::endl;

    // 将使用 push_back(const T&&), 不会出现拷贝行为
    // 而整个字符串会被移动到 vector 中,所以有时候 std::move 会用来减少拷贝出现的开销
    // 这步操作后, str 中的值会变为空
    v.push_back(std::move(str));
    // 将输出 "str: "
    std::cout << "str: " << str << std::endl;

    return 0;
}
#include <iostream>
class A {
    
    
public:
    int *pointer;
    A():pointer(new int(1)) {
    
    
        std::cout << "构造" << pointer << std::endl;
    }
    A(A& a):pointer(new int(*a.pointer)) {
    
    
        std::cout << "拷贝" << pointer << std::endl;
    } // 无意义的对象拷贝
    A(A&& a):pointer(a.pointer) {
    
      // 如果没有这一函数,下面的obj构造将自动调用上面的“拷贝”方式的构造函数
        a.pointer = nullptr;
        std::cout << "移动" << pointer << std::endl;
    }
    ~A(){
    
    
        std::cout << "析构" << pointer << std::endl;
        delete pointer;
    }
};
// 防止编译器优化
A return_rvalue(bool test) {
    
    
    A a,b;
    if(test) return a; // 等价于 static_cast<A&&>(a);
    else return b;     // 等价于 static_cast<A&&>(b);
}
int main() {
    
    
    A obj = return_rvalue(false);
    std::cout << "obj:" << std::endl;
    std::cout << obj.pointer << std::endl;
    std::cout << *obj.pointer << std::endl;
    return 0;
}

Running Results and Explanation

构造0x2425c20  // a被创建出来
构造0x2425c40  // b被创建出来
移动0x2425c40  // return b时,b作为右值引用传递到obj的构造函数中,指针被移动给obj
析构0  // return_rvalue函数运行结束,对涉及的临时变量进行析构,移动完成后b本身变为了nullptr
析构0x2425c20  // a的指针地址
obj:
0x2425c40
1
析构0x2425c40  // 程序结束,析构obj

perfect forwarding

In traditional C++, we cannot continue to refer to a reference type, but C++ relaxes this requirement due to the appearance of rvalue references, resulting in the so-called "reference collapse rule", which allows references (left/right) to refer to , follow The rules are as follows:

Function parameter type (parameter definition type) Actual parameter type (call incoming type) deduced parameter type (type used within the function)
T& left quote T&
T& right quote T&
T&& left quote T&
T&& right quote T&&
void reference(int& v) {
    
    
    std::cout << "左值" << std::endl;
}
void reference(int&& v) {
    
    
    std::cout << "右值" << std::endl;
}
template <typename T>
void pass(T&& v) {
    
                                            // 函数形参类型
    std::cout << "普通传参:";
    reference(v); // 始终调用 reference(int&)    // 推导后形参类型
}
int main() {
    
    
    std::cout << "传递右值:" << std::endl;
    pass(1); // 1是右值, 但输出是左值       // 实参参数类型是右引用,但是进入pass后实际调用了 void reference(int& v)

    std::cout << "传递左值:" << std::endl;
    int l = 1;
    pass(l); // l 是左值, 输出左值            // 实参参数类型

    return 0;
}

Because of the existence of this reference collapse rule, right references may become left references in the process of passing, so some kind of rules are needed to help us keep the original parameter type when passing parameters (left references keep left references , right references remain right references), this is equivalent std::forwardto static_cast<T&&>. The method of use is as follows, so that no redundant copy is caused, and the perfect forwarding of function arguments is also realized .

void reference(int& v) {
    
    
    std::cout << "左值引用" << std::endl;
}
void reference(int&& v) {
    
    
    std::cout << "右值引用" << std::endl;
}

reference(std::forward<T>(v)); // 保持v原本的参数类型(左/右引用)
// 等效于
reference(static_cast<T&&>(v));

[Perfect forwarding auto&&in ]

Guess you like

Origin blog.csdn.net/lj164567487/article/details/126853600