2020 Autumn Recruitment _C++ notes of left and right values, copy construction and move construction, automatic type deduction

Copy constructor

Copy constructor and assignment constructor

The copy constructor is an object to initialize a memory area, this memory is the memory of the new object; and the assignment function is to assign a value to an object that has been initialized.

The scenario of calling the copy constructor

  1. An object is passed into a function by value transfer (including pointer transfer);
  2. An object is returned from the function by value;
  3. One object needs to be initialized by another object.

PS: The assignment constructor is called when the assignment operation is performed with "=". (In addition, cls c2 = c1; // Use "=" to call the copy constructor when initializing)

The parameter type of the copy constructor must be a reference

If the parameter type is not a reference, value transfer is performed, and the value transfer will call the copy constructor itself, resulting in endless recursive calls to the copy constructor.

Deep copy and shallow copy

The difference between shallow copy and deep copy is that shallow copy does not copy the external memory pointed to by the pointer in the object for the new object when copying the object, while deep copy creates an independent assignment of external memory for the new object when copying the object. .
PS: If the two objects are not independent, when the object is about to end, the destructor will be called twice, and the external memory shared by the two objects will be released twice, resulting in the phenomenon of pointer hanging (wild pointer), and the second memory Release abnormally.

The default copy constructor of the class will simply initialize the copy class with the value of the member of the copied class. That is to say, if the class member is a pointer, the pointer is just copied, and the memory space pointed to by the pointers of the two is not copied. Consistent. Therefore, the default copy constructor of the class is shallow copy. Here is the test code:

#include <iostream>

using namespace std;

class cls{
    
    
public:
    int* data = new int[10];
    int d = 1;
};

int main(void)
{
    
    
    cls* c1 = new cls();
    cls c2 = *c1;    // 还可以 cls c2(*c1),编译器默认提供的拷贝构造函数
    cls c3;          // 对象默认初始化,分配内存
    c3 = *c1;        // 编译器默认提供的赋值构造函数

    // 对象内部内容的成员变量进行了拷贝
    c1->d =  3;
    cout << "c1: " << c1->d << endl; // 3
    cout << "c1: " << c2.d << endl; // 1
    cout << "c1: " << c3.d << endl; // 1

    // 对象内部的指针所指向的外部内存没有进行拷贝,指向同一块内存地址
    c1->data[0] =  3;
    cout << "c1: " << c1->data[0] << endl; // 3
    cout << "c1: " << c2.data[0] << endl; // 3
    cout << "c1: " << c3.data[0] << endl; // 3
    // 指针所指向的数组首地址一样
    cout << "c1: " << c1->data << endl; 
    cout << "c1: " << c2.data << endl; 
    cout << "c1: " << c3.data << endl; 
    return 0;
}

The following content is a condensed version of the notes, the details are derived from [c++11] I understand the rvalue reference, move semantics and perfect forwarding .
Additional reference: Lesson 13 rvalue references

Lvalue and rvalue

The lvalue refers to the persistent object that still exists after the expression ends, and the rvalue refers to the temporary object that no longer exists when the expression ends. All named variables or objects are lvalues, and rvalues ​​are unnamed. It is difficult to get the real definition of lvalue and rvalue, but there is a convenient way to distinguish between lvalue and rvalue: see if you can take the address of the expression, if it can, it is the lvalue, otherwise it is the rvalue.

Lvalue references and rvalue references

Lvalue references (T&) can only bind lvalues, and rvalue references (T&&) can only bind rvalues. If the binding is incorrect, the compilation will fail. However, the constant lvalue reference (const T&) is a "universal" reference type, it can bind a non-constant lvalue, constant lvalue, rvalue, and when binding the rvalue, the constant lvalue reference can also Extend the lifetime of the rvalue like an rvalue reference. The disadvantage is that it can only be read but not changed.

To summarize, where T is a specific type:

  • Lvalue reference, using T&, can only bind lvalue
  • Rvalue reference, using T&&, can only bind rvalue
  • Constant lvalue, using const T&, you can bind both lvalue and rvalue
  • A named rvalue reference, the compiler will consider it an lvalue
  • The compiler has return value optimization (RVO), but don’t rely too much on it

Move construction and move assignment

The difference between the move constructor and the copy constructor is that the parameter of the copy construction const T& stris a constant lvalue reference, while the parameter of the move construction T&& stris an rvalue reference. The temporary object is an rvalue, and enters the move constructor first instead of the copy constructor. The move constructor is different from the copy construction. It does not redistribute a new space and copy the object to be copied, but "stole" it, pointing its own pointer to someone else's resource, and then modifying someone else's pointer to nullptr, This step is very important. If you don’t modify someone else’s pointer to null, then this resource will be released when the temporary object is destructed, and "steal" is also a waste of stealing. The following picture can explain the difference between copy and move.
Insert picture description here

Move semantics and std::move()

For an lvalue, the copy constructor must be called, but some lvalues ​​are local variables and have a short life cycle. Can they be moved instead of copied? In order to solve this problem, C++11 provides the std::move() method to convert lvalues ​​to rvalues ​​to facilitate the application of move semantics.

important point:

  • B = std::move(A), although A’s resources are given to B, A is not immediately destructed, only when A leaves its scope, so if you continue to use A’s Member variables, unexpected errors may occur.
  • If we do not provide a move constructor, but only provide a copy constructor, std::move() will fail but no error will occur, because the compiler will find the copy constructor when it cannot find the move constructor, which is also a copy construction The parameter of the function is the reason for the const T& constant lvalue reference!
  • All containers in c++11 implement the move semantics. Move only transfers the control of resources. In essence, it forces the lvalue to be used as an rvalue for moving copy or assignment, and avoiding objects containing resources A needless copy occurs. Move is effective for objects that have members with resources such as memory and file handles. If it is some basic types, such as int and char[10] arrays, etc., if you use move, copying will still occur (because there is no corresponding move constructor), So move is more meaningful to objects that contain resources.

Universal references

When rvalue references are combined with templates, T&& does not necessarily mean rvalue references, it may be an lvalue reference or an rvalue reference. The && here is an undefined reference type, called a universal reference. It must be initialized. Whether it is an lvalue reference or an rvalue reference depends on its initialization. If it is initialized by an lvalue, it is an lvalue Reference; if initialized by an rvalue, it is an rvalue reference .
Note: Only when automatic type inference occurs (such as the automatic type inference of a function template, or the auto keyword), && is a universal reference.

Perfect forwarding and std::forward()

The so-called forwarding is to continue to transfer parameters to another function for processing through a function. The original parameter may be an rvalue or an lvalue. If the original characteristics of the parameter (left value/right value, const/non) can be maintained -const), then it is perfect.

