Поделиться через


Явно используемые по умолчанию и удаленные функции

В C++11 установленные по умолчанию и удаленные функции позволяют явным образом указывать, будут ли специальные функции-члены создаваться автоматически. Кроме того, удаленные функции определяют простой язык, который помогает предотвращать проблемы с повышением типов в аргументах любых функций — специальных функций-членов, а также обычных функций членов и функций, не являющихся членами. Такие проблемы могут приводить к ошибкам в вызовах функций.

Преимущества явным образом установленных по умолчанию и удаленных функций

Если в типе не определен конструктор по умолчанию, конструктор копии, оператор присваивания копии и деструктор для типа, то компилятор C ++ создает их автоматически. Эти функции известны как специальные функции-члены; именно благодаря им простые пользовательские типы в С++ действуют подобно структурам в С. Иными словами, их можно создавать, копировать и уничтожать без написания дополнительного кода. В C++11 в язык привносится семантика перемещения: для этого в список специальных функций-членов, которые компилятор может создавать автоматически, добавлены конструктор перемещения и оператор перемещения и присваивания.

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

  • Если конструктор был объявлен явным образом, то автоматическое создание конструктора не выполняется.

  • Если виртуальный деструктор был объявлен явным образом, то автоматическое создание деструктора не выполняется.

  • Если конструктор перемещения или оператор перемещения и присваивания был объявлен явным образом, то:

    • автоматическое создание конструктора копии не выполняется;

    • автоматическое создание оператора копирования и присваивания не выполняется.

  • Если конструктор копии, оператор копирования и присваивания, конструктор перемещения, оператор перемещения и присваивания или деструктор был объявлен явным образом, то:

    • автоматическое создание конструктора перемещения не выполняется;

    • автоматическое создание оператора перемещения и присваивания не выполняется.

Примечание

Кроме того, в стандарте C++11 определены следующие дополнительные правила.

  • Если конструктор копии или деструктор был объявлен явным образом, то автоматическое создание оператора копирования и присваивания не рекомендуется.

  • Если оператор копирования и присваивания или деструктор был объявлен явным образом, то автоматическое создание конструктора копии не рекомендуется.

В обоих случаях Visual Studio продолжит неявное автоматическое создание необходимых функций; предупреждение не создается.

Возможна утечка этих правил в иерархии объектов. Например, если по какой-либо причине в базовом классе конструктор по умолчанию, который может быть вызван из производного класса (то есть конструктор public или protected, который не принимает параметров), то класс, производный от него, не может автоматически создавать собственный конструктор по умолчанию.

Эти правила могут усложнить реализацию пользовательских типов и стандартных идиом С++, которые должны быть простыми и понятными. К примеру, если конструктор копии и оператор копирования и присваивания в пользовательском типе определены, но не объявлены, то такой тип будет некопируемым.

struct noncopyable
{
  noncopyable() {};
  
private:
  noncopyable(const noncopyable&);
  noncopyable& operator=(const noncopyable&);
};

В версиях до C++11 такой фрагмент кода был идиоматичной формой некопируемых типов. Однако здесь возникает ряд проблем.

  • Для того чтобы конструктор копии был скрыт, его необходимо объявлять как private, однако поскольку он в принципе был объявлен, автоматическое создание конструктора по умолчанию не выполняется. Если конструктор по умолчанию необходим, он должен быть определен явным образом, даже если он не выполняет никаких действий.

  • Даже если явно определенный конструктор по умолчанию не выполняет никаких действий, компилятор считает его нетривиальным. Это не так эффективно, как автоматическое создание конструктора по умолчанию, поскольку в этом случае тип noncopyable не может являться истинным типом POD.

  • Хотя конструктор копии и оператор копирования и присваивания скрыты от внешнего кода, однако функции-члены и дружественные функции для типа noncopyable все равно могут их видеть и вызывать. Если они объявлены, но не определены, при их вызове возникает ошибка компоновщика.

  • Хотя это и общепринятая идиома, однако если у вас нет четкого понимания всех правил автоматического создания специальных функций-членов, намерение будет неясным.

В C++11 идиому некопируемости можно реализовать более простым способом.

struct noncopyable
{
  noncopyable() =default;
  noncopyable(const noncopyable&) =delete;
  noncopyable& operator=(const noncopyable&) =delete;
};

Обратите внимание, как разрешаются проблемы с такими идиомами в версиях до C++11.

  • Создание конструктора по умолчанию по-прежнему можно предотвратить путем объявления конструктора копии, однако его можно восстановить, явным установив его по умолчанию.

  • Явно установленные по умолчанию специальные функции-члены по-прежнему считаются тривиальными, поэтому производительность не снижается, а тип noncopyable может быть истинным типом POD.

  • Конструктор копии и оператор копирования и присваивания являются открытыми, но удаленным. Определение или вызов удаленной функции представляет собой ошибку времени компиляции.

  • Намерение ясно всем, кто разобрался в сути ключевых слов =default и =delete. Понимать правила автоматического создания специальных функций-членов не требуется.

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

Явно установленные по умолчанию функции

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

Специальная функция-член устанавливается по умолчанию путем ее объявления, как показано в следующем примере.

struct widget
{
  widget()=default;

  inline widget& operator=(const widget&);
};

inline widget& widget::operator=(const widget&) =default;

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

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

Примечание

Visual Studio не поддерживает установленные по умолчанию конструкторы перемещения или операторы перемещения и присваивания, как требуется стандартом C ++11.Дополнительные сведения см. в подразделе "Установленные по умолчанию и удаленные функции" раздела Поддержка функций C++11 (современный C++).

Удаленные функции

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

struct widget
{
  // deleted operator new prevents widget from being dynamically allocated.
  void* operator new(std::size_t) =delete;
};

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

// deleted overload prevents call through type promotion of float to double from succeeding.
void call_with_true_double_only(float) =delete;
void call_with_true_double_only(double param) { return; }

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

template < typename T >
void call_with_true_double_only(T) =delete; //prevent call through type promotion of any T to double from succeeding.

void call_with_true_double_only(double param) { return; } // also define for const double, double&, etc. as needed.