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

Я недостаточно хорошо знаком с расположением памяти объектов, содержащих виртуальные базы, чтобы понять, почему следующее выглядит некорректно скомпилированным как clang, так и gcc. Это академическое упражнение, поэтому извините за легкомыслие memset() в конструкторе. Я тестирую Linux x86-64 с clang 7 и gcc 8.2:

#include <cstring>

struct A {
    A() { memset(this, 0, sizeof(A)); }

    int i;
    char a;
};

struct B { char b = 'b'; };
struct C : virtual B, A {};

char foo() {
    C c;
    return c.b;
}

При компиляции с -O2 -Wall -pedantic -std=c++17 оба компилятора создают следующую сборку без предупреждений:

foo():
    xor     eax, eax
    ret

Изменение C, чтобы не наследовать B виртуально, или изменение sizeof(A) на 5 или менее в вызове memset меняет вывод компилятора на возврат 'b', как я и ожидал:

foo():
    mov     al, 98     # gcc uses eax directly, here
    ret

Какова структура памяти C, когда она происходит от B виртуально/невиртуально, и ошибаются ли эти компиляторы, позволяя конструктору A обнулять члены другого базового класса? Я знаю, что макет не определен стандартом, но я ожидаю, что все реализации гарантируют, что конструктор класса не может мешать членам данных несвязанного класса, даже при использовании в множественном наследовании, подобном этому. Или хотя бы предупредить, что что-то подобное может произойти. (новое предупреждение gcc -Wclass-memaccess здесь не диагностируется).

Если дело доходит до того, что memset(this, 0, sizeof(A)) недействителен в конструкторе, то я ожидаю, что компиляторы либо не смогут скомпилировать, либо, по крайней мере, предупредят.

Ссылка: https://godbolt.org/z/OSQV1j


person John Drouhard    schedule 09.01.2019    source источник
comment
Почему компилятор отказывается компилировать ваш глючный код?   -  person curiousguy    schedule 09.01.2019
comment
Вы можете посмотреть макет, созданный вашим компилятором, с помощью чего-то вроде этого Demo. В этом случае компилятор использует заполнение A для размещения B в качестве оптимизации.   -  person Jarod42    schedule 09.01.2019
comment
Трудно диагностировать все возможные ошибки. (Кроме того, в некоторых случаях memset может быть допустимым, код может быть разделен на разные TU, что усложняет или даже делает невозможным проверку).   -  person Jarod42    schedule 09.01.2019
comment
@ Jarod42 memset в порядке при построении полного объекта (или элемента массива или подобъекта-члена).   -  person curiousguy    schedule 09.01.2019
comment
Вам нужен практический ответ или стандартный ответ?   -  person curiousguy    schedule 09.01.2019


Ответы (1)


Я недостаточно знаком с расположением памяти объектов, содержащих виртуальные базы

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

  • элемент массива
  • член класса
  • не виртуальный подобъект базового класса, который не имеет виртуального базового класса

которые все построены и представлены как законченный объект.

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

чтобы понять, почему следующее кажется неправильно скомпилированным как clang, так и gcc.

Вы не опубликовали никаких доказательств плохой генерации кода.

Это академическое упражнение, так что извините за легкомыслие.

Это не легкомыслие, это совершенно неправильно.

memset() в конструкторе копирования.

Это уничтожает объект, перезаписывая его.

В коде используется неподдерживаемая операция (перезапись памяти объекта c2 во время построения), и компилятор не предупредил вас о том, что ваш код использует объект, время жизни которого было завершено вызовом низкоуровневого доступа к памяти. функция (memset). Завершение времени жизни внутри конструктора базового класса является незаконным: технически время жизни даже не началось, когда вы его заканчиваете.

Если вы хотите завершить жизненный цикл объекта, перезаписав его, сделайте это после построения.

Обзор:

Нет никакой гарантии, что каждый подобъект типа T "владеет" sizeof (T) байтами и может перезаписать их; однако это гарантируется для элементов и членов массива.

person curiousguy    schedule 09.01.2019
comment
Я знаю, что это неправильно. Я также знаю, что memset в конструкторе технически работает для классов, содержащих только типы POD. Конструкторы обычно могут предположить, что они могут инициализировать полный размер класса. Что я не понимаю, так это то, почему структура памяти класса C меняется, когда он использует виртуальное наследование (по сравнению с невиртуальным); похоже, что он упаковывает элементы по-разному, что делает предположение о размере недействительным. Вы коснулись этого в своем ответе, и это была та часть, которая меня больше всего интересовала. Если вы объясните, почему это опасно в контексте виртуального наследования, я приму ваш ответ. - person John Drouhard; 09.01.2019
comment
Кроме того, в самой последней части моего вопроса спрашивается, почему компиляторы хотя бы не предупреждают об обнаружении такой ситуации. В gcc есть -Wclass-memaccess, который кажется идеальной диагностикой, но это не так. - person John Drouhard; 09.01.2019
comment
Предупреждать, когда целью вызова функции необработанной памяти, такой как memset или memcpy, является объект типа класса, и при записи в такой объект может обойти нетривиальный или удаленный конструктор класса или присваивание копии Не уверен, предназначалось ли это для применения к коду внутри ctor. нарушение корректности константы здесь нет константы или инкапсуляции нет инкапсуляции внутри функции-члена или повреждены указатели виртуальной таблицы не полиморфный класс, поэтому нет vptr - person curiousguy; 10.01.2019
comment
gcc имеет -Wclass-memaccess, который кажется идеальной диагностикой, чтобы споткнуться здесь, но это не так. И с чего бы это? Как memset(this, 0, sizeof(A)); неправильно? Является ли memset в неполиморфном классе недопустимым по своей сути? У класса нет базового класса, нет виртуальной функции... - person curiousguy; 10.01.2019
comment
gcc имеет -Wclass-memaccess, который кажется идеальной диагностикой для срабатывания здесь, но это не так. А с чего бы это? Как работает memset(this, 0, sizeof(A)); неправильно? Является ли memset для неполиморфного класса недействительным по своей сути? Я считаю, что вы недвусмысленно сказали мне, насколько это неправильно и недействительно в вашем ответе. Цитирую: Это не легкомыслие, это явно неправильно. ... В коде используется неподдерживаемая операция (перезапись памяти объекта c2 во время построения) Если это так явно неправильно и не поддерживается, почему компилятор не предупреждает? - person John Drouhard; 10.01.2019
comment
Давайте продолжим обсуждение в чате. - person curiousguy; 10.01.2019