Чем «= default» отличается от «{}» для конструктора и деструктора по умолчанию?

169

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

Если я хочу дать моему классу деструктор, который является виртуальным, но в остальном такой же, как и тот, который генерирует компилятор, я могу использовать =default:

class Widget {
public:
   virtual ~Widget() = default;
};

Но кажется, что я могу получить тот же эффект с меньшим количеством печати, используя пустое определение:

class Widget {
public:
   virtual ~Widget() {}
};

Есть ли способ, по которому эти два определения ведут себя по-разному?

Судя по ответам на этот вопрос, ситуация для конструктора по умолчанию выглядит аналогично. Учитывая, что для деструкторов почти нет различий в значении между " =default" и " {}", существует ли аналогичное различие в значении между этими опциями для конструкторов по умолчанию? То есть, если я хочу создать тип, в котором объекты этого типа будут создаваться и уничтожаться, почему я хочу сказать

Widget() = default;

вместо того

Widget() {}

?

Я прошу прощения, если расширение этого вопроса после первоначальной публикации нарушает некоторые правила SO. Размещение почти идентичного вопроса для конструкторов по умолчанию показалось мне менее желательным вариантом.

KnowItAllWannabe
источник
1
Не то, что я знаю, но = defaultэто более явно imo и согласуется с поддержкой его конструкторами.
Крис
11
Я не знаю наверняка, но я думаю, что первое соответствует определению «тривиальный деструктор», а второе - нет. Так std::has_trivial_destructor<Widget>::valueи trueдля первого, но falseдля второго. Что это значит, я тоже не знаю. :)
GManNickG
10
Виртуальный деструктор никогда не бывает тривиальным.
Люк Дантон
@LucDanton: Полагаю, открывать глаза и смотреть на код тоже будет хорошо! Спасибо за исправление.
GManNickG
Связанный: stackoverflow.com/questions/20828907/…
Габриэль Стейплз

Ответы:

103

Это совершенно другой вопрос, когда мы спрашиваем о конструкторах, а не о деструкторах.

Если ваш деструктор virtual, то разница незначительна, как отметил Говард . Однако, если ваш деструктор не виртуален , это совсем другая история. То же самое верно и для конструкторов.

Использование = defaultсинтаксиса для специальных функций-членов (конструктор по умолчанию, конструкторы / назначения копирования / перемещения, деструкторы и т. Д.) Означает нечто очень отличное от простого выполнения {}. С последним функция становится «предоставленной пользователем». И это все меняет.

Это тривиальный класс по определению C ++ 11:

struct Trivial
{
  int foo;
};

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

Это:

struct NotTrivial
{
  int foo;

  NotTrivial() {}
};

Как следует из названия, это уже не тривиально. Он имеет конструктор по умолчанию, предоставленный пользователем. Неважно, если он пуст; Что касается правил C ++ 11, то это не может быть тривиальным типом.

Это:

struct Trivial2
{
  int foo;

  Trivial2() = default;
};

Опять же, как следует из названия, это тривиальный тип. Зачем? Потому что вы сказали компилятору автоматически генерировать конструктор по умолчанию. Следовательно, конструктор не "предоставляется пользователем". И, следовательно, тип считается тривиальным, поскольку у него нет предоставленного пользователем конструктора по умолчанию.

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

Николь Болас
источник
2
Таким образом, ключевой вопрос, по-видимому, заключается в том, является ли результирующий класс тривиальным, и в основе этой проблемы лежит различие между специальной функцией, объявленной пользователем (что имеет место для =defaultфункций), и предоставляемыми пользователем (что имеет место для {}) функциями. Как объявленные пользователем, так и предоставленные пользователем функции могут предотвращать создание других специальных функций-членов (например, объявленный пользователем деструктор предотвращает генерацию операций перемещения), но только предоставленная пользователем специальная функция делает класс нетривиальным. Правильно?
KnowItAllWannabe
@KnowItAllWannabe: Это общая идея, да.
Николь Болас
Я выбираю это как принятый ответ, только потому, что он охватывает как конструкторы, так и (со ссылкой на ответ Говарда) деструкторы.
KnowItAllWannabe
Кажется, здесь отсутствует слово «что касается правил C ++ 11, вы - права тривиального типа», я бы это исправил, но я не уверен на 100%, что было задумано.
Jcoder
2
= defaultкажется полезным для принуждения компилятора генерировать конструктор по умолчанию, несмотря на присутствие других конструкторов; конструктор по умолчанию не объявляется неявно, если предоставляются любые другие объявленные пользователем конструкторы.
bgfvdu3w
42

