У меня есть этот пример:
#include <iostream>
#define print(X) std::cout << X << std::endl
struct B1 {
B1(int _i = 5): i(_i) { print("B1 constructor"); };
int i;
};
struct B2 {
B2(int _j = 7): j(_j) { print("B2 constructor"); }
int j;
};
struct D : B2, B1 {
using B1::B1;
};
int main(void)
{
D d = D{10};
print("B1::i = " << d.i);
print("B2::j = " << d.j);
}
Результат этой программы:
B2 constructor
B1 constructor
B1::i = 10
B2::j = 7
Согласно §11.9.4[class.inhctor.init]/1:
Когда конструктор типа B вызывается для инициализации объекта другой тип D (то есть, когда конструктор был унаследован ([namespace.udecl])), инициализация выполняется так, как если бы конструктор по умолчанию использовался для инициализации объекта D и каждой базы. подобъект класса, от которого был унаследован конструктор, за исключением того, что подобъект B инициализируется унаследованным конструктором, если подобъект базового класса должен был быть инициализирован как часть объекта D ([класс.base.init]). Вызов унаследованного конструктора, включая оценку любых аргументов, опускается, если B Подобъект не должен инициализироваться как часть объекта D. полной инициализацией считается один вызов функции; в в частности, если не опущено, инициализация унаследованного Параметры конструктора упорядочиваются перед инициализацией любого часть объекта D.
Во-первых, согласно §11.9.4/1
, поскольку D
наследует конструктор B1(int)
из базы B1
, этот унаследованный конструктор может инициализировать подобъект B1
; кроме того, параметр _i
полностью инициализируется перед инициализацией любой части D
, следовательно, унаследованный конструктор D::B1(int)
выбирается путем разрешения перегрузки, которое инициализирует элементы B1::i
с помощью mem-initializer-list.
Поскольку B2 constructor
печатается первым, это означает, что конструктор B2
вызывается раньше B1
. Итак, как элемент B2::j
инициализируется аргументом по умолчанию 7
, а не значением 10
, которое передается в вызове D{10}
?
Я не уверен, произошло ли это, но не могу понять последовательность исполнения в данном случае. и где такое поведение необходимо в стандарте.
Он работает точно так, как указано в выводе: сначала инициализируется (по умолчанию) подобъект B2
, затем конструктору B1
передается число 10. Сначала инициализируется B2
, поскольку он упоминается первым в списке базовых классов.
Я думаю, что вопрос будет легче понять, если вы покажете ожидаемый результат и объясните, почему вы его ожидаете. Добавление вашего объяснения — это хорошо, но 4 строки вывода намного понятнее, чем большой текст.
То есть он ведет себя почти так же, как если бы у D
был конструктор D(int x): B2(), B1(x) {}
.
@molbdnilo B2 инициализируется первым, потому что он упоминается первым в списке базовых классов. Так как же инициализируется член B1::i
перед подобъектом B2
?
Примечание: не используйте макросы. Используйте шаблоны функций в C++
Также хорошей практикой является явное указание конструкторов с одним аргументом, а использование множественного наследования, подобное этому, является недостатком дизайна. Итак, по сути, вы сталкиваетесь с проблемой из-за совместного применения некоторых плохих практик.
Итак, как элемент B1::i инициализируется перед подобъектом B2? Как вы думаете, почему это происходит? Из вашего открытого вывода сначала вызывается B2::B2
, как и должно быть.
@Scheff'sCat Как ты думаешь, почему это происходит? потому что член i
сначала инициализируется значением 10
- возможно, это означает, что D d = D{10}
сначала вызывает B1
ctor, чтобы инициализировать i
с помощью 10
@mada Опять же: как вы думаете, почему i
инициализируется первым? В выводе нет ничего, что указывало бы на то, что это так.
оба конструктора являются конструкторами по умолчанию, поскольку их можно вызывать без аргументов. Не знаю, в чем на самом деле ваше недопонимание, поэтому просто пытаюсь догадаться.
«может быть, это означает, что D d = D{10}
сначала вызывает B1
ctor, чтобы инициализировать i значением 10...» Нет, B2
ctor используется первым. Это видно из первой строки вывода.
i
всегда инициализируется значением 10, пока вызывается B1::B1(10)
. Вы получаете один и тот же результат независимо от того, выполняется ли конструктор первым или последним.
@user12002570 user12002570 Нет, сначала используется ctor B2. - Так почему j
инициализируется значением 7
, а не 10
? - вызов B2 сначала инициализирует B2::j
с помощью 10
Обратите внимание, что ctor по умолчанию вообще не наследуется от using
до c++17.
@mada значение j
равно 7
, потому что вызывается конструктор B2, чтобы установить для него значение, как вам требуется с помощью объявления using. Порядок создания подклассов определяется порядком их помещения в класс. Поменяйте местами B1 и B2 в объявлении класса, и порядок печати изменится.
Прочтите еще раз комментарий molbdnilo выше: «То есть он ведет себя так, как если бы D
имел конструктор D(int x): B2(), B1(x) {}
.». B2
вызывается первым, но 10
передается B1
. Кажется, вы путаете способ передачи параметров с хронологическим порядком их вызова.
@user12002570 user12002570 - дайте мне знать порядок инициализации этих членов
Поправляю себя, конструкция более близка к D(int x): B2(7), B1(x) {}
, поскольку вызывающая сторона заполняет аргументы по умолчанию. (Но порядок в списке инициализации не имеет значения, имеет значение только порядок в struct D : B2, B1
.)
@mada Я понимаю твой вопрос и то, о чем ты здесь спрашиваешь. Вы спрашиваете, если сначала вызывается ctor B2
, то почему 10
не используется для инициализации j
. То есть, почему j
не равно 10
.
@user12002570 user12002570 - Да, это то, что я пытаюсь понять.
Возможно, на этот вопрос стоит ответить, но такие вещи, как «Вывод этой программы показывает, что конструктор B1(int) первым инициализирует член i
значением 10». это просто неправильно. Предлагаю отредактировать, чтобы внести ясность.
@mada, у меня есть ответ. Это дано только в вашей цитируемой ссылке. Позвольте мне написать ответ, объясняющий вашу цитату в моем ответе.
Я не решался удалить этот абзац, но, поскольку вы удалили его сами, я думаю, что вопрос теперь в порядке, и я поддержу его.
если вы не вызываете конструктор базы явно, то он инициализируется конструктором по умолчанию. Вы не вызываете конструктор B2
явно. Возможно, удаление аргумента и превращение его в B2(): j(7) { print("B2 constructor"); }
поможет прояснить ваше недоразумение. Это фактически то, что называется
@mada По сути, ctor класса, который вы наследуете, будет использовать параметр ctor D
в качестве аргумента для собственного параметра ctor. В вашем примере это означает, что параметр D
ctor будет передан в качестве аргумента B1
ctor. Если вместо этого вы унаследовали ctor B2
, то параметр D
ctor будет использоваться в качестве аргумента B2
ctor, как описано в моем ответе
или удалите значение по умолчанию, и вы получите сообщение об ошибке: B2(int _j): j(42) { print("B2 constructor"); }
Вывод этой программы показывает, что сначала вызывается конструктор B1(int) для инициализации элемента i значением 10. Затем перед выполнением тела B1(int) и я не знаю, как последовательность выполнения переходит к B2(int) )
Как вы пришли к такому выводу? Вывод программы указывает только на две вещи:
D{10}
, был выбран конструктор B1 для передачи этого аргумента через выражение using
в теле класса D.Я не знаю, что конкретно говорит стандарт о порядке инициализации при наследовании от нескольких независимых базовых классов (и я не хочу делать здесь предположения, которые могут быть неверными), но ваша программа работает так, как я и ожидал. к.
«Я не знаю, что конкретно говорит стандарт…» Вопрос языковой законности. Так что это на самом деле не отвечает на вопрос.
В вопросе цитируется соответствующий раздел стандарта.
но я не могу понять последовательность выполнения
Здесь важно то, что «единственный» унаследованный ctor (B1::B1()
здесь) будет использовать параметр D
ctor для инициализации своего элемента данных.
Здесь это означает, что, поскольку вы унаследовали ctor B1
, параметр _i
в ctor D::D(int _i)
будет передан в качестве аргумента для инициализации i
подобъекта B1
.
В принципе, ваш класс кода' D
эквивалентен написанию:
struct D : public B2, public B1
{
inline D(int _i)
: B2(7)
//--vvvvvv------------->this is because you inherited B1's ctor
, B1(_i)
{
}
};
Это дано в цитируемом вами class.inhctor.init только то, что было выделено:
Когда конструктор типа B вызывается для инициализации объекта другого типа D (то есть, когда конструктор был унаследован ([namespace.udecl])), инициализация продолжается так, как если бы для инициализации объекта D использовался конструктор по умолчанию. и каждый подобъект базового класса, от которого был унаследован конструктор, за исключением того, что подобъект B инициализируется унаследованным конструктором, если подобъект базового класса должен был быть инициализирован как часть объекта D ([class.base.init]). Вызов унаследованного конструктора, включая оценку любых аргументов, опускается, если подобъект B не должен быть инициализирован как часть объекта D. Полная инициализация считается одним вызовом функции; в частности, если это не опущено, инициализация параметров унаследованного конструктора выполняется перед инициализацией любой части объекта D.
(выделено мной)
Вы можете дополнительно убедиться в этом, унаследовав ctor B2
, выполнив using B2::B2
. Тогда ваш класс D
будет эквивалентен:
struct D : public B2, public B1
{
inline D(int _j)
//--vvvvvv------------>this happens when you inherit B2's ctor
: B2(_j)
, B1(5)
{
}
};
Спасибо за Ваш ответ. Что-то относительно несущественное: в приведенной цитате: «... если подобъект базового класса должен быть инициализирован как часть объекта D...» - Почему if
здесь? Я имею в виду, есть ли случай, когда базовый подобъект не инициализируется как часть производного D
?
@mada Может быть какой-то крайний случай. Но я не вижу здесь какого-либо крайнего случая. Возможно, лучше было бы задать новый вопрос для этого дополнительного вопроса. Пожалуйста. Возможно, на это ответит кто-то другой.
@mada Виртуальное наследование.
Это ожидаемо. Обратите внимание, что для создания D
вам нужно вызвать конструкторы для всех предков, то есть это означает B1
и B2
. Порядок вызова этих конструкторов определяется порядком списка наследования. Это не старое правило C++, которое нужно соблюдать вечно.
Поэтому, когда создается D, вызывается конструктор по умолчанию для B2
, а затем вызывается конструктор для B1
с аргументом, который вы ему передали.
#include <print>
#include <source_location>
void log(int val, const std::source_location& from, const std::source_location& loc = std::source_location::current())
{
std::println("{}:{} val = {} invoked from: {}:{}", loc.function_name(), loc.line(), val, from.function_name(), from.line());
}
struct B1 {
B1(int _i = 5, const std::source_location& loc = std::source_location::current())
: i(_i)
{
log(i, loc);
};
int i;
};
struct B2 {
B2(int _j = 7, const std::source_location& loc = std::source_location::current())
: j(_j)
{
log(j, loc);
}
int j;
};
struct D : B2, B1 {
using B1::B1;
};
int main(void)
{
D d = D { 10 };
std::println("B1::i = {}", d.i);
std::println("B2::j = {}", d.j);
}
Вывод журнала:
B2::B2(int, const std::source_location &):22 val=7 invoked from: D::B1(int, const std::source_location &):28
B1::B1(int, const std::source_location &):13 val=10 invoked from: int main():33
B1::i = 10
B2::j = 7
Обратите внимание, что компилятор создал конструктор, который называется: D::B1
!
Мне трудно понять проблему, кажется, в вашем описании смешаны B1 и B2 (или i и j), не могли бы вы отредактировать? Также: «Но выходные данные этой программы показывают, что конструктор B1 (int) вызывается первым для инициализации элемента i значением 10», как выходные данные указывают на это? На самом деле я не вижу проблем с этой программой, она просто вызывает родительские конструкторы в порядке наследования и вызывает переопределенный конструктор с правильным параметром.