C++ interview eight-part essay

C++11

  • auto and decltype
  • rvalue reference
  • List initialization
    C++14
    C++17
    C++20
    Continually updated…

auto and decltype

Type derivation: C++11 refers to the auto and decltype keywords, which can be used to deduce variables or expressions atcompile time type.

auto

Derivation rules:

  • The type of the variable is deduced from the type on the right side of =:auto a = 10; // 10是int型,可以自动推导出a是int

Derivation restrictions:

  • The use of auto must be initialized immediately, otherwise the type cannot be deduced:auto e; // error,使用auto必须马上初始化,否则无法推导类型

  • When auto defines multiple variables in one line, the derivation of each variable must not be ambiguous, otherwise the compilation will fail:auto d = 0, f = 1.0; // error,0和1.0类型不同,对于编译器有二义性,没法推导

  • auto cannot be used as a function parameter:void func(auto *value*) {} // error,auto不能用作函数参数

  • Auto cannot be used as non-static member variables in a class:

  • class A
    {
           
           
    	auto a = 1;				 // error,在类中auto不能用作非静态成员变量
    	static auto b = 1;		 // error,这里与auto无关,正常static int b = 1也不可以
    	static const auto c = 1; // ok
    };
    
  • auto cannot define arrays, but can define pointers:auto c[10] = a; // error,auto不能定义数组,可以定义指针

  • auto cannot deduce template parameters:

  • vector<int> d;
    vector<auto> f = d; // error,auto无法推导出模板参数
    
  • When not declared as a reference or pointer, auto ignores the reference type and const and volatile qualifications on the right side of the equal sign.

  • When declared as a reference or pointer, auto will retain the reference and const and volatile attributes on the right side of the equal sign.

  • int i = 0;
    auto *a = &i; // a是int*
    auto &b = i;  // b是int&
    auto c = b;	  // c是int,忽略了引用
    
    const auto d = i; // d是const int
    auto e = d;		  // e是int
    
    const auto &f = e; // f是const int&
    auto &g = f;	   // g是const int&
    

decltype

Derivation rules:

decltype is used to deduce the expression type. It is only used by the compiler to analyze the type of expression, and the expression will not actually be evaluated.

For decltype(exp) there is

  • exp is an expression, decltype(exp) has the same type as exp

  • exp is a function call, decltype(exp) has the same type as the function return value

  • In other cases,If exp is an lvalue, decltype(exp) is an lvalue reference of type exp

  • decltype retains expression references and const and volatile attributes

  • int a = 0, b = 0;
    decltype(a + b) c = 0; // c是int,因为(a+b)返回一个右值
    decltype(a += b) d = c;// d是int&,因为(a+=b)返回一个左值
    
    d = 20;
    cout << "c " << c << endl; // 输出c 20
    

Auto and decltype are used together

The purpose of the return value post-type syntax isTo solve the problem that the function return value type depends on the parameters but it is difficult to determine the return value type

template<typename T, typename U>
auto add(T t, U u) -> decltype(t + u) {
     
         
    return t + u;
}

rvalue reference

C++11 Quote

lvalue&rvalue

Concept 1 :

  • lvalue: Things that can be placed on the left side of the equal sign are called lvalues.

  • Rvalue: Things that cannot be placed on the left side of the equal sign are called rvalues.

Concept 2 :

  • lvalue: Anything that can take an address and has a name is an lvalue.

  • Rvalue: Something without a name that cannot take an address is an rvalue.

int a = b + c; // a是左值,有变量名,可以取地址,也可以放到等号左边, 表达式b+c的返回值是右值,没有名字且不能取地址,&(b+c)不能通过编译,而且也不能放到等号左边。
int a = 4;  // a是左值,4作为普通字面量是右值

lvalues ​​generally include:

  • Function names and variable names
  • function call that returns an lvalue reference
  • Prefixed self-increasing and self-decreasing expressions ++i, –i
  • Expressions (a=b, a += b, etc.) connected by assignment expressions or assignment operators
  • Dereference expression *p
  • String literal "abcd"

Privalue & Checkmate

Both pure rvalues ​​and checkmate values ​​are rvalues.

prvalue

Temporary variables generated by operation expressions, primitive literals not associated with objects, temporary variables returned by non-references, lambda expressions, etc. are all pure rvalues.

Example:

  • Literals other than string literals
  • Function call that returns a non-reference type
  • Post-increment and self-decrement expressions i++, i–
  • Arithmetic expression(a+b, a*b, a&&b, a==b, etc.)
  • Get address expression, etc.(&a)
dying value

The dying value refers to the new expression related to rvalue reference in C++11. usually refers to the object to be moved and the return of the T&& function value, the return value of the std::move function, the return value converted to T&& type conversion function, the dying value can be understood as the value that is about to be destroyed, through "stealing" The values ​​obtained through the memory space of other variables can avoid the release and allocation of memory space and extend the life cycle of variable values ​​when ensuring that other variables are no longer used or are about to be destroyed.Special tasks. move assignment or move construction, often used to complete

