Новый синтаксис "= default" в С++ 11

Я не понимаю, зачем мне это делать

struct S { 
    int a; 
    S(int aa) : a(aa) {} 
    S() = default; 
};

Почему бы просто не сказать:

S() {} // instead of S() = default;

зачем вводить новый синтаксис для этого?

Ответ 1

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

§12.1/6 [class.ctor] Конструктор по умолчанию, который по умолчанию и не определен как удаленный, неявно определяется, когда он является odr, используется для создания объекта его типа класса или когда он явно дефолт после его первого объявления, Неявно определенный конструктор по умолчанию выполняет набор инициализаций класса, который будет выполняться написанным пользователем конструктором по умолчанию для этого класса без инициализатора ctor (12.6.2) и пустой составной инструкции. [...]

Однако, хотя оба конструктора будут вести себя одинаково, предоставление пустой реализации влияет на некоторые свойства класса. Предоставление определяемого пользователем конструктора, даже если оно ничего не делает, делает тип не совокупным, а также не тривиальным. Если вы хотите, чтобы ваш класс представлял собой совокупность или тривиальный тип (или транзитивность, тип POD), тогда вам нужно использовать = default.

§8.5.1/1 [dcl.init.aggr] Агрегат - это массив или класс без конструкторов, предоставляемых пользователем, [и...]

§12.1/5 [class.ctor] Конструктор по умолчанию является тривиальным, если он не предоставляется пользователем и [...]

§9/6 [класс] Тривиальный класс - это класс, имеющий тривиальный конструктор по умолчанию и [...]

Чтобы продемонстрировать:

#include <type_traits>

struct X {
    X() = default;
};

struct Y {
    Y() { };
};

int main() {
    static_assert(std::is_trivial<X>::value, "X should be trivial");
    static_assert(std::is_pod<X>::value, "X should be POD");

    static_assert(!std::is_trivial<Y>::value, "Y should not be trivial");
    static_assert(!std::is_pod<Y>::value, "Y should not be POD");
}

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

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

Ответ 2

n2210 содержит некоторые причины:

Управление дефолтами имеет несколько проблем:

  • Определения конструктора связаны; объявление любого конструктора подавляет конструктор по умолчанию.
  • По умолчанию деструктор не подходит для полиморфных классов, требующих явного определения.
  • Как только значение по умолчанию подавлено, нет средств для его восстановления.
  • Реализации по умолчанию часто более эффективны, чем описанные вручную реализации.
  • Нестандартные реализации нетривиальны, что влияет на семантику типов, например. делает тип не-POD.
  • Нельзя запрещать специальную функцию-член или глобальный оператор без объявления (нетривиальной) замены.

type::type() = default;
type::type() { x = 3; }

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

См. Правило-тройка становится Правилом пяти с С++ 11?:

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

Ответ 3

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

Для конструктора по умолчанию было бы возможным, чтобы любой конструктор по умолчанию с пустым телом считался кандидатом на тривиальный конструктор, так же как и с использованием =default. В конце концов, старые пустые конструкторы по умолчанию были законными С++.

struct S { 
  int a; 
  S() {} // legal C++ 
};

Независимо от того, является ли компилятор понятным, что этот конструктор является тривиальным, в большинстве случаев неактуальен вне оптимизации (ручной или компилятор).

Однако эта попытка обработать тела пустой функции как "по умолчанию" полностью разрушается для других типов функций-членов. Рассмотрим конструктор копирования:

struct S { 
  int a; 
  S() {}
  S(const S&) {} // legal, but semantically wrong
};

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

struct S { 
  int a; 
  S() {}
  S(const S& src) : a(src.a) {} // fixed
};
Тем не менее, даже в этом простом случае для компилятора становится намного сложнее проверить, что конструктор копирования идентичен тому, который он сгенерировал сам, или чтобы он увидел, что конструктор копирования тривиален (эквивалентен a memcpy, в основном). Компилятор должен будет проверить каждое выражение инициализатора каждого элемента и убедиться, что он идентичен выражению для доступа к соответствующему элементу источника и никому другому, убедитесь, что ни один из участников не оставлен с нетривиальной конструкцией по умолчанию и т.д. Он обращается в обратном порядке компилятор будет использовать для проверки того, что собственные генерируемые версии этой функции тривиальны.

Рассмотрим тогда оператор присваивания копии, который может стать еще более волосатым, особенно в нетривиальном случае. Это тонна котельной плиты, которую вы не хотите писать для многих классов, но вы все равно вынуждены в С++ 03:

struct T { 
  std::shared_ptr<int> b; 
  T(); // the usual definitions
  T(const T&);
  T& operator=(const T& src) {
    if (this != &src) // not actually needed for this simple example
      b = src.b; // non-trivial operation
    return *this;
};

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

Это снова вопрос согласованности. Пустое тело означает "ничего не делать", но для таких вещей, как конструкторы копирования, вы действительно не хотите "ничего не делать", а скорее "делайте все, что обычно делаете, если не подавляете". Следовательно, =default. Это необходимо для преодоления подавленных функций, созданных компилятором, таких как конструкторы копирования/перемещения и операторы присваивания. Затем это просто "очевидно", чтобы заставить его работать и для конструктора по умолчанию.

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

Есть еще несколько вещей, которые создадут функции-члены, созданные компилятором, и вы должны явно внести изменения в поддержку. Одним из примеров является поддержка constexpr для конструкторов по умолчанию. Просто умнее использовать =default, чем добавлять функции со всеми другими специальными ключевыми словами и такими, которые подразумеваются под =default, и это была одна из тем С++ 11: сделать язык проще. У него по-прежнему много бородавок и компромиссов с обратной связью, но ясно, что это большой шаг вперед от С++ 03, когда дело доходит до простоты использования.

Ответ 4

У меня есть пример, который покажет разницу:

#include <iostream>

using namespace std;
class A 
{
public:
    int x;
    A(){}
};

class B 
{
public:
    int x;
    B()=default;
};


int main() 
{ 
    int x = 5;
    new(&x)A(); // Call for empty constructor, which does nothing
    cout << x << endl;
    new(&x)B; // Call for default constructor
    cout << x << endl;
    new(&x)B(); // Call for default constructor + Value initialization
    cout << x << endl;
    return 0; 
} 

Выход:

5
5
0

Как мы видим, вызов пустого конструктора A() не инициализирует члены, а B() делает это.