Currently, the only way to achieve perfect forwarding is to use universal reference types and the std::forward() template function to achieve perfect forwarding. (Implementation principle: Lesson 15 perfect forwarding (std::forward) )

template<class T>
void g(T&& t){
    
    
	f(std::forward<T>(t));
}

The code tested below can prove that perfect forwarding can be achieved with the help of universal reference types and the std::forward() template function.

#include <iostream>
using namespace std;

// 在模板定义语法中关键字class与typename的作用完全一样。 
template<typename T>
void f(T& t){
    
    
	cout << "lvalue" << endl;
}
 
template<typename T>
void f(T&& t){
    
    
	cout << "rvalue" << endl;
}

template<typename T>
void f(const T&& t){
    
    
	cout << "const rvalue" << endl;
}

template<typename T>
void f(const T& t){
    
    
	cout << "const lvalue" << endl;
}

// 完美转发实现
// 将函数g的参数原封不动传递到函数f中
template<class T>
void g(T&& t){
    
    
	f(std::forward<T>(t));
}
 
int main(){
    
    
	g(1); // 传入右值
	
	int x = 1;
	g(x); // 传入左值

	const int y = 1;
	g(move(y)); // 传入const右值

	g(y); // 传入const左值
}

Output result:
rvalue
lvalue
const rvalue
const lvalue

emplace_back reduces memory copy and move

We generally like to use vector before push_back(). From the above we can see that unnecessary copies are prone to occur. The solution is to add mobile copy and assignment functions to our classes, but there is actually an easier way! Just use emplace_back()replacement push_back().

#include <iostream>
#include <cstring>
#include <vector>
using namespace std;

class A {
    
    
public:
    A(int i){
    
    
//        cout << "A()" << endl;
        str = to_string(i);
    }
    ~A(){
    
    }
    A(const A& other): str(other.str){
    
    
        cout << "A&" << endl;
    }

public:
    string str;
};

int main()
{
    
    
    vector<A> vec;
    vec.reserve(10);
    for(int i=0;i<10;i++){
    
    
        vec.push_back(A(i)); //调用了10次拷贝构造函数
//        vec.emplace_back(i);  //一次拷贝构造函数都没有调用过
    }
    for(int i=0;i<10;i++)
        cout << vec[i].str << endl;
}

emplace_back()You can construct an object directly through the parameters of the constructor, but the premise is that there is a corresponding constructor.
For mapsum set, you can use emplace(). Basically emplace_back()correspond push_bakc(), emplce()correspond insert(). Use of newly added emplace related functions in C++11 container

