Новий синтаксис "= за замовчуванням" в C ++ 11


136

Я не розумію, навіщо мені це робити:

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

Чому б просто не сказати:

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

навіщо вводити для цього новий синтаксис?


30
Nitpick: defaultце не нове ключове слово, це лише нове використання вже зарезервованого ключового слова.


Mey be Це питання може вам допомогти.
FreeNickname

7
На додаток до інших відповідей я також стверджую, що '= за замовчуванням;' є більше самодокументування.
Марк

Відповіді:


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. І в лясканні, і в 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, так новий (& 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? Я спробував прочитати всі відповіді тут, купити, я новачок у деяких c ++ речах, і у мене виникають багато проблем з його розумінням.
Сила Водолія

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

це означає, що можна мати = defaultі все ще aбуде грант =0? якимось чином? ви думаєте, я міг створити нове запитання на кшталт "як створити конструктор = defaultі надавати поля буде належним чином ініціалізовано?", btw У мене виникла проблема з a, structа не class, і додаток працює правильно навіть не використовуючи = default, я можу додайте мінімальну структуру в цьому питанні, якщо вона хороша :)
Power Aquarius Power

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.