In-depth understanding of C++ special member functions

In-depth understanding of C++ special member functions

In C++, there are the following 6 special member functions:

  • Constructor
  • destructor
  • copy constructor(copy constructor)
  • Assignment operator (copy operator)
  • Move constructor (introduced in c++11)
  • Move operator (introduced in c++11)

Taking the Widget class as an example, the signature of its special member function is as follows:

class Widget{
    
    
public:
    Widget();//构造函数
    ~Widget();//析构函数
    Widget(const Widget& rhs);//复制构造函数(拷贝构造函数)
    Widget& operator=(const Widget& rhs);//赋值运算符(拷贝运算符)
    Widget(Widget&& rhs);//移动构造函数
    Widget& operator=(Widget&& rhs);//移动运算符
}

What are the functions of each method and what should be paid attention to?

This article will explain these methods in detail.

Constructor

The function of the constructor is to helpcreate an instance of the object and initialize the instance.

In C++, the following two forms of statements will call the constructor of a class:

Widget widget;
Widget *w = new Widget();

Calling the constructor will create an instance object of the class. When a class has data members, it needs to write a constructor for the class and initialize the data members in the constructor.

For C++98, if a class does not have a set method, then it needs to create a constructor with parameters, as shown below:

#include <iostream>

class Widget
{
    
    
public:
    Widget(int width, int height):height_(height), width_(width){
    
    }
private:
    int height_;
    int width_;
};

int main()
{
    
    
    Widget w(1,1);
    return 0;
}

If a constructor with parameters is not created for it at this time, the values ​​of the members in the object created at this time will be random. Obviously, the object created in this way is not good.

#include<iostream>
using namespace std;

class Widget
{
    
    
public:
    int getHeight() const{
    
    
        return height_;
    }

    int getWidth() const{
    
    
        return width_;
    }
private:
    int height_;
    int width_;
};

int main()
{
    
    
    Widget w;
    std::cout << w.getHeight()<<std::endl;
    std::cout << w.getWidth()<<std::endl;   
    return 0;
}

But for standards after C++11, the initial value of a member can be defined in the class.

In this scenario, all objects created by this class will have the same initial value. If you want the initial values ​​of the created objects to be different, you still need to add a constructor with parameters.

#include<iostream>
using namespace std;

class Widget
{
    
    
public:
    int getHeight() const{
    
    
        return height_;
    }

    int getWidth() const{
    
    
        return width_;
    }
private:
    int height_{
    
    1};
    int width_{
    
    1};
};

int main()
{
    
    
    Widget w;
    std::cout << w.getHeight()<<std::endl;
    std::cout << w.getWidth()<<std::endl;   
    return 0;
}

destructor

The function of the constructor is to helpdestroy an instance.

This is easy to understand, but when do you need a custom destructor?

First look at the following class. Does this class need to write a custom destructor?

class Student{
    
    
public:
    Student(std::string name , int age, int id):name_(name), age_(age), id(id_){
    
    }//需要析构函数吗?
public:
    std::string getName() const{
    
    
        return name_;
    }
    int getAge() const{
    
    
        return age_;
    }
    int getId() const{
    
    
        return id_;
    }
private:
    std::string name_;
    int age_;
    int id_;
}

The answer is no. This Student class only contains three members. The default destructor will clean up these data, so there is no need to customize the destructor.

Take another look at the example below. Do you need to customize the destructor for it?

class Student{
    
    
public:
    Student(const char* s , std::size_t n)name_(new char[n]);{
    
    
        memcpy(name_, s, n);
    }
    //需要析构函数吗?
public:
    char* getName() const{
    
    
        return name_;
    }

private:
    char* name_{
    
    nullptr};
}

Obviously, this class requires a custom destructor. The default destructor will only set name_ to nullptr and will not release the memory space created by new.

Therefore, the above example needs to be transformed into the following form:

class Student{
    
    
public:
    Student(const char* s , std::size_t n)name_(new char[n]);{
    
    
        memcpy(name_, s, n);
    }
    ~Student(){
    
    
        if(name_){
    
    
            delete[] name_;
        }
    }
    //需要析构函数吗?
public:
    char* getName() const{
    
    
        return name_;
    }

private:
    char* name_{
    
    nullptr};
}

In fact, this class still has problems so far. The reason why will be explained when the copy operation is mentioned below.

Take another look at the example below. Do you need to customize the destructor for it?

