PVOID - Специальные члены

Этот раздел является переводом туториала C++ Language


[ПРИМЕЧАНИЕ: этот раздел требует хорошего понимания динамически выделяемой памяти]

Специальные функции-члены - это такие функции члены, которые неявно определены как члены класса при некоторых условиях. Всего их шесть:

 Функция-член  Типичная форма для класса C: 
 Конструктор по умолчанию  C::C();
 Деструктор  C::~C();
 Конструктор копирования  C::C (const C&);
 Присваивание копированием  C& operator= (const C&);
 Конструктор перемещения  C::C (C&&);
 Присваивание перемещением  C& operator= (C&&);

 Рассмотрим каждый из них.

Конструктор по умолчанию


   Конструктор по умолчанию - это конструктор, вызываемый, когда объекты класса объявлены, но не инициализированы какими-либо аргументами.

Если в определении класса нет конструкторов, компилятор предполагает, что класс имеет неявно определенный конструктор по умолчанию. Поэтому, после объявления класса, подобных этому:

class Example {
  public:
    int total;
    void accumulate (int x) { total += x; }
};

   Компилятор предполагает, что Example имеет конструктор по умолчанию. Таким образом, объекты этого класса могут быть созданы простым объявлением их без каких-либо аргументов:

Example ex;

   Но если класс имеет какой-либо конструктор, принимающий любое количество параметров и объявленный явным образом, то компилятор не предоставляет неявный конструктор по умолчанию и больше не позволяет объявлять объекты этого класса без аргументов. Например:

class Example2 {
  public:
    int total;
    Example2 (int initial_value) : total(initial_value) { };
    void accumulate (int x) { total += x; };
};

   Здесь мы объявили конструктор с параметром типа int. Поэтому следующее объявление объекта будет корректным:

Example2 ex (100);   // ok: вызывается конструктор 

В отличие от этого:

Example2 ex;   // неверно: отсутствует конструктор по умолчанию 

   Это будет некорректно, так как класс объявлен с явным конструктором, принимающим один аргумент, который заменяет неявный конструктор по умолчанию, не принимающий аргументов.

Поэтому если объекты этого класса должны создаваться без аргументов, то в классе должен быть объявлен соответствующий конструктор по умолчанию, например:

// classes and default constructors
#include <iostream>
#include <string>
using namespace std;

class Example3 {
    string data;
  public:
    Example3 (const string& str) : data(str) {}
    Example3 () {}
    const string& content() const {return data;}
};

int main () {
  Example3 foo;
  Example3 bar ("Example");

  cout << "bar's content: " << bar.content() << '\n';
  return 0;
}
bar's content: Example

   Здесь Example3 имеет конструктор по умолчанию (т.е. конструктор без параметров), определенный как пустой блок:

Example3 () {}

   Это позволяет создавать объекты класса Example3 без аргументов (как foo в этом примере). Обычно конструктор по умолчанию как этот неявно создается для всех классов, которые не имеют других конструкторов, и поэтому не требуется их явное определение. Но в данном случае Example3 имеет другой конструктор:

Example3 (const string& str);

И когда любой конструктор явно определен в классе, то конструктор по умолчанию не предоставляется автоматически.

 

   Деструктор 


   Деструкторы выполняют противоположную конструкторам функцию: они отвечают за необходимую очистку, которая нужна классу, когда время его жизни заканчивается. Классы, которые мы создавали в предыдущих разделах, не занимали каких-либо ресурсов и потому не нуждались в какой-либо очистке.

Но сейчас давайте представим, что класс в последнем примере выделяет динамическую память для хранения строки, которая является его членом. В этом случае будет полезно иметь функцию, вызываемую автоматически в конце жизни объекта, для освобождения этой памяти. Чтобы это сделать, мы используем деструктор. Деструктор - это метод, очень похожий на конструктор по умолчанию: он не принимает аргументов и ничего не возвращает, даже void. В качестве имени он также использует имя класса, перед которым ставится знак тильда (~):

// destructors
#include <iostream>
#include <string>
using namespace std;

class Example4 {
    string* ptr;
  public:
    // constructors:
    Example4() : ptr(new string) {}
    Example4 (const string& str) : ptr(new string(str)) {}
    // destructor:
    ~Example4 () {delete ptr;}
    // access content:
    const string& content() const {return *ptr;}
};

int main () {
  Example4 foo;
  Example4 bar ("Example");

  cout << "bar's content: " << bar.content() << '\n';
  return 0;
}
bar's content: Example

   При создании Example4 выделяет память для string. Эта память позже освобождается деструктором.

Деструктор для объекта вызывается в конце его жизни. В случае foo и bar это происходит в конце функции main().

Копирующий конструктор


Когда объект принимает именованный объект своего типа как аргумент, вызывается его копирующий конструктор для создания копии.

Копирующий конструктор - это конструктор, первый параметр которого имеет тип ссылки на собственный класс (возможно, объявленный как const) и который может быть вызван с одним аргументом этого типа. Например, для класса MyClass конструктор копирования может иметь следующую сигнатуру:

MyClass::MyClass (const MyClass&);

   Если класс не имеет других определенных копирующих или перемещающих конструкторов (или оператора присваивания), неявно предоставляется копирующий конструктор. Этот конструктор просто производит копию своих членов. Например, для такого класса, как этот:

class MyClass {
  public:
    int a, b; string c;
};

автоматически определяется копирующий конструктор явно. Предполагаемое определение этой функции производит поверхностное копирование, примерно похожее на:

MyClass::MyClass(const MyClass& x) : a(x.a), b(x.b), c(x.c) {}

   Этот стандартный копирующий конструктор может соответствовать нуждам многих классов. Но поверхностные копии это только копии самих членов класса, и это не то, что мы ожидаем для таких классов как Example4, определенный выше, потому что он содержит указатель, данные которого он обрабатывает. Для этого класса выполнение поверхностного копирования означает, что будет скопировано значение указателя, но не его содержание. Это означает, что оба объекта (копия и оригинал) будут разделять один объект string (они оба будут обращаться к одному и тому же объекту) и в какой-то момент (при уничтожении) оба объекта попытаются удалить один и тот же блок памяти, что, вероятно, приведет к краху программы во время выполнения. Это может быть решено путем определения следующего собственного копирующего конструктора, который производит глубокое копирование:

// copy constructor: deep copy
#include <iostream>
#include <string>
using namespace std;

class Example5 {
    string* ptr;
  public:
    Example5 (const string& str) : ptr(new string(str)) {}
    ~Example5 () {delete ptr;}
    // copy constructor:
    Example5 (const Example5& x) : ptr(new string(x.content())) {}
    // access content:
    const string& content() const {return *ptr;}
};

int main () {
  Example5 foo ("Example");
  Example5 bar = foo;

  cout << "bar's content: " << bar.content() << '\n';
  return 0;
}
bar's content: Example

   Глубокое копирование, производимое этим копирующим конструктором, выделяет память для новой строки, которая инициализируется для хранения копии оригинального объекта. В этом случае оба объекта (копия и оригинал) имеют различные копии данных, хранящиеся в разных местах.

 Копирующее присваивание


   Объекты копируются не только в конструкторе, в котором они инициализируются: они также могут быть скопированы операцией присваивания. Посмотрите на различия:

MyClass foo;
MyClass bar (foo); // инициализация объекта: вызывается копирующий конструктор
MyClass baz = foo; // инициализация объекта: вызывается копирующий конструктор
foo = bar; // объект уже инициализирован: вызывается копирующее присваивание

   Отметим, что baz инициализируется в конструкторе с использованием знака присваивания, но это не операция присваивания! (хотя это может так выглядеть): это просто другой синтаксис для вызова конструктора с одним аргументом.

Присваивание foo это операция присваивания. Здесь не объявляется объектов, но производится операция над существующим объектом foo.

Оператор копирующего присваивания это перегрузка оператора operator=, который принимает значение или ссылку на объект такого же класса как параметр. Возвращаемое значение это обычно ссылка на *this (хотя это и не требуется). Например, для класса MyClass копирующее присваивание может иметь следующую сигнатуру:

MyClass& operator= (const MyClass&);

   Оператор копирующего присваивания - это также специальная функция и он также обычно определен неявно, если класс не имеет собственных определенных копирующих или перемещающих операторов присваивания (или перемещающего конструктора).

Однако неявная версия производит поверхностное копирование, которое приемлемо для многих классов, но не для классов с указателями на хранимые ими объекты, как в случае с Example5. В этом случае не только класс подвергается риску удаления объекта, на который он указывает, дважды, но и создает утечки памяти, не удаляя объекты, на которые он указывал до присваивания. Эти проблемы могут быть решены использованием копирующего присваивания, которое удаляет старый объект и производит глубокое копирование:

Example5& operator= (const Example5& x) {
  delete ptr;                      // delete currently pointed string
  ptr = new string (x.content());  // allocate space for new string, and copy
  return *this;
}

   Или даже лучше, если его поле string не является константой, он может быть повторно использован тем же объектом string:

Example5& operator= (const Example5& x) {
  *ptr = x.content();
  return *this;
}

 

Перемещающие конструктор и присваивание


   Так же, как копирование, перемещение использует значение объекта для установки этого значения другому объекту. Но, в отличие от копирования, данные обычно перемещаются от одного объекта (источника) к другому (целевому объекту): источник теряет данные, которые перемещаются к целевому объекту. Это перемещение возможно только тогда, когда источник данных это безымянный объект.

   Безымянные объекты - это объекты, которые являются временными по природе и поэтому даже не имеют имени. Типичный пример безымянного объекта это возвращаемые значения функцией или преобразованием типа.

   Использование значения временных объектов как эти для инициализации других объектов  или присваивания им значения действительно не требует копии: объект никогда не будет использован где-либо еще и поэтому его значение может быть перемещено в целевой объект. Такие случаи инициируют перемещающий конструктор или перемещающее присваивание:

   Перемещающий конструктор вызывается, когда объект инициализируется при создании с использованием безымянного временного объекта. Перемещающее присваивание вызывается, когда объекту присваивается значение безымянного объекта:

MyClass fn();            // функция, возвращающая объект MyClass
MyClass foo;             // конструктор по умолчанию
MyClass bar = foo;       // копирующий конструктор
MyClass baz = fn();      // перемещающий конструктор
foo = bar;               // копирующее присваивание
baz = MyClass();         // перемещающее присваивание

   Значение, возвращаемое fn() и значение, создаваемое MyClass(), это безымянные временные объекты. В таких случаях нет необходимости создавать копию, потому что безымянные объекты существуют очень недолго и могут быть присвоены другим объектам в момент, когда это наиболее эффективно.

 Перемещающий конструктор и перемещающее присваивание - это методы, которые принимают параметр типа ссылки rvalue на объект своего собственного класса:

MyClass (MyClass&&);             // перемещающий конструктор
MyClass& operator= (MyClass&&);  // перемещающее присваивание

   Ссылка rvalue определяется передачей типа с двумя амперсандами (&&). Как параметр, ссылка rvalue соответствует аргументам временных объектов этого типа.

   Концепция перемещения наиболее полезна для объектов, которые управляют используемой ими памятью, такие, как объекты, выделяющие память с помощью new и удаляющие ее с помощью delete. Для таких объектов копирование и перемещение это действительно разные операции:

Копирование из A в B означает, что новая память выделяется для B и затем все содержимое A копируется в эту новую память, выделенную для B.

Перемещение из A в B означает, что память, выделенная для A перемещается в B без выделения новой памяти. Это совершается путем простого копирования указателя.

Например:

// перемещающее копирование/присваивание
#include <iostream>
#include <string>
using namespace std;

class Example6 {
    string* ptr;
  public:
    Example6 (const string& str) : ptr(new string(str)) {}
    ~Example6 () {delete ptr;}
    // перемещающий конструктор
    Example6 (Example6&& x) : ptr(x.ptr) {x.ptr=nullptr;}
    // перемещающее присваивание
    Example6& operator= (Example6&& x) {
      delete ptr; 
      ptr = x.ptr;
      x.ptr=nullptr;
      return *this;
    }
    // доступ к содержимому:
    const string& content() const {return *ptr;}
    // сложение:
    Example6 operator+(const Example6& rhs) {
      return Example6(content()+rhs.content());
    }
};

int main () {
  Example6 foo ("Exam");
  Example6 bar = Example6("ple");   // вызов перемещающего конструктора
  
  foo = foo + bar;                  // перемещающее присваивание

  cout << "foo's content: " << foo.content() << '\n';
  return 0;
}
foo's content: Example

   Компиляторы уже оптимизируют многие случаи, которые формально требуют перемещающего конструктора, что известно как оптимизация возвращаемого значения. Особенно, когда значение, возвращаемое функцией используется для инициализации объекта. В этих случаях перемещающий конструктор на самом деле может вообще никогда не вызываться.

  Отметим, что даже несмотря на то, что ссылки rvalue могут быть использованы для указания типа любого параметра функции, они редко бывают полезны для использования где-то, кроме перемещающего конструктора. Ссылки rvalue при использовании без необходимости могут быть источником ошибок, которые довольно сложно отследить.

Члены класса, определенные неявно


Шесть специальных методов, описанных выше, это члены, неявно объявленные в классе при определенных обстоятельствах:

Метод Неявно определен: Определение по умолчанию:
 Конструктор по умолчанию  если нет конструкторов  ничего не делает
 Деструктор  если нет деструктора   ничего не делает
 Копирующий конструктор  если нет перемещающего конструктора и перемещающего присваивания  копирует все члены
 Копирующее присваивание  если нет перемещающего конструктора и перемещающего присваивания  копирует все члены
 Перемещающий конструктор  если нет деструктора, копирующего конструктора и нет ни копирующего ни перемещающего  присваивания  перемещает все члены
 Перемещающее присваивание если нет деструктора, копирующего конструктора и ни копирующего, ни перемещающего присваивания  перемещает все члены

 

   Обратите внимание, что не все специальные методы неявно определяются в одних и тех же случаях. Это обычно делается для обратной совместимости со структурами языка Си и более ранними версиями языка C++. Фактически, многие из этих случаев являются устаревшими. К счастью, каждый класс может явно указать, какие из этих членов существует с определением по умолчанию, а какие удаляются с использованием ключевых слов default и delete соответственно. Их синтаксис:

function_declaration = default;
function_declaration = delete;

Например:

// неявные члены: default и delete
#include <iostream>
using namespace std;

class Rectangle {
    int width, height;
  public:
    Rectangle (int x, int y) : width(x), height(y) {}
    Rectangle() = default;
    Rectangle (const Rectangle& other) = delete;
    int area() {return width*height;}
};

int main () {
  Rectangle foo;
  Rectangle bar (10,20);

  cout << "bar's area: " << bar.area() << '\n';
  return 0;
}
bar's area: 200

   Здесь Rectangle может быть создан либо с двумя аргументами типа int, либо конструктором по умолчанию (без аргументов).

Он, однако, не может быть скопирован из другого объекта типаRectangle копирующим конструктором, потому что эта функция удалена. Поэтому, применимо к объекта в последнем примере, следующее выражение будет некорректным:

Rectangle baz (foo);

Он, однако, может быть создан явно путем определения копирующего конструктора:

Rectangle::Rectangle (const Rectangle& other) = default;

Что будет эквивалентно:

Rectangle::Rectangle (const Rectangle& other) 
  : width(other.width), height(other.height) {}

   Отметим, что ключевое слово default не определяет метод, эквивалентный конструктору по умолчанию (т.е. когда конструктор по умолчанию - это конструктор без параметров), но эквивалентен конструктору, который будет неявно определен, если не удален.

В основном, в т.ч. и для будущей совместимости, является хорошим тоном для классов, которые явно определяют один копирующий/перемещающий конструктор или один копирующий/перемещающий оператор присваивания , но не оба, определить такжеdelete или default для другого специального метода, который в нем не определен.