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

Инициировано этим ответом, который я читал в основные рекомендации:

C.45. Не определяйте конструктор по умолчанию, который только инициализирует элементы данных; вместо этого используйте инициализаторы членов класса

Приведенное рассуждение таково:

Причина

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

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

Плохой пример:

Example, bad

class X1 { // BAD: doesn't use member initializers
    string s;
    int i;
public:
    X1() :s{"default"}, i{1} { }
    // ...
};

Хороший пример - использование инициализаторов членов класса и отсутствие конструктора, объявленного пользователем:

Example

class X2 {
    string s = "default";
    int i = 1;
public:
    // use compiler-generated default constructor
    // ...
};

Что может сгенерированный компилятором конструктор сделать более эффективным, чем конструктор, предоставленный пользователем, в этом конкретном примере (или в любом другом примере)?

Не дает ли список инициализаторов те же возможности для оптимизации, что и классовые инициализаторы?


person 463035818_is_not_a_number    schedule 23.05.2019    source источник
comment
@ P.W это тесно связано, но не дублирует imho.   -  person 463035818_is_not_a_number    schedule 23.05.2019
comment
@ P.W, если подумать, может быть, это идеальный дубликат. Надо подумать и изучить еще немного;) не стесняйтесь отмечать   -  person 463035818_is_not_a_number    schedule 23.05.2019
comment
Фактический ответ - stackoverflow.com/a/4417899/10749452, не так ли?   -  person Benjamin Bihler    schedule 23.05.2019
comment
@BenjaminBihler В этом вопросе класс имеет NSDMI, что делает конструктор нетривиальным, поэтому соображения в этом ответе не относятся к этому вопросу.   -  person M.M    schedule 21.01.2021
comment
Обратите внимание, что основные рекомендации - это просто стилистические мнения небольшой группы людей, поэтому в этом вопросе есть элемент, основанный на мнении.   -  person M.M    schedule 21.01.2021
comment
@ M.M. ну, функция, сгенерированная компилятором, может быть более эффективной. утверждение, которое может быть правильным или неправильным. В самом общем смысле может быть правильно, что в определенных обстоятельствах что-либо может быть более эффективным, чем что-либо другое, но я не думаю, что это то, что они имели в виду, когда писали «Причину». Обратите внимание, что меня устраивало близкое дублирование, хотя я все еще немного озадачен, что они имели в виду, когда написание могло быть более эффективным.   -  person 463035818_is_not_a_number    schedule 21.01.2021


Ответы (2)


Короткий ответ

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

Я подозреваю, что может быть более эффективным, имеется в виду тот факт, что в целом он будет генерировать более оптимальный код, чем эквивалентный код, созданный разработчиком, который упускает такие возможности, как inline, constexpr и noexcept.

Длинный ответ

Важная особенность, которую выполняют конструкторы defaulted, заключается в том, что они интерпретируют и определяют правильный статус как для constexpr, так и для noexcept

Это то, что многие разработчики C ++ не указывают или могут указать неправильно. Поскольку основные рекомендации предназначены как для новых, так и для старых разработчиков C ++, вероятно, поэтому и упоминается оптимизация.

Статусы constexpr и noexcept могут по-разному влиять на генерацию кода:

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

  • noexcept может обеспечить лучшую сборку потребляющего кода, поскольку компилятор может предполагать, что никаких исключений не может произойти (и, следовательно, не требуется никакого кода для разворачивания стека). Кроме того, такие служебные программы, как шаблоны, которые проверяют std::is_nothrow_constructible..., могут генерировать более оптимальные пути кода.

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


Примеры в Основных рекомендациях не очень хорошо демонстрируют эту оптимизацию. Однако рассмотрим следующий пример, который иллюстрирует реалистичный пример, в котором defaulting может выиграть:

class Foo {
    int a;
    std::unique_ptr<int> b;
public:
    Foo() : a{42}, b{nullptr}{}
};

В этом примере верно следующее:

  • Конструкция Foo{} не постоянное выражение
  • Строительство Foo{} не noexcept

Сравните это с:

class Foo {
    int a = 42;
    std::unique_ptr<int> b = nullptr;
public:
    Foo() = default;
};

На первый взгляд, это то же самое. Но внезапно теперь меняется следующее:

  • Foo{} равно constexpr, поскольку std::nullptr_t конструктор constexpr (хотя std::unique_ptr не может использоваться в полном постоянном выражении)
  • Foo{} - это noexcept выражение

Вы можете сравнить созданную сборку с этим живым примером. Обратите внимание, что в случае default не требуется никаких инструкций для инициализации foo; вместо этого он просто назначает значения как константы через директиву компилятора (даже если значение не является постоянным).

Конечно, это тоже можно было написать:

class Foo {
    int a;
    std::unique_ptr<int> b;
public:
    constexpr Foo() noexcept :a{42}, b{nullptr};
};

Однако для этого требуется предварительное знание того, что Foo может быть одновременно constexpr и noexcept. Неправильный ответ может привести к проблемам. Что еще хуже, по мере развития кода состояние _39 _ / _ 40_ может стать некорректным - и это то, что default конструктор обнаружил бы.

Использование default также имеет дополнительное преимущество, заключающееся в том, что по мере развития кода оно может добавлять _43 _ / _ 44_ там, где это становится возможным, например, когда стандартная библиотека добавляет дополнительную поддержку constexpr. Последний пункт - это то, что в противном случае выполнялось бы вручную каждый раз, когда автор меняет код.


Мелочь

Если вы откажетесь от использования инициализаторов-членов класса, то стоит упомянуть еще один важный момент: в коде нет способа достичь тривиальности, если он не генерируется компилятором (например, через конструкторы defaulted).

class Bar {
    int a;
public:
    Bar() = default; // Bar{} is trivial!
};

Тривиальность предлагает совершенно иное направление потенциальных оптимизаций, поскольку тривиальный конструктор по умолчанию не требует никаких действий со стороны компилятора. Это позволяет компилятору полностью опускать Bar{}, если он видит, что объект позже перезаписывается.

person Human-Compiler    schedule 23.01.2021
comment
Ваше представление о тривиальности тоже пришло мне в голову, но теперь я думаю, что это неправда. Я прочитал это как означающее, что обе формы инициализации не являются банально. Некоторые тесты с std::is_trivial, кажется, подтверждают. (Возможно, я ошибаюсь, конечно) - person Drew Dormann; 23.01.2021
comment
@DrewDormann Вы правы - члены с инициализаторами нетривиальны, поэтому во втором наборе примеров инициализатор не используется. Однако, перечитывая вопрос, я понимаю, что в нем явно упоминаются инициализаторы. Ой! - person Human-Compiler; 23.01.2021
comment
Есть также некоторые семантические различия: тип класса с явно заданным по умолчанию конструктором (по умолчанию при первом объявлении) может быть агрегатом (до C ++ 20; при условии, что он выполняет другие ограничения для тип класса для агрегата), тогда как конструктор с конструктором предоставленный пользователем не может. - person dfrib; 25.01.2021
comment
@dfrib Хотя это правда, я не верю, что состояние агрегата на самом деле обеспечит какую-либо лучшую эффективность / оптимизацию, чем эквивалентный конструктор defaulted, о чем, в конечном счете, и идет речь. - person Human-Compiler; 25.01.2021
comment
@ Human-Compiler это, безусловно, является предметом спекуляций (/ незначительный эффект), но фигурная инициализация агрегатного класса означает инициализацию значений его членов, тогда как фигурная инициализация неагрегатного класса означает инициализацию по умолчанию его члены (при условии, что последний имеет пустой предоставленный пользователем ctor или внеклассный ctor с явно заданным по умолчанию). Для надуманного класса с элементом данных, который представляет собой, скажем, огромный массив фундаментального типа, скажем struct S { int arr_[1000000]; /* .... */ };, инициализация в скобках для S будет означать либо инициализацию нулем для ... - person dfrib; 25.01.2021
comment
... элемент данных arr_ или фактически no инициализация элемента данных arr_ (неопределенные значения) в зависимости от того, является ли S агрегатным классом или нет, соответственно. Последний мог, возможно (в этой области предположений) быть более эффективным, открывая дверь для UB, а также дверь для разработчиков, чтобы предположить, что у нас нет UB, и, возможно, выполнить последующие оптимизации, которые было бы невозможно, если бы элемент данных был инициализирован нулем. (Или, другими словами, он может допускать непреднамеренные ошибки, которые могут привести к опасным оптимизациям). - person dfrib; 25.01.2021

Я думаю, что важно предположить, что C.45 относится к константам (пример и исполнение):

Пример плохой

class X1 { // BAD: doesn't use member initializers
    string s;
    int i; public:
    X1() :s{"default"}, i{1} { }
    // ... };

Пример

 class X2 {
    string s = "default";
    int i = 1; public:
    // use compiler-generated default constructor
    // ... };

Правоприменение

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

Имея это в виду, легче обосновать (через C.48), почему мы должны предпочесть инициализаторы класса инициализаторам элементов в конструкторах для констант:

C.48: Предпочитайте инициализаторы класса инициализаторам членов в конструкторах для инициализаторов констант

Причина

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

Пример плохой

class X {   // BAD
    int i;         string s;
    int j; public:
    X() :i{666}, s{"qqq"} { }   // j is uninitialized
    X(int ii) :i{ii} {}         // s is "" and j is uninitialized
    // ... };

Как сопровождающий может узнать, был ли j намеренно неинициализирован (в любом случае это плохая идея) и было ли намеренно присвоить s значение по умолчанию в одном случае и qqq в другом (почти наверняка ошибка)? Проблема с j (забвение инициализировать член) часто возникает, когда новый член добавляется к существующему классу.

Пример

class X2 {
    int i {666};
    string s {"qqq"};
    int j {0}; public:
    X2() = default;        // all members are initialized to their defaults
    X2(int ii) :i{ii} {}   // s and j initialized to their defaults
    // ... };

Альтернатива: мы можем получить часть преимуществ от аргументов по умолчанию для конструкторов, и это не редкость в старом коде. Однако это менее явно, приводит к передаче большего количества аргументов и повторяется при наличии более одного конструктора:

class X3 {   // BAD: inexplicit, argument passing overhead
    int i;
    string s;
    int j; public:
    X3(int ii = 666, const string& ss = "qqq", int jj = 0)
        :i{ii}, s{ss}, j{jj} { }   // all members are initialized to their defaults
    // ... };

Правоприменение

(Simple) Every constructor should initialize every member variable (either explicitly, via a delegating ctor call or via default

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

person Jose    schedule 21.01.2021
comment
вы действительно правильно подметили. Как пишут еще и в C48 Избегает проблем с обслуживанием. Это приводит к самому короткому и эффективному коду. это говорит о том, что также в C45 они используют эффективный, как более простой в поддержке код, а не больше возможностей для компилятора. Обратите внимание, что это не дает ответа. Не дает ли список инициализаторов те же возможности для оптимизации, что и классовые инициализаторы? разница относительно оптимизации компилятора? Я начинаю верить, что нет. - person 463035818_is_not_a_number; 21.01.2021
comment
@ large_prime_is_463035818 Я тоже. Я действительно думаю, что оптимизация здесь означает меньшую подверженность ошибкам. C.48 говорит, что это более эффективно, но пример и альтернатива обращаются к разработчику ошибки вместо оптимизаций. - person Jose; 21.01.2021