class AsyncExec{
    
    
public:
    void exec(std::function<void()>& func){
    
    
        threadPtr_ = new std::thread(func);
    }
    //需要析构函数吗?
private
    std::thread* threadPtr_{
    
    nullptr};
}

Obviously, this class also needs a custom destructor. After calling the Exec method, an instance of the AsyncExec class contains a pointer internally, and its members are pointers of type std::thread. If it has not been detached, it must be joined, otherwise it will will terminate the program.

Therefore, the above example needs to be transformed into the following form:

class AsyncExec{
    
    
public
    ~AsyncExec(){
    
    
        if(threadPtr){
    
    
            threadPtr->join;
        }
        delete threadPtr;
    }
public:
    void exec(std::function<void()>& func){
    
    
        threadPtr_ = new std::thread(func);
    }
    //需要析构函数吗?
private
    std::thread* threadPtr_{
    
    nullptr};
}

Through the above two examples, we can basically find this rule:

Usually when a class needs tomanage some resources (raw pointers, threads, file descriptors, etc.), it usually needs to be written custom destructor because the behavior of the default destructor at this time is incorrect.

Next you need to understand a famousrule of three theorem. If a class requires the user to Defined destructor, user-defined copy constructor, or user Defined assignment operatorOne of the three, then it almost certainly requires these three functions.

For example the following example

#include <cstdint>
#include <cstring>

class Student{
    
    
public:
    Student(const char* s , std::size_t n) :name_(new char[n]){
    
    
        memcpy(name_, s, n);
    }
    explicit Student(const char* s = "")
        : Student(s, std::strlen(s) + 1) {
    
    }
    ~Student(){
    
    
        if(name_){
    
    
            delete[] name_;
        }
    }
public:
    char* getName() const{
    
    
        return name_;
    }

private:
    char* name_;
};

int main()
{
    
    
    Student s1("shirley");
    Student s2("tom");

    Student s3(s1);//(1)
    s2 = s1;//(2)
}

If you use the default copy constructor, a double free error will occur. Because the default copy constructor is a value copy, the name_ members of s1 and s3 point to the same memory at this time, and they will be destroyed repeatedly when s1 and s3 are destructed.

If you use the default assignment operator, there will not only be a double free problem, but also a memory leak. Since s2 is assigned the value of s1, the memory pointed to by the original name_ of s2 will no longer have a pointer pointing to it, resulting in a memory leak. Next, in the same way, the name_ members of s1 and s2 point to the same memory. When s1 and s2 are destroyed, they will be destroyed again.

The correct way to write it is to add a custom assignment constructor and a custom assignment operator while adding a custom destructor.

#include <cstdint>
#include <cstring>

class Student{
    
    
public:
    Student(const char* s , std::size_t n) :name_(new char[n]){
    
    
        memcpy(name_, s, n);
    }
    explicit Student(const char* s = "")
        : Student(s, std::strlen(s) + 1) {
    
    }
    ~Student(){
    
    
        if(name_){
    
    
            delete[] name_;
        }
    }
    Student(const Student& other) // II. copy constructor
        : Student(other.name_) {
    
    }
 
    Student& operator=(const Student& other) // III. copy assignment
    {
    
    
        if (this == &other)
            return *this;
 
        std::size_t n{
    
    std::strlen(other.name_) + 1};
        char* new_cstring = new char[n];            // allocate
        std::memcpy(new_cstring, other.name_, n); // populate
        delete[] name_;                           // deallocate
        name_ = new_cstring;
 
        return *this;
    }
public:
    char* getName() const{
    
    
        return name_;
    }

private:
    char* name_;
};

int main()
{
    
    
    Student s1("shirley");
    Student s2("tom");

    Student s3(s1);
    s2 = s1;
}

The writing method of this code in the assignment operator is mentioned in effective c++. This is done to ensure exception safety , this way of writing can ensure that if new fails, the data of the original object will not be destroyed.

    std::size_t n{
    
    std::strlen(other.name_) + 1};
    char* new_cstring = new char[n];            // allocate
    std::memcpy(new_cstring, other.name_, n); // populate
    delete[] name_;                           // deallocate
    name_ = new_cstring;

Copy constructor and assignment operator

The function of the copy constructor isto create a new object using an existing object.

The function of the assignment operator is to assignall member variables of one object to another object. Created object.