class A {
     
     
    xxx;
};
A a;
auto c = std::move(a); // c是将亡值
auto d = static_cast<A&&>(a); // d是将亡值

lvalue reference & rvalue reference

An lvalue reference is a type that refers to an lvalue;

An rvalue reference is a type that refers to an rvalue;

They are all references, both are aliases of objects, do not own the bound The heap of objects must be initialized immediately.

type &name = exp; // 左值引用
type &&name = exp; // 右值引用

lvalue reference

int a = 5;
int &b = a; // b是左值引用
b = 4;
int &c = 10; // error,10无法取地址,无法进行引用,非常量引用的初始值必须为左值
const int &d = 10; // ok,因为是常引用,引用常量数字,这个常量数字会存储在内存中,可以取地址

// 对于左值引用,等号右边的值必须可以取地址,如果不能取地址,则会编译失败,或者可以使用const引用形式,但这样就只能通过引用来读取输出,不能修改数组,因为是常量引用。

rvalue reference

// 如果使用右值引用,那表达式等号右边的值需要时右值,可以使用std::move函数强制把左值转换为右值。
int a = 4;
int &&b = a; // error, a是左值,无法将右值引用绑定到左值
int &&c = std::move(a); // ok

Deep copy & shallow copy

the difference

  • When no explicit copy constructor is defined, the system will call the default copy function—shallow copy, which can copy members one by one. When there are no pointers in the data members, shallow copy is feasible; butWhen there are pointers in the data members, if a simple shallow copy is used, the two pointers in the two classes will point to the same For an address, when the object is about to end, the destructor will be called twice, causing the pointer to hang. Therefore, at this time, a deep copy must be used.
  • The difference between deep copy and shallow copy is thatDeep copy will apply for additional space in the heap memory to store data, thus solving the problem of hanging pointers . In short, when there is a pointer in the data member, a deep copy must be used.
// 浅拷贝
class A {
     
     
public:
    A(int size) : size_(size) {
     
     
        data_ = new int[size];
    }
    A(){
     
     }
    A(const A& a) {
     
     
        size_ = a.size_;
        data_ = a.data_;
        cout << "copy " << endl;
    }
    ~A() {
     
     
        delete[] data_;
    }
    int *data_;
    int size_;
};
int main() {
     
     
    A a(10);
    A b = a;
    cout << "b " << b.data_ << endl;
    cout << "a " << a.data_ << endl;
    return 0;
}

/*
	输出:
	b 0x6b61a0
	a 0x6b61a0
*/
// 深拷贝
class A {
     
     
public:
    A(int size) : size_(size) {
     
     
        data_ = new int[size];
    }
    A(){
     
     }
    A(const A& a) {
     
     
        size_ = a.size_;
        data_ = new int[size_];  // 深拷贝
        cout << "copy " << endl;
    }
    ~A() {
     
     
        delete[] data_;
    }
    int *data_;
    int size_;
};
int main() {
     
     
    A a(10);
    A b = a;
    cout << "b " << b.data_ << endl;
    cout << "a " << a.data_ << endl;
    return 0;
}

/*
	输出:
	copy
	b 0x976b80
	a 0x976b10
*/

Deep copy means that when copying an object, if there are pointer references pointing to other resources inside the copied object, you need to create a new memory storage resource instead of simply assigning a value.

move semantics

Transfer ownership, the previous copy was to re-allocate a piece of memory to store the copied resources, and the move semantics are similar to transfer or resource theft. ,For that resource, it becomes owned by yourself, and others will no longer own it and will not use it again, through the new move semantics of C++11 It cansave a lot of copying burden. How to use move semantics? It is through the move constructor< a i=7>.

class A {
     
     
public:
    A(int size) : size_(size) {
     
     
        data_ = new int[size];
    }
    A(){
     
     }
    A(const A& a) {
     
     
        size_ = a.size_;
        data_ = new int[size_];
        cout << "copy " << endl;
    }
    A(A&& a) {
     
     
        this->data_ = a.data_;
        a.data_ = nullptr;
        cout << "move " << endl;
    }
    ~A() {
     
     
        if (data_ != nullptr) {
     
     
         delete[] data_;
        }
    }
    int *data_;
    int size_;
};
int main() {
     
     
    A a(10);
    A b = a;
    A c = std::move(a); // 调用移动构造函数
    return 0;
}

/*
	输出:
	copy
	move
*/

If you do not use std::move(), there will be a huge copy cost. Using move semantics can avoid many useless copies and improve program performance. All STL in C++ implement move semantics, which is convenient for us to use.

Move semantics only apply to objects of classes that implement move constructors. There is no optimization effect on basic types such as int and float, and they will still be copied because they do not have corresponding move constructors.

