Когда S является тривиальным подклассом T, безопасно ли использовать массив S, где ожидается массив T?
Рассмотрим следующие объявления пары связанных структур. Класс потомков не добавляет никаких переменных-членов, и единственная функция-член - это конструктор, который ничего не делает, а перенаправляет все свои аргументы на конструктор базового класса.
struct Base {
Base(int a, char const* b):
a(a), b(b)
{ }
int a;
char const* b;
};
struct Descendant: Base {
Descendant(int a, char const* b):
Base(a, b)
{ }
};
Теперь рассмотрим следующий код, используя эти типы. Функция foo
ожидает получить массив Base
. Тем не менее, main
определяет массив Descendant
и передает это значение вместо foo
.
void foo(Base* array, unsigned len)
{
/* <access contents of array> */
}
int main()
{
Descendant main_array[] = {
Descendant(0, "zero"),
Descendant(1, "one")
};
foo(main_array, 2);
return 0;
}
Определено ли поведение этой программы? Ответ зависит от тела foo
, например, записывается ли он в массив или только читается из него?
Если sizeof(Derived)
не соответствует sizeof(Base)
, то поведение undefined соответствует ответам на предыдущий вопрос о базовом указателе на массив производных объектов. Есть ли вероятность, что объекты в этом вопросе будут иметь разные размеры?
Ответы
Ответ 1
Определено ли поведение этой программы? Отвечает ли ответ на тело foo, например, пишет ли он массиву или только читает его?
Я собираюсь опасаться ответа, говоря, что программа хорошо определена (пока foo
), даже если она написана на другом языке (например, C).
Если sizeof(Derived)
не соответствует sizeof(Base)
, тогда поведение undefined в соответствии с ответами на предыдущий вопрос о базовом указателе на массив производных объектов. Есть ли вероятность, что объекты в этом вопросе будут иметь разные размеры?
Я так не думаю. Согласно моему чтению стандарта (*) §9.2, раздел 17
Два типа стандартной структуры (раздел 9) являются совместимыми с макетами, если они имеют одинаковое количество нестатических члены данных и соответствующие нестатические элементы данных (в порядке объявления) имеют совместимость с макетами типы (3.9).
В пунктах 9-9 раздела 9 подробно описаны требования к компоновке макета:
7 Класс стандартного макета - это класс, который:
-
не содержит нестатических элементов данных класса нестандартного макета (или массива таких типов) или ссылки,
-
не имеет виртуальных функций (10.3) и виртуальных базовых классов (10.1),
-
имеет тот же контроль доступа (раздел 11) для всех нестатических членов данных,
-
не имеет базовых классов нестандартной компоновки,
-
либо не имеет нестатических членов данных в самом производном классе и не более одного базового класса с нестатическими членами данных, либо не имеет базовых классов с нестатическими членами данных, а
-
не имеет базовых классов того же типа, что и первый нестатический элемент данных.
8 Структура стандартного макета - это класс стандартного макета, определенный с помощью ключа класса struct
или ключа класса class
. Стандартное расположение макета - это класс стандартного макета, определенный с помощью ключа класса union
.
9 [Примечание. Стандартные классы макета полезны для общения с кодом, написанным на других языках программирования. Их макет указан в 9.2. - конечная нота]
Обратите внимание, что последнее предложение (в сочетании с §3.9) - согласно моему чтению, это гарантирует, что до тех пор, пока вы не добавляете слишком много "вещей на С++" (виртуальные функции и т.д. и тем самым нарушаете требование стандартного макета ) ваши структуры/классы будут вести себя как C-структуры с добавленным синтаксическим сахаром.
Кто-нибудь сомневался в законности, если Base
не имел конструктора? Я не думаю, что этот шаблон (полученный из структуры C, добавляющий конструктор/вспомогательные функции) является идиоматическим.
Я открыт для возможности, что я ошибаюсь и приветствую дополнения/исправления.
(*) Я действительно смотрю на N3290 здесь, но фактический стандарт должен быть достаточно близко.
Ответ 2
BEWARE! Хотя это почти наверняка верно в вашем компиляторе, это не гарантируется стандартом для работы.
По крайней мере добавить if (sizeof (Derived)!= sizeof (Base)) logAndAbort ( "несоответствие размера между Derived и Base" ); проверить.
В случае, если вам интересно, компиляторы, для которых это безопасно, являются единым целым, в котором размер не изменяется. В стандарте есть что-то, что позволяет производным классам быть несмежными с базовыми классами. Во всех случаях, когда это происходит, размер должен расти (по понятным причинам).
Ответ 3
Если вы объявите массив указателей на Base, тогда код будет работать правильно.
В качестве бонуса новый foo() будет безопасен для использования с каким-либо будущим подклассом Base, который имеет новые структуры данных.
void foo(Base **array, unsigned len)
{
// Example code
for(unsigned i = 0; i < len; ++i)
{
Base *x = array[i];
std::cout << x->a << x->b;
}
}
void do_something()
{
Base *data[2];
data[0] = new Base(1, "a");
data[2] = new Descendent(2, "b");
foo(data, 2);
delete data[0];
delete data[1];
}
Ответ 4
Добавление новых данных в класс Descendent
приведет к нарушению взаимозаменяемости Descendent[]
с помощью Base[]
. Для того чтобы какая-либо функция могла притворяться, что массив более крупных структур представляет собой массив меньших, но в противном случае совместимых структур, должен быть подготовлен новый массив, в котором лишние байты срезаны, и в этом случае невозможно определить поведение система. Что произойдет, если некоторые указатели отрезаны? Что произойдет, если состояние этих объектов должно изменяться как часть вызванной процедуры, а фактические объекты, к которым они относятся, не являются оригиналами?
В противном случае, если срез не происходит, а Base*
в Derived[]
был ++
ed, sizeof(Base)
будет добавлен к его двоичному значению, и он больше не будет указывать на Base*
. Очевидно, что нет никакого способа определить поведение системы в этом случае.
Зная, что использование этой идиомы НЕбезопасно, даже если стандарт и президент и Бог определяют его как работающий. Любое дополнение к Descendent
разбивает ваш код. Даже если вы добавите утверждение, будут выполняться функции, легитимность которых зависит от Base[]
взаимозаменяемости с Descendent[]
. Тот, кто поддерживает ваш код, должен будет выследить каждый из этих случаев и разработать подходящий метод обхода. Факторинг вашей программы вокруг этой идиомы, чтобы избежать этих проблем, вероятно, не стоит того, чтобы удобство.
Ответ 5
Хотя этот конкретный пример безопасен на всех современных платформах и компиляторах, с которыми я знаком, он небезопасен вообще и является примером плохого кода.
- Он не может работать на компиляторах/платформах, где sizeof (Base)!= sizeof (Descendant)
- Это небезопасно, потому что кто-то в вашем проекте добавит новый нестатический член в класс Descendant или сделает базовый класс виртуальным.
UPD. Оба Base и Descendant являются стандартными типами макета. Таким образом, стандартным является требование, чтобы указатель на Descendant мог корректно reinterpret_cast на указатель на Base, это означает, что никакая прокладка перед структурой не разрешена.
Но в стандарте С++ нет никаких требований к заполнению в конце структуры, поэтому он зависит от компилятора.
Существует также стандартное предложение явно отмечать это поведение как undefined.
http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_defects.html#1504