The difference between the two is that one iscreating a new object, and the other isassigning a value to an existing object Objects that exist.

In the following example, syntax (1) is to call the copy constructor, and syntax (2) is to call the assignment operator.

{
    
    
    Student s1("shirley");
    Student s2("tom");

    Student s3(s1);//(1)复制构造函数
    s2 = s1;//(2)赋值运算符
}

Let's review the Student class mentioned below and see what needs to be paid attention to when writing the correct copy constructor and assignment operator.

The function of the copy constructor is relatively simple, mainly copying members. If there is a pointer managed by the class, a deep copy is required.

Student(const Student& other) // II. copy constructor
    : Student(other.name_) {
    
    }

There are relatively many points to pay attention to when writing assignment operators.

  • First, add self-assignment judgment.
  • Secondly, since the assignment operator reassigns an existing object, the members of the original object need to be destroyed first.
  • Then the assignment of the member object needs to be processed. If there is a pointer managed by the class, a deep copy needs to be performed.
  • Finally, *this needs to be returned for continuous assignment.
Student& operator=(const Student& other) // III. copy assignment
{
    
    
    if (this == &other)
        return *this;

    std::size_t n{
    
    std::strlen(other.name_) + 1};
    char* new_cstring = new char[n];            // allocate
    std::memcpy(new_cstring, other.name_, n); // populate
    delete[] name_;                           // deallocate
    name_ = new_cstring;

    return *this;
}

When you do not provide a custom copy constructor and assignment operator, the compiler will create a default copy constructor and assignment operator, which will shallow the members Copy.

If your class does not manage resources, then a shallow copy may be appropriate. If your class manages certain resources (raw pointer, thread object, File descriptor, etc.), then there is a high probability that the default copy constructor and assignment operator are inappropriate.

But please note that sometimes, although a class member contains a raw pointer, it does not mean that the raw pointer is managed by the class.

For example, in the following example, the Client class has the handler_ pointer, but the life cycle of the pointer is not managed by the class. The class only uses the pointer. Therefore, in this scenario, there is no problem with shallow copy. The default copy is Constructors and assignment operators suffice.

#include <memory>
#include <functional>
#include <iostream>
#include <thread>
#include <future>

class IHandler
{
    
    
public:
    IHandler() = default;
    virtual ~IHandler() = default;
public:
    virtual void connect() = 0;
};

class TcpHandler  :public IHandler
{
    
    
public:
    TcpHandler() = default;
    virtual ~TcpHandler() = default;
public:
    void connect(){
    
    
        std::cout << "tcp connect" << std::endl;
    }
};

class UdpHandler : public IHandler
{
    
    
public:
    UdpHandler() = default;
    virtual ~UdpHandler() = default;
public:
    void connect() {
    
    
        std::cout << "udp connect" << std::endl;
    }
};

class Client{
    
    
public:
    Client(IHandler* handler):handler_(handler){
    
    };
    ~Client() = default;
public:
    void connect(){
    
    
        handler_->connect();
    }
private:
    IHandler* handler_{
    
    nullptr};
};

void process(IHandler* handler)
{
    
    
    if(!handler) return;

    Client client(handler);
    client.connect();
}
int main()
{
    
    
    IHandler* handler = new TcpHandler();
    process(handler);
    delete handler;
    handler = nullptr;
    handler = new UdpHandler();
    process(handler);   
    delete handler;
    handler = nullptr;
}

Therefore, when designing a class, you need to pay attention to whether the class manages resources or just uses resources. If you are managing resources, then there is a high probability that you need to customize the copy constructor and assignment operator .

Here we will mention againrule of three theorem, usually, if you needself When defining the destructor, you will most likely need to customize the copy constructor and < a i=7>Assignment operator.

Keep this in mind when you design a class to have such conditional reflections.

In fact, if you customize the destructor, the default copy constructor and The assignment operator can be deleted, but in the C++98 era, this point has not been taken seriously. In the C++11 era, this point is still not supported due to the difficulty of migrating old code. The compiler chooses to support the newly supported move constructor and move operator based on this consideration, that is, if a destructor is defined, the default move constructor and move operator will delete. This point will be continued below. explain.

Move constructor and move operator

Move semantics is widely used after C++11, which allows the ownership of an object to be transferred from one object to another without the need to copy the data. This transfer can be performed at any time during the object's life cycle and can be said to be a lightweight copy operation.