Move semantics swap()also has a great impact on functions. The previous implementation of swap may require three memory copies, but with move semantics, high-performance exchange functions can be realized.

to sum up

  1. There are two value types, lvalue and rvalue.
  2. There are three types of references, lvalue references, rvalue references and general references. Lvalue references can only bind lvalues, rvalue references can only bind rvalues, and universal references are determined by the type of value bound during initialization.
  3. Lvalue and rvalue are independent of their types. Rvalue references may be lvalues ​​or rvalues. If this rvalue reference has been named, it is an lvalue.
  4. Reference folding rule: All rvalue references superimposed on the rvalue reference are still an rvalue reference, and other reference folding are all lvalue references. When T&& is a template parameter, enter an lvalue, it will become an lvalue reference, and an rvalue will become a named rvalue application.
  5. Move semantics can reduce unnecessary memory copy. To realize move semantics, you need to implement move constructor and move assignment function.
  6. std::move()Convert an lvalue to an rvalue, and force the use of move copy and assignment functions. This function itself does not have any special operations on this lvalue.
  7. std::forward()universal referencesRealize perfect forwarding together with Universal Reference.
  8. Increase performance with empalce_back()replacement push_back().

The following content is a condensed version of the notes, the details are derived from the automatic deduction of C++ types

Automatic deduction and understanding of template type

The following is a general example of a function template:

template <typename T>
void f(ParamType param);

f(expr);   // 对函数进行调用

The compiler must infer the types of T and ParamType based on expr. Special attention is that these two types may be different, because ParamType may contain modifiers, such as const and &.

Case 1: ParamType is a pointer or reference type

In the simplest case, ParamType is a pointer or reference type, but not a universal reference type (&&). At this point, the key points of type inference are: 1. If expr is a reference type, ignore the reference part; 2. Determine the type of T by subtracting the types of expr and ParamType.

Case 2: ParamType is a universal reference type (&&)

This situation is a bit complicated, because the form of universal reference type parameters and rvalue reference parameters are the same, but they are different. The former allows lvalue to be passed in. The rules for type inference are as follows: 1. If expr is an lvalue, both T and ParamType are deduced as lvalue references, even though they are formally rvalue references (in this case, only the && matching symbol is used. Once the match is an lvalue reference, then && Can be ignored). 2. If expr is an rvalue, it can be regarded as an rvalue reference in case 1.

Case 3: ParamType is neither a pointer nor a reference type

If ParamType is neither a reference type nor a pointer type, it means that the parameter of the function is passed by value. Passing by value means that param is a new copy of the passed object. Accordingly, the type inference rules are: 1. If the type of expr is a reference, then its reference attribute is ignored; 2. If the reference feature of expr is ignored, its It is a const type, so it is also ignored.

Because param is a new object, no matter how it changes, it will not affect the parameters passed in, so the reference attributes and const attributes are ignored. But there is a special case, when you put a pointer variable, there will be some changes. Although it is still a way of passing by value, copying is a pointer. Of course, changing the value of the pointer itself will not affect the value of the passed pointer, so the const attribute of the pointer can be ignored. But the property of the pointer to the constant cannot be ignored, because you can dereference through the copy of the pointer, and then modify the value pointed to by the pointer. The content pointed to by the original pointer will also change, but the original pointer points to a const object . Contradictions will arise, so this attribute cannot be ignored.

The following content is a condensed version of the notes, the detailed content comes from [C++11 new features-the difference and connection between auto and decltype](https://blog.csdn.net/y1196645376/article/details/51441503)

auto sum decltype

We want to infer the type of the variable to be defined from the expression, but we don't want to initialize the variable with the value of the expression. It is also possible that the return type of the function is the value type of an expression. At these times, auto seems weak, so C++11 introduces the second type specifier decltype, which is used to select and return the data type of the operand. In this process, the compiler just analyzes the expression and gets its type, but does not actually calculate the value of the expression.

decltype(f()) sum = x; // sum的类型就是函数f的返回值类型。 

For the expression used in decltype, if the variable name is added with a pair of parentheses, the type obtained may be different from that without the parentheses. If decltype uses a variable without parentheses, the result is the type of the variable. But if you add one or more parentheses to this variable, the compiler will treat this variable as an expression. The variable is a special expression that can be used as an lvalue, so such a decltype will return a reference type:

int i = 42; 
//decltype(i)   int  类型 
//decltype((i)) int& 类型 

Guess you like

Origin blog.csdn.net/XindaBlack/article/details/105828296