Они оба нетривиальны.

Они оба имеют одинаковую спецификацию noexcept в зависимости от спецификации noexcept для баз и членов.

Единственное отличие, которое я обнаружил, состоит в том, что если Widgetсодержит базу или член с недоступным или удаленным деструктором:

struct A
{
private:
    ~A();
};

class Widget {
    A a_;
public:
#if 1
   virtual ~Widget() = default;
#else
   virtual ~Widget() {}
#endif
};

Тогда =defaultрешение будет скомпилировано, но Widgetне будет разрушаемым типом. Т.е. если вы попытаетесь уничтожить a Widget, вы получите ошибку во время компиляции. Но если нет, у вас есть рабочая программа.

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

test.cpp:8:7: error: field of type 'A' has private destructor
    A a_;
      ^
test.cpp:4:5: note: declared private here
    ~A();
    ^
1 error generated.
Говард Хиннант
источник
9
Интересно: другими словами, =default;компилятор не будет генерировать деструктор, если он не используется, и, следовательно, не будет вызывать ошибку. Это кажется странным для меня, даже если это не обязательно ошибка. Я не могу себе представить, что такое поведение является обязательным в стандарте.
Ник Бугалис
"Тогда решение по умолчанию будет скомпилировано" Нет, не будет. Только что протестировано в
нано
Какое было сообщение об ошибке и какая версия VS?
Говард
35

Важное различие между

class B {
    public:
    B(){}
    int i;
    int j;
};

и

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

является то, что конструктор по умолчанию, определенный с помощью B() = default;, считается не определенным пользователем . Это означает, что в случае инициализации значения, как в

B* pb = new B();  // use of () triggers value-initialization

Будет иметь место специальный тип инициализации, который вообще не использует конструктор, а для встроенных типов это приведет к нулевой инициализации . В случае B(){}этого не произойдет. Стандарт C ++ N3337 § 8.5 / 7 говорит

Инициализировать значение объекта типа T означает:

- если T является (возможно, cv-квалифицированным) типом класса (раздел 9) с предоставленным пользователем конструктором (12.1), то вызывается конструктор по умолчанию для T (и инициализация является некорректной, если у T нет доступного конструктора по умолчанию) );

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

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

Например:

#include <iostream>

class A {
    public:
    A(){}
    int i;
    int j;
};

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

int main()
{
    for( int i = 0; i < 100; ++i) {
        A* pa = new A();
        B* pb = new B();
        std::cout << pa->i << "," << pa->j << std::endl;
        std::cout << pb->i << "," << pb->j << std::endl;
        delete pa;
        delete pb;
    }
  return 0;
}

возможный результат:

0,0
0,0
145084416,0
0,0
145084432,0
0,0
145084416,0
//...

http://ideone.com/k8mBrd

4pie0
источник
Тогда почему "{}" и "= default" всегда инициализируют std :: string ideone.com/LMv5Uf ?
Nawfel BGH
1
@nawfelbgh Конструктор по умолчанию A () {} вызывает конструктор по умолчанию для std :: string, так как это не POD-тип. Ctor по умолчанию для std :: string инициализирует его пустой строкой размера 0. Стандартный ctor для скаляров ничего не делает: объекты с автоматической продолжительностью хранения (и их подобъекты) инициализируются неопределенными значениями.
4pie0