The move constructor and move operator are two methods that support move semantics in a class.

Regarding how to write move constructors and move operators, please refer to Microsoft's documentation for understanding.

Move constructor and move assignment operator

The following example is the C++ class MemoryBlock used to manage memory buffers.

// MemoryBlock.h
#pragma once
#include <iostream>
#include <algorithm>

class MemoryBlock
{
    
    
public:

   // Simple constructor that initializes the resource.
   explicit MemoryBlock(size_t length)
      : _length(length)
      , _data(new int[length])
   {
    
    
      std::cout << "In MemoryBlock(size_t). length = "
                << _length << "." << std::endl;
   }

   // Destructor.
   ~MemoryBlock()
   {
    
    
      std::cout << "In ~MemoryBlock(). length = "
                << _length << ".";

      if (_data != nullptr)
      {
    
    
         std::cout << " Deleting resource.";
         // Delete the resource.
         delete[] _data;
      }

      std::cout << std::endl;
   }

   // Copy constructor.
   MemoryBlock(const MemoryBlock& other)
      : _length(other._length)
      , _data(new int[other._length])
   {
    
    
      std::cout << "In MemoryBlock(const MemoryBlock&). length = "
                << other._length << ". Copying resource." << std::endl;

      std::copy(other._data, other._data + _length, _data);
   }

   // Copy assignment operator.
   MemoryBlock& operator=(const MemoryBlock& other)
   {
    
    
      std::cout << "In operator=(const MemoryBlock&). length = "
                << other._length << ". Copying resource." << std::endl;

      if (this != &other)
      {
    
    
         // Free the existing resource.
         delete[] _data;

         _length = other._length;
         _data = new int[_length];
         std::copy(other._data, other._data + _length, _data);
      }
      return *this;
   }

   // Retrieves the length of the data resource.
   size_t Length() const
   {
    
    
      return _length;
   }

private:
   size_t _length; // The length of the resource.
   int* _data; // The resource.
};

Create a move constructor for MemoryBlock

  • 1. Define an empty constructor method that takes an rvalue reference to the class type as a parameter, as shown in the following example:
MemoryBlock(MemoryBlock&& other)
   : _data(nullptr)
   , _length(0)
{
    
    
}
  • 2. In the move constructor, add the class data members from the source object to the object to be constructed:
_data = other._data;
_length = other._length;
  • 3. Assign the source object's data members to default values. This prevents the destructor from freeing resources (such as memory) multiple times:
other._data = nullptr;
other._length = 0;

Create move assignment operator for MemoryBloc class

  • 1. Define an empty assignment operator that takes an rvalue reference to a class type as a parameter and returns a reference to the class type, as shown in the following example:
MemoryBlock& operator=(MemoryBlock&& other)
{
    
    
}
  • 2. In the move assignment operator, add a conditional statement that does not perform the operation if you try to assign the object to itself.
if (this != &other)
{
    
    
}
  • 3. In the conditional statement, release all resources (such as memory) from the object to which it is assigned.

The following example releases the _data member from the object to which it is assigned:

// Free the existing resource.
delete[] _data;
  • 4. Perform steps 2 and 3 in the first procedure to transfer the data members from the source object to the object to be constructed:
// Copy the data pointer and its length from the
// source object.
_data = other._data;
_length = other._length;

// Release the data pointer from the source object so that
// the destructor does not free the memory multiple times.
other._data = nullptr;
other._length = 0;
  • 5. Return a reference to the current object, as shown in the following example:
return *this;

The complete MemoryBlock class looks like this:

#include <iostream>
#include <algorithm>

class MemoryBlock
{
    
    
public:

   // Simple constructor that initializes the resource.
   explicit MemoryBlock(size_t length)
      : _length(length)
      , _data(new int[length])
   {
    
    
      std::cout << "In MemoryBlock(size_t). length = "
                << _length << "." << std::endl;
   }

   // Destructor.
   ~MemoryBlock()
   {
    
    
      std::cout << "In ~MemoryBlock(). length = "
                << _length << ".";

      if (_data != nullptr)
      {
    
    
         std::cout << " Deleting resource.";
         // Delete the resource.
         delete[] _data;
      }

      std::cout << std::endl;
   }

   // Copy constructor.
   MemoryBlock(const MemoryBlock& other)
      : _length(other._length)
      , _data(new int[other._length])
   {
    
    
      std::cout << "In MemoryBlock(const MemoryBlock&). length = "
                << other._length << ". Copying resource." << std::endl;

      std::copy(other._data, other._data + _length, _data);
   }

   // Copy assignment operator.
   MemoryBlock& operator=(const MemoryBlock& other)
   {
    
    
      std::cout << "In operator=(const MemoryBlock&). length = "
                << other._length << ". Copying resource." << std::endl;

      if (this != &other)
      {
    
    
          // Free the existing resource.
          delete[] _data;

          _length = other._length;
          _data = new int[_length];
          std::copy(other._data, other._data + _length, _data);
      }
      return *this;
   }
    // Move constructor.
    MemoryBlock(MemoryBlock&& other) noexcept
    : _data(nullptr)
    , _length(0)
    {
    
    
        std::cout << "In MemoryBlock(MemoryBlock&&). length = "
                    << other._length << ". Moving resource." << std::endl;

        // Copy the data pointer and its length from the
        // source object.
        _data = other._data;
        _length = other._length;

        // Release the data pointer from the source object so that
        // the destructor does not free the memory multiple times.
        other._data = nullptr;
        other._length = 0;
    }

    // Move assignment operator.
    MemoryBlock& operator=(MemoryBlock&& other) noexcept
    {
    
    
        std::cout << "In operator=(MemoryBlock&&). length = "
                    << other._length << "." << std::endl;

        if (this != &other)
        {
    
    
            // Free the existing resource.
            delete[] _data;

            // Copy the data pointer and its length from the
            // source object.
            _data = other._data;
            _length = other._length;

            // Release the data pointer from the source object so that
            // the destructor does not free the memory multiple times.
            other._data = nullptr;
            other._length = 0;
        }
        return *this;
    }
   // Retrieves the length of the data resource.
   size_t Length() const
   {
    
    
        return _length;
   }

private:
   size_t _length; // The length of the resource.
   int* _data; // The resource.
};

It is worth mentioning that sometimes in order to reduce repeated code, the move operator can also be called in the move constructor, but you need to ensure that there will be no problems in doing so.

// Move constructor.
MemoryBlock(MemoryBlock&& other) noexcept
   : _data(nullptr)
   , _length(0)
{
    
    
    *this = std::move(other);
}

What will be introduced below is that if a class customizes one of the destructor, assignment constructor, and assignment operator, the default move constructor and move operator will be deleted. If an rvalue is used to construct an object, the compiler will call the copy constructor.

For example, MemoryBlock customizes the destructor, assignment constructor, and assignment operator, so the default move constructor and move operator will be deleted.

Even if you useMemoryBlock m2(std::move(m1));, it still calls the copy constructor.

#include <iostream>
#include <algorithm>

class MemoryBlock
{
    
    
public:

   // Simple constructor that initializes the resource.
   explicit MemoryBlock(size_t length)
      : _length(length)
      , _data(new int[length])
   {
    
    
      std::cout << "In MemoryBlock(size_t). length = "
                << _length << "." << std::endl;
   }

   // Destructor.
   ~MemoryBlock()
   {
    
    
      std::cout << "In ~MemoryBlock(). length = "
                << _length << ".";

      if (_data != nullptr)
      {
    
    
         std::cout << " Deleting resource.";
         // Delete the resource.
         delete[] _data;
      }

      std::cout << std::endl;
   }

   // Copy constructor.
   MemoryBlock(const MemoryBlock& other)
      : _length(other._length)
      , _data(new int[other._length])
   {
    
    
      std::cout << "In MemoryBlock(const MemoryBlock&). length = "
                << other._length << ". Copying resource." << std::endl;

      std::copy(other._data, other._data + _length, _data);
   }

   // Copy assignment operator.
   MemoryBlock& operator=(const MemoryBlock& other)
   {
    
    
      std::cout << "In operator=(const MemoryBlock&). length = "
                << other._length << ". Copying resource." << std::endl;

      if (this != &other)
      {
    
    
         // Free the existing resource.
         delete[] _data;

         _length = other._length;
         _data = new int[_length];
         std::copy(other._data, other._data + _length, _data);
      }
      return *this;
   }

   // Retrieves the length of the data resource.
   size_t Length() const
   {
    
    
      return _length;
   }

private:
   size_t _length; // The length of the resource.
   int* _data; // The resource.
};

int main()
{
    
    
    MemoryBlock m1(10);
    MemoryBlock m2(std::move(m1));
}

So this gave birth to another famous theoremrule of five theorem. That is, if you need to customize the move constructor and move operator, then there is a high probability that you need to customize 5 special functions (destructor, copy constructor, assignment operator, move constructor, move operator).

Here is another theoremrule of zero

  • 1. Classes should not define any special functions (copy/move constructors/assignments and destructors) unless they are classes dedicated to resource management.

In order to meet the single responsibility principle in design, this move separates data modules and functional modules at the code level to reduce coupling.

class rule_of_zero
{
    
    
    std::string cppstring;
public:
    rule_of_zero(const std::string& arg) : cppstring(arg) {
    
    }
};
  • 2. When the base class is inherited as a class that manages resources, the destructor may have to be declared as public virtual. Such behavior will destroy the move copy construction. Therefore, if the default function of the base class at this time should be set to default.

This is done to satisfy the coding principle that polymorphic classes prohibit copying in the C++ core guidelines.

class base_of_five_defaults
{
    
    
public:
    base_of_five_defaults(const base_of_five_defaults&) = default;
    base_of_five_defaults(base_of_five_defaults&&) = default;
    base_of_five_defaults& operator=(const base_of_five_defaults&) = default;
    base_of_five_defaults& operator=(base_of_five_defaults&&) = default;
    virtual ~base_of_five_defaults() = default;
};

Regarding this point, an example is still needed to deepen the impression:

#include <iostream>
#include <algorithm>
#include <vector>

class A{
    
    
public:
    A() {
    
    
        std::cout << "A()" << std::endl;
    };
    ~A() = default;
    A(const A& other){
    
    
        std::cout << "A(const A& other)" << std::endl;
    }
    A& operator=(const A& other){
    
    
        std::cout << "operator=(const A& other)" << std::endl;
        return *this;
    }
    A(A&& other){
    
    
        std::cout << "A(A&& other)" << std::endl;
    }
    A& operator=(A&& other){
    
    
        std::cout << "operator=(A&& other)" << std::endl;
        return *this;
    }
};

class DataMgr {
    
    
public:
    DataMgr(){
    
    
        val_.reserve(10);
    }
    virtual ~DataMgr() = default;
    // DataMgr(const DataMgr& other) = default;
    // DataMgr& operator=(const DataMgr& other) = default;
    // DataMgr(DataMgr&& other) = default;
    // DataMgr& operator=(DataMgr&& other) = default;

public:
    void push(A& a){
    
    
        val_.emplace_back(a);
    }
private:
    std::vector<A> val_;              //同之前一样
};

int main()
{
    
    
    A a1, a2;
    DataMgr s1;
    s1.push(a1);
    s1.push(a2);
    std::cout << "========" << std::endl;
    DataMgr s2 ;
    s2 = std::move(s1);
}

The running results here are as follows:

A()
A()
A(const A& other)
A(const A& other)
========
A(const A& other)
A(const A& other)

Although s2 = std::move(s1) move semantics are used here, but because the destructor is defined, the move operation is deleted, causing the copy constructor to be called. Just imagine if the amount of data in val_ here is large, then the running efficiency of the program will be very different.

Summarize

  • Special member functions are functions that may be automatically generated by the compiler, including the following six default constructors, destructors, copy constructors, assignment operators, move constructors, and move operators.
  • For constructors, if you need to customize the way to initialize members, you cannot use the default constructor and need to write a custom constructor.
  • For the destructor, if it manages resources internally (raw pointers, file descriptors, threads, etc.), you usually need to write a custom destructor. If you are just borrowing resources, you can usually use the default destructor.
  • According to the rule of three, the destructor is customized, and there is a high probability that you will also need to customize the copy constructor and assignment operator.
  • Default move operations are automatically generated only when the class does not explicitly declare a move operation, copy operation, or destructor. If you define a destructor or copy operation, the move operation at this time will call the copy constructor.
  • If a class does not explicitly define a copy constructor but explicitly defines a move constructor, the copy constructor is deleted. Similarly, if a class does not explicitly define an assignment operator but explicitly defines a move operator, the assignment operator will be deleted.
  • In daily development, try to explicitly indicate whether to use default special functions to avoid certain member functions from being deleted. If some methods do not need to be generated, they should be deleted.

Guess you like

Origin blog.csdn.net/qq_31442743/article/details/132560155