Perfect forwarding

Write a function template that accepts any arguments and forward it to other functions. The target function will receive exactly the same function as the forwarded function. Actual parameters, if the actual parameters of the forwarding function are lvalues, then the actual parameters of the target function are also lvalues. If the actual parameters of the forwarding function are rvalues, then the actual parameters of the target function are also rvalues. Use std::forward() to achieve perfect forwarding.

void PrintV(int &t) {
     
     
    cout << "lvalue" << endl;
}

void PrintV(int &&t) {
     
     
    cout << "rvalue" << endl;
}

template<typename T>
void Test(T &&t) {
     
     
    PrintV(t);
    PrintV(std::forward<T>(t));

    PrintV(std::move(t));
}

int main() {
     
     
    Test(1); // lvalue rvalue rvalue
    int a = 1;
    Test(a); // lvalue lvalue rvalue
    Test(std::forward<int>(a)); // lvalue rvalue rvalue
    Test(std::forward<int&>(a)); // lvalue lvalue rvalue
    Test(std::forward<int&&>(a)); // lvalue rvalue rvalue
    return 0;
}
  • Test(1): 1 is an rvalue. T &&t in the template is a universal reference. The rvalue 1 is passed to the Test function and becomes an rvalue reference. However, when PrintV() is called, t becomes lvalue, because it becomes a variable with a name, so lvalue is printed, and when PrintV(std::forward(t)), it will be forwarded perfectly and forwarded according to the original type, so rvalue is printed, PrintV(std: :move(t)) will undoubtedly print rvalue.
  • Test(a): a is an lvalue. T && in the template is a universal reference. The lvalue a is passed to the Test function and becomes an lvalue reference, so it is printed in the code.
  • Test(std::forward(a)): Whether forwarding is an lvalue or rvalue depends on T. If T is an lvalue, then it is forwarded as an lvalue. If T is an rvalue, then it is forwarded as an rvalue.

Return value optimization

Return Value Optimization (RVO) is a C++ compilation optimization technology.When a function needs to return an object instance, a temporary object is created and the target is copied through the copy constructor. The object is copied to the temporary object. There are copy constructors and destructors that will be called redundantly at a cost. However, through return value optimization, the C++ standard allows the omission of calling these copy constructors.

So when will the compiler perform return value optimization?

  • The value type of return is the same as the return value type of the function
  • What is returned is a local object
std::vector<int> return_vector(void) {
     
     
    std::vector<int> tmp {
     
     1,2,3,4,5};
    return tmp;
}

std::vector<int> rval_ref = return_vector();

RVO is triggered, no copying or moving is performed, and no temporary objects are generated.

List initialization

Initialize the object by adding an initialization list after the variable name.

struct A {
     
     
public:
   A(int) {
     
     }
private:
    A(const A&) {
     
     }
};
int main() {
     
     
   A a(123);
   A b = 123; // error
   A c = {
     
      123 };
   A d{
     
     123}; // c++11
   
   int e = {
     
     123};
   int f{
     
     123}; // c++11
   
   return 0;
}

List initialization can also be used on the return value of a function

std::vector<int> func() {
     
        
	return {
     
     };
}

First of all, let’s talk about the aggregation type that can be initialized directly by list. Here you need to know what isaggregation type:

  1. The type is an ordinary array, such as int[5], char[], double[], etc.

  2. The type is a class, and the following conditions are met:

    • No user-declared constructor
    • No user-provided constructors (allows display of prepared or discarded constructors)
    • No private or protected non-static data members
    • no base class
    • no virtual function
    • Non-static data members initialized directly without {} and =
    • No default member initializer
// 类A不是聚合类型,无法进行列表初始化,必须以自定义的构造函数来构造对象。
struct A {
     
     
   int a;
   int b;
   int c;
   A(int, int){
     
     }
};
int main() {
     
     
   A a{
     
     1, 2, 3};// error,A有自定义的构造函数,不能列表初始化
}
struct A {
     
     
int a;
   int b;
   virtual void func() {
     
     } // 含有虚函数,不是聚合类
};

struct Base {
     
     };
struct B : public Base {
     
      // 有基类,不是聚合类
int a;
   int b;
};

struct C {
     
     
   int a;
   int b = 10; // 有等号初始化,不是聚合类
};

struct D {
     
     
   int a;
   int b;
private:
   int c; // 含有私有的非静态数据成员,不是聚合类
};

struct E {
     
     
int a;
   int b;
   E() : a(0), b(0) {
     
     } // 含有默认成员初始化器,不是聚合类
};

For an aggregate type, using list initialization is equivalent to assigning a value to each element; for non-aggregate types, you need to customize a corresponding constructor first, and then list initialization will call the corresponding constructor.

Guess you like

Origin blog.csdn.net/kangjielearning/article/details/124561622
Recommended