Почему неявно сгенерированный конструктор отличается от конструктора, предоставленного пользователем?

Учитывая следующий код (доступен на godbolt):

#include <memory>
#include <cstddef>

class A {
  public:
    A() : top_(data_) {}
  private:
    // Note: removing alignas removes the `rep stosq` from gcc output
    alignas(std::max_align_t) std::byte data_[1024];
    std::byte* top_;
};

class B {
public:
    B() {};
    void f();
private:
    A a;
};

class C {
public:
    C() = default;
    void f();
private:
    A a;
};

void f() {
    B b{}; // using B b = B() or B b; does not change the output
    b.f();
}

void g() {
   C c{}; // C c = C() does not change the output, C c; does
   c.f();
}

Две функции компилируются в следующее (в случае clang см. godbolt, где описаны другие компиляторы, все они генерируют инструкцию rep stos вместо memset):

f():
        sub     rsp, 1032
        lea     rdi, [rsp + 4]
        call    B::f()@PLT
        add     rsp, 1032
        ret

g():
        push    rbx
        sub     rsp, 1040
        lea     rbx, [rsp + 12]
        mov     edx, 1028
        mov     rdi, rbx
        xor     esi, esi
        call    memset@PLT
        mov     rdi, rbx
        call    C::f()@PLT
        add     rsp, 1040
        pop     rbx
        ret

В моем случае A — это распределитель стека, поэтому использование memset при создании довольно расточительно. Это происходит в GCC, Clang и MSVC, поэтому мне не хватает какого-то стандартного предложения. Даже замена data_ союзом этого не меняет.

Почему B и C инициализируются по-разному? cppreference говорит:

... и он [неявно сгенерированный конструктор] имеет тот же эффект, что и пользовательский конструктор с пустым телом и пустым списком инициализаторов.

Далее говорится, что

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

Хотя это несколько двусмысленно, я подумал, что это может относиться к тому факту, что конструкторы, предоставляемые пользователем, делают класс уже нетривиально конструируемым.

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

Да в godbolt это понятно из сборки. Я отредактирую вопрос, включив это. Также будет отредактировано определенное вручную значение, чтобы быть более точным.

ARentalTV 05.08.2024 09:27

Это зависит от 1. того, как вы создаете объект, например. в каком объеме и используете ли вы C c;, C c{}; или C c = C(); (все с разной семантикой), а в случае C c{}; также в версии C++. Поэтому, пожалуйста, покажите, как вы создаете объекты. Но, что важно, конструктор ни в коем случае не инициализируется data_. Если вообще, то именно синтаксис инициализации приводит к инициализации data_ нулевым значением.

user17732522 05.08.2024 09:31

@user17732522 В демонстрации godbolt они показали, как создают экземпляр. Но им также следует опубликовать это здесь, на SO. Они используют C c{};

user12002570 05.08.2024 09:37

Да, извините, что не включил это, оно довольно большое, и думаю, будет проще предоставить его в виде ссылки. Изменение C c{} на C c = C() не меняет сборку. Конечно, можно сделать просто C c;, но тогда потенциальные другие поля также останутся неинициализированными.

ARentalTV 05.08.2024 09:42

«Выполнение только C c; конечно, работает, но тогда потенциальные другие поля также останутся неинициализированными».: Нет, это инициализация с помощью конструктора по умолчанию, как вы хотите. Синтаксис с пустыми фигурными скобками и круглыми скобками инициализирует (потенциально) больше, чем то, что намереваются инициализировать ваши конструкторы. Вам следует использовать B b;/C c;, если для вас важно, чтобы ничего дополнительно не инициализировалось нулями.

user17732522 05.08.2024 09:46

@ARentalTV Мой ответ был ерундой. Извините, я еще не выпил утренний кофе. Я перепишу его позже.

user17732522 05.08.2024 10:18

@user17732522 user17732522 Я добавил объяснение наблюдаемого поведения. По сути, это связано с инициализацией значения и главным образом с тем, что B::B() предоставляется пользователем, а C::C()=default — нет.

user12002570 05.08.2024 10:20
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
7
101
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

тлдр; ctor B предоставляется пользователем, поскольку он не используется по умолчанию при первом объявлении, в то время как ctor C не предоставляется пользователем. В результате объект c сначала инициализируется нулем, поэтому мы видим дополнительный ассемблерный код для c, как описано ниже.


Поведение программы можно понять с помощью dcl.init.

Прежде всего обратите внимание, что оба B b{}; и C c{}; являются инициализацией списка, что имеет следующий эффект:

  1. Инициализация списка объекта или ссылки типа cv T определяется следующим образом:
  • 3.1
  • 3.2
  • 3.3
  • 3.4
  • 3.5 В противном случае В противном случае, если в списке инициализаторов нет элементов и T является типом класса с конструктором по умолчанию, объект инициализируется значением.

Это означает, что оба объекта будут инициализированы значением . Итак, мы переходим к инициализации значений:

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

  • Если T является типом класса (возможно, с указанием cv) ([class]), то пусть C будет конструктором, выбранным для инициализации объекта по умолчанию, если таковой имеется. Если C не предоставлен пользователем, объект сначала инициализируется нулем. Во всех случаях объект инициализируется по умолчанию.

Обратите внимание на акцент на , предоставленном пользователем . В частности, из dcl.fct.def.defaut мы видим, что только ctor B::B() предоставляется пользователем, поскольку в первом объявлении он не является значением по умолчанию.

Функция предоставляется пользователем, если она объявлена ​​пользователем и не задана явно по умолчанию или не удалена при первом объявлении.

Таким образом, ctor C::C()=default не предоставляется пользователем, и объект c сначала будет инициализирован нулем, а не объект b. Объект b не будет инициализирован нулем.

Как обсуждалось в комментариях под вопросом, это существенно отличается от C++20, где C был агрегатом, так что он инициализировал только значение подобъекта a, что, в свою очередь, означает отсутствие нулевой инициализации data_.

user17732522 05.08.2024 10:21

@user17732522 user17732522 Да, я это видел, поэтому взял рабочий проект C++26, чтобы ответить на последний случай.

user12002570 05.08.2024 10:22

Другие вопросы по теме