Учитывая следующий код (доступен на 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 говорит:
... и он [неявно сгенерированный конструктор] имеет тот же эффект, что и пользовательский конструктор с пустым телом и пустым списком инициализаторов.
Далее говорится, что
типы классов с пустым конструктором, предоставленным пользователем, могут обрабатываться иначе, чем типы классов с неявно определенным конструктором по умолчанию во время инициализации значения.
Хотя это несколько двусмысленно, я подумал, что это может относиться к тому факту, что конструкторы, предоставляемые пользователем, делают класс уже нетривиально конструируемым.
Может ли кто-нибудь дать разъяснения по этому поводу, пожалуйста?
Это зависит от 1. того, как вы создаете объект, например. в каком объеме и используете ли вы C c;
, C c{};
или C c = C();
(все с разной семантикой), а в случае C c{};
также в версии C++. Поэтому, пожалуйста, покажите, как вы создаете объекты. Но, что важно, конструктор ни в коем случае не инициализируется data_
. Если вообще, то именно синтаксис инициализации приводит к инициализации data_
нулевым значением.
@user17732522 В демонстрации godbolt они показали, как создают экземпляр. Но им также следует опубликовать это здесь, на SO. Они используют C c{};
Да, извините, что не включил это, оно довольно большое, и думаю, будет проще предоставить его в виде ссылки. Изменение C c{}
на C c = C()
не меняет сборку. Конечно, можно сделать просто C c;
, но тогда потенциальные другие поля также останутся неинициализированными.
«Выполнение только C c; конечно, работает, но тогда потенциальные другие поля также останутся неинициализированными».: Нет, это инициализация с помощью конструктора по умолчанию, как вы хотите. Синтаксис с пустыми фигурными скобками и круглыми скобками инициализирует (потенциально) больше, чем то, что намереваются инициализировать ваши конструкторы. Вам следует использовать B b;
/C c;
, если для вас важно, чтобы ничего дополнительно не инициализировалось нулями.
@ARentalTV Мой ответ был ерундой. Извините, я еще не выпил утренний кофе. Я перепишу его позже.
@user17732522 user17732522 Я добавил объяснение наблюдаемого поведения. По сути, это связано с инициализацией значения и главным образом с тем, что B::B()
предоставляется пользователем, а C::C()=default
— нет.
тлдр; ctor B
предоставляется пользователем, поскольку он не используется по умолчанию при первом объявлении, в то время как ctor C
не предоставляется пользователем. В результате объект c
сначала инициализируется нулем, поэтому мы видим дополнительный ассемблерный код для c
, как описано ниже.
Поведение программы можно понять с помощью dcl.init.
Прежде всего обратите внимание, что оба B b{};
и C c{};
являются инициализацией списка, что имеет следующий эффект:
- Инициализация списка объекта или ссылки типа 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 user17732522 Да, я это видел, поэтому взял рабочий проект C++26, чтобы ответить на последний случай.
Да в godbolt это понятно из сборки. Я отредактирую вопрос, включив это. Также будет отредактировано определенное вручную значение, чтобы быть более точным.