Порядок инициализации наследуемых конструкторов

У меня есть этот пример:

#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}?

Я не уверен, произошло ли это, но не могу понять последовательность исполнения в данном случае. и где такое поведение необходимо в стандарте.

Мне трудно понять проблему, кажется, в вашем описании смешаны B1 и B2 (или i и j), не могли бы вы отредактировать? Также: «Но выходные данные этой программы показывают, что конструктор B1 (int) вызывается первым для инициализации элемента i значением 10», как выходные данные указывают на это? На самом деле я не вижу проблем с этой программой, она просто вызывает родительские конструкторы в порядке наследования и вызывает переопределенный конструктор с правильным параметром.

Nifil 10.07.2024 10:00

Он работает точно так, как указано в выводе: сначала инициализируется (по умолчанию) подобъект B2, затем конструктору B1 передается число 10. Сначала инициализируется B2, поскольку он упоминается первым в списке базовых классов.

molbdnilo 10.07.2024 10:01

Я думаю, что вопрос будет легче понять, если вы покажете ожидаемый результат и объясните, почему вы его ожидаете. Добавление вашего объяснения — это хорошо, но 4 строки вывода намного понятнее, чем большой текст.

463035818_is_not_an_ai 10.07.2024 10:09

То есть он ведет себя почти так же, как если бы у D был конструктор D(int x): B2(), B1(x) {}.

molbdnilo 10.07.2024 10:09

@molbdnilo B2 инициализируется первым, потому что он упоминается первым в списке базовых классов. Так как же инициализируется член B1::i перед подобъектом B2?

mada 10.07.2024 10:12

Примечание: не используйте макросы. Используйте шаблоны функций в C++

Pepijn Kramer 10.07.2024 10:12

Также хорошей практикой является явное указание конструкторов с одним аргументом, а использование множественного наследования, подобное этому, является недостатком дизайна. Итак, по сути, вы сталкиваетесь с проблемой из-за совместного применения некоторых плохих практик.

Pepijn Kramer 10.07.2024 10:13

Итак, как элемент B1::i инициализируется перед подобъектом B2? Как вы думаете, почему это происходит? Из вашего открытого вывода сначала вызывается B2::B2, как и должно быть.

Scheff's Cat 10.07.2024 10:15

@Scheff'sCat Как ты думаешь, почему это происходит? потому что член i сначала инициализируется значением 10 - возможно, это означает, что D d = D{10} сначала вызывает B1 ctor, чтобы инициализировать i с помощью 10

mada 10.07.2024 10:21

@mada Опять же: как вы думаете, почему i инициализируется первым? В выводе нет ничего, что указывало бы на то, что это так.

molbdnilo 10.07.2024 10:23

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

463035818_is_not_an_ai 10.07.2024 10:23

«может быть, это означает, что D d = D{10} сначала вызывает B1 ctor, чтобы инициализировать i значением 10...» Нет, B2 ctor используется первым. Это видно из первой строки вывода.

user12002570 10.07.2024 10:23
i всегда инициализируется значением 10, пока вызывается B1::B1(10). Вы получаете один и тот же результат независимо от того, выполняется ли конструктор первым или последним.
Weijun Zhou 10.07.2024 10:25

@user12002570 user12002570 Нет, сначала используется ctor B2. - Так почему j инициализируется значением 7, а не 10? - вызов B2 сначала инициализирует B2::j с помощью 10

mada 10.07.2024 10:26

Обратите внимание, что ctor по умолчанию вообще не наследуется от using до c++17.

user12002570 10.07.2024 10:26

@mada значение j равно 7, потому что вызывается конструктор B2, чтобы установить для него значение, как вам требуется с помощью объявления using. Порядок создания подклассов определяется порядком их помещения в класс. Поменяйте местами B1 и B2 в объявлении класса, и порядок печати изменится.

ALX23z 10.07.2024 10:31

Прочтите еще раз комментарий molbdnilo выше: «То есть он ведет себя так, как если бы D имел конструктор D(int x): B2(), B1(x) {}.». B2 вызывается первым, но 10 передается B1. Кажется, вы путаете способ передачи параметров с хронологическим порядком их вызова.

Weijun Zhou 10.07.2024 10:32

@user12002570 user12002570 - дайте мне знать порядок инициализации этих членов

mada 10.07.2024 10:34

Поправляю себя, конструкция более близка к D(int x): B2(7), B1(x) {}, поскольку вызывающая сторона заполняет аргументы по умолчанию. (Но порядок в списке инициализации не имеет значения, имеет значение только порядок в struct D : B2, B1.)

molbdnilo 10.07.2024 10:34

@mada Я понимаю твой вопрос и то, о чем ты здесь спрашиваешь. Вы спрашиваете, если сначала вызывается ctor B2, то почему 10 не используется для инициализации j. То есть, почему j не равно 10.

user12002570 10.07.2024 10:35

@user12002570 user12002570 - Да, это то, что я пытаюсь понять.

mada 10.07.2024 10:36

Возможно, на этот вопрос стоит ответить, но такие вещи, как «Вывод этой программы показывает, что конструктор B1(int) первым инициализирует член i значением 10». это просто неправильно. Предлагаю отредактировать, чтобы внести ясность.

Weijun Zhou 10.07.2024 10:37

@mada, у меня есть ответ. Это дано только в вашей цитируемой ссылке. Позвольте мне написать ответ, объясняющий вашу цитату в моем ответе.

user12002570 10.07.2024 10:37

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

Weijun Zhou 10.07.2024 10:47

если вы не вызываете конструктор базы явно, то он инициализируется конструктором по умолчанию. Вы не вызываете конструктор B2 явно. Возможно, удаление аргумента и превращение его в B2(): j(7) { print("B2 constructor"); } поможет прояснить ваше недоразумение. Это фактически то, что называется

463035818_is_not_an_ai 10.07.2024 10:48

@mada По сути, ctor класса, который вы наследуете, будет использовать параметр ctor D в качестве аргумента для собственного параметра ctor. В вашем примере это означает, что параметр D ctor будет передан в качестве аргумента B1 ctor. Если вместо этого вы унаследовали ctor B2, то параметр D ctor будет использоваться в качестве аргумента B2 ctor, как описано в моем ответе

user12002570 10.07.2024 10:49

или удалите значение по умолчанию, и вы получите сообщение об ошибке: B2(int _j): j(42) { print("B2 constructor"); }

463035818_is_not_an_ai 10.07.2024 10:49
Стоит ли изучать 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 называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
5
27
183
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Вывод этой программы показывает, что сначала вызывается конструктор B1(int) для инициализации элемента i значением 10. Затем перед выполнением тела B1(int) и я не знаю, как последовательность выполнения переходит к B2(int) )

Как вы пришли к такому выводу? Вывод программы указывает только на две вещи:

  • Конструктор B2 выполнялся раньше конструктора B1.
  • Когда вы создали экземпляр D{10}, был выбран конструктор B1 для передачи этого аргумента через выражение using в теле класса D.

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

«Я не знаю, что конкретно говорит стандарт…» Вопрос языковой законности. Так что это на самом деле не отвечает на вопрос.

user12002570 10.07.2024 10:17

В вопросе цитируется соответствующий раздел стандарта.

463035818_is_not_an_ai 10.07.2024 14:49
Ответ принят как подходящий

но я не могу понять последовательность выполнения

Здесь важно то, что «единственный» унаследованный 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 10.07.2024 11:06

@mada Может быть какой-то крайний случай. Но я не вижу здесь какого-либо крайнего случая. Возможно, лучше было бы задать новый вопрос для этого дополнительного вопроса. Пожалуйста. Возможно, на это ответит кто-то другой.

user12002570 10.07.2024 11:10

@mada Виртуальное наследование.

Weijun Zhou 10.07.2024 11:11

Это ожидаемо. Обратите внимание, что для создания 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!

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