Новый синтаксис «= default» в C ++ 11


136

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

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

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

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

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


30
Nitpick: defaultне новое ключевое слово, это просто новое использование уже зарезервированного ключевого слова.


Может быть, этот вопрос может помочь вам.
FreeNickname

7
В дополнение к другим ответам я бы также сказал, что '= default;' более самодокументируется.
Марк

Ответы:


136

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

§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) для каждой из этих специальных функций-членов упрощает чтение кода за счет явного указания вашего намерения.


Почти. 12.1 / 6: «Если этот пользовательский конструктор по умолчанию будет удовлетворять требованиям constexprконструктора (7.1.5), неявно определенный конструктор по умолчанию - это constexpr».
Кейси

На самом деле, 8.4.2 / 2 более информативен: «Если функция явно установлена ​​по умолчанию в своем первом объявлении, (а) она неявно считается constexprтаковой, если неявное объявление будет, (б) она неявно считается имеющей то же самое спецификация исключения, как если бы она была неявно объявлена ​​(15.4), ... "В этом конкретном случае это не имеет значения, но в целом foo() = default;имеет небольшое преимущество перед foo() {}.
Кейси

2
Вы говорите, что нет никакой разницы, а затем продолжаете объяснять различия?

@hvd В этом случае нет никакой разницы, потому что неявного объявления не было бы constexpr(поскольку элемент данных остается неинициализированным), а его спецификация исключения допускает все исключения. Я сделаю это яснее.
Джозеф Мэнсфилд

2
Спасибо за разъяснения. Тем не менее, похоже, что разница есть constexpr(о которой вы упомянули, здесь не должно иметь значения): выдает struct S1 { int m; S1() {} S1(int m) : m(m) {} }; struct S2 { int m; S2() = default; S2(int m) : m(m) {} }; constexpr S1 s1 {}; constexpr S2 s2 {};только s1ошибку, а не s2. И в clang, и в g ++.

10

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

#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 () делает это.


7
не могли бы вы объяснить этот синтаксис -> new (& x) A ();
Vencat

5
Мы создаем новый объект в памяти, начиная с адреса переменной x (вместо нового выделения памяти). Этот синтаксис используется для создания объекта в заранее выделенной памяти. Как и в нашем случае размер B = размер int, поэтому new (& x) A () создаст новый объект вместо переменной x.
Славенский

Спасибо за ваше объяснение.
Vencat

1
Я получаю разные результаты с gcc 8.3: ideone.com/XouXux
Adam.Er8

Даже с C ++ 14 я получаю разные результаты: ideone.com/CQphuT
Mayank Bhushan

9

N2210 приводит несколько причин:

Управление по умолчанию имеет несколько проблем:

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

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

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

Видите, Правило трех становится Правилом пяти с C ++ 11? :

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


1
Это причины наличия = defaultв целом, а не причины делать = defaultна конструкторе вместо того, чтобы делать { }.
Джозеф Мэнсфилд

@JosephMansfield Правда, но так как {}было уже особенность языка до начала введения =default, эти причины этого неявно опираются на различия (например , «нет средств , чтобы воскресить [подавленным по умолчанию] нет» означает , что {}это не эквивалентно по умолчанию ).
Кайл Стрэнд

7

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

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

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
};

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

Затем рассмотрим оператор присваивания копии, который может стать еще более привлекательным, особенно в нетривиальном случае. Это тонна изобилия, который вам не нужно писать для многих классов, но вы все равно будете вынуждены писать на C ++ 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(особенно когда мы бросаем операции перемещения в смесь). Мы не можем полагаться на пустое тело, означающее «заполнить значения по умолчанию», потому что пустое тело уже совершенно правильно и имеет ясное значение. Фактически, если бы пустое тело использовалось для обозначения «заполнения значений по умолчанию», то не было бы никакого способа явно создать конструктор копирования без операции или тому подобное.

Это опять вопрос последовательности. Пустое тело означает «ничего не делать», но для таких вещей, как конструкторы копирования, вы действительно не хотите «ничего не делать», а «делайте все, что вы обычно делаете, если не подавлены». Отсюда =default. Это необходимо для преодоления подавленных функций-членов, генерируемых компилятором, таких как конструкторы копирования / перемещения и операторы присваивания. Тогда просто «очевидно» заставить его работать и для конструктора по умолчанию.

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

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


У меня была проблема, где я ожидал = defaultбы сделать a=0;и не было! Я должен был отбросить это в пользу : a(0). Я все еще не понимаю, насколько это полезно = default, дело в производительности? это сломается где-нибудь, если я просто не использую = default? Я пробовал прочитать здесь все ответы, купить, я новичок в некоторых вещах о С ++, и у меня много проблем с их пониманием.
Водолей Сила

@AquariusPower: дело не только в производительности, но и в некоторых случаях в связи с исключениями и другой семантикой. А именно, оператор по умолчанию может быть тривиальным, но оператор, не заданный по умолчанию, никогда не может быть тривиальным, и некоторый код будет использовать методы метапрограммирования для изменения поведения или даже запрета типов с нетривиальными операциями. Ваш a=0пример из-за поведения тривиальных типов, которые являются отдельной (хотя и связанной) темой.
Шон Мидлдич

Значит ли это, что можно иметь = defaultи все же грант aбудет =0? каким-то образом? как вы думаете, я мог бы создать новый вопрос, например, «как создать конструктор = defaultи предоставить поля, которые будут правильно инициализированы?», кстати, у меня была проблема в a, structа не в a class, и приложение работает правильно, даже не используя = default, я могу добавьте минимальную структуру на этот вопрос, если он хороший :)
Водолей Сила

1
@AquariusPower: вы можете использовать нестатические инициализаторы элементов данных. Напишите свою структуру так: struct { int a = 0; };если вы затем решите, что вам нужен конструктор, вы можете использовать его по умолчанию, но обратите внимание, что тип не будет тривиальным (что нормально).
Шон Миддледич

2

Из-за устаревания std::is_podи его альтернативы std::is_trivial && std::is_standard_layoutфрагмент из ответа @JosephMansfield становится следующим:

#include <type_traits>

struct X {
    X() = default;
};

struct Y {
    Y() {}
};

int main() {
    static_assert(std::is_trivial_v<X>, "X should be trivial");
    static_assert(std::is_standard_layout_v<X>, "X should be standard layout");

    static_assert(!std::is_trivial_v<Y>, "Y should not be trivial");
    static_assert(std::is_standard_layout_v<Y>, "Y should be standard layout");
}

Обратите внимание, что по- Yпрежнему стандартного формата.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.