Когда 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