Совместимость макета в стандарте С++ 11 (рабочий проект) слишком слаба?

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

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

файл1.с

#include <cstddef>

struct Foo {
public:
   int pub;
private:
   int priv;
};

size_t getsize1(Foo const &foo) {
  return sizeof(foo);
}

файл2.с

#include <cstddef>

struct Foo {
public:
   int pub;
private:
   int priv;
};

size_t getsize2(Foo const &foo) {
  return sizeof(foo);
}

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

main.cc

#include <iostream>
struct Foo {
public:
   int pub;
private:
   int priv;
};

size_t getsize1(Foo const &);
size_t getsize2(Foo const &);

int main() {
    Foo foo;
    std::cout << getsize1(foo) << ", " << getsize2(foo) << ", " << sizeof(foo) << '\n';
}

Один из способов сделать это — использовать g++:

g++ -std=c++11 -c -Wall file1.cc 
g++ -std=c++11 -c -Wall file2.cc 
g++ -std=c++11 -c -Wall main.cc 
g++ -std=c++11 -Wall *.o -o main

И (в моей архитектуре и среде) это показывает: 8, 8, 8. Sizeof одинаковы для каждой компиляции file1.cc, file2.cc и main.cc.

Но гарантирует ли это стандарт c++11, действительно можно ли рассчитывать на совместимость макета со всеми тремя Foo? Foo содержит как частные, так и общедоступные поля, поэтому это не структура стандартного макета, как определено в пункте 9, п. 7 стандарта С++ 11 (рабочий проект):

Класс стандартной компоновки — это класс, который:

  • не имеет нестатических элементов данных типа класса нестандартного макета (или массива таких типов) или ссылки,
  • не имеет виртуальных функций (10.3) и виртуальных базовых классов (10.1),
  • имеет одинаковый контроль доступа (пункт 11) для всех нестатических элементов данных,
  • не имеет базовых классов нестандартной компоновки,
  • либо не имеет нестатических элементов данных в самом производном классе и не более одного базового класса с нестатическими элементами данных, либо не имеет базовых классов с нестатическими элементами данных, и
  • не имеет базовых классов того же типа, что и первый нестатический член данных.

Поскольку мы используем структуры, и, чтобы быть тщательным, следующий параграф говорит:

Структура стандартного макета — это класс стандартного макета, определенный с помощью структуры ключа класса или класса ключа класса. Объединение стандартного макета — это класс стандартного макета, определенный с помощью объединения классов и ключей.

Насколько мне известно, стандарт определяет совместимость макетов только между структурами в стандартном макете (пункт 9.2, пар. 18).

Два типа структуры стандартной компоновки (раздел 9) являются компоновочно-совместимыми, если они имеют одинаковое количество нестатических элементов данных, а соответствующие нестатические элементы данных (в порядке объявления) имеют типы, совместимые с компоновкой (3.9).

Так гарантируется, что все три Foo совместимы с макетом, и, что более важно, почему?

Почему (недетерминированный) компилятор, который создает разные макеты для Foo во время компиляции, не может быть компилятором С++ 11?


person Herbert    schedule 23.10.2014    source источник
comment
Я не могу дать авторитетный или определенный ответ, но я бы сказал, что да, ожидается, что будет одинаковая компоновка трех разных (но равных) классов, иначе их наличие в заголовочном файле не будет работать (как вы заметили) .   -  person Some programmer dude    schedule 23.10.2014
comment
Смысл бы предложил это, но я искал что-то в стандарте С++ 11, которое обеспечивает его соблюдение. Это то же самое, что разные версии компилятора должны иметь один и тот же ABI, но технически такой гарантии не существует; формально нарушая правильность идеи, которую имеют все типы менеджеров бинарных пакетов. Однако в этом случае ответственность за это несет разработчик компилятора, случай, описанный в ОП, должен обрабатываться, ИМХО, языком.   -  person Herbert    schedule 23.10.2014
comment
Это не имеет никакого отношения к понятию совместимости с макетом и имеет отношение к одному правилу определения. Все три определения Foo относятся к одному и тому же объекту, который должен иметь одинаковый набор свойств во всех единицах перевода. С этой целью все определения должны быть идентичными токен для токена, а токены должны иметь одинаковое значение; в противном случае программа неправильно сформирована, диагностика не требуется. Подробнее см. 3.2/5.   -  person Igor Tandetnik    schedule 23.10.2014
comment
Еще один аргумент здравого смысла (хотя и не подкрепленный стандартом AFAIK): при одинаковых входных данных и параметрах компилятор должен создать один и тот же макет, иначе двоичный макет любой программы/общего объекта рискует измениться с каждый сборник. Это полностью разрушило бы любую надежду на динамическую компоновку.   -  person Cameron    schedule 23.10.2014
comment
Это кажется очень разумным, текст ODR в стандарте слишком длинный, чтобы его можно было прочитать за несколько минут; поэтому я отложу это немного. Однако, если я правильно понимаю, если оба класса имели разные имена, компилятору разрешено компилировать их по-разному?   -  person Herbert    schedule 23.10.2014
comment
@Cameron стандарт не обещает никакой динамической компоновки.   -  person n. 1.8e9-where's-my-share m.    schedule 23.10.2014
comment
@Cameron: я знаю о проблемах, которые существуют с таким недетерминированным компилятором, мне просто интересно, как стандарт описывает это детерминированное поведение.   -  person Herbert    schedule 23.10.2014
comment
если оба класса имеют разные имена, компилятору разрешено компилировать их по-разному? Да (если они не являются типами стандартной компоновки).   -  person n. 1.8e9-where's-my-share m.    schedule 23.10.2014
comment
Конечно, ответ "нет", потому что люди, написавшие это, очень тяжело об этом отзывались Я нахожу этот аргумент бессмысленным. Если бы это было правдой, то ни одна предыдущая стандартная формулировка никогда не была бы изменена.   -  person Lightness Races in Orbit    schedule 23.10.2014
comment
@LightnessRacesinOrbit: Верно. Однако я написал это, чтобы обозначить, что я не сомневаюсь в реализации стандарта C++11, а только в своей способности понять его.   -  person Herbert    schedule 23.10.2014
comment
@н.м. Единицы перевода не «осведомлены» друг о друге, поэтому, чтобы иметь разные выходные данные компиляции двух классов, которые являются одинаковыми по токенам, за исключением имени, компилятору необходимо будет использовать имя класса как подсказку для другого макета класса. и реализация, которая смехотворна, но разрешена :) Она не может использовать случайность из-за ODR.   -  person Herbert    schedule 23.10.2014
comment
смешно, но разрешено Почему бы и нет? Хэшируйте имя, заполните PRNG результатом, вычислите случайную перестановку элементов, расположите элементы в указанном порядке. Это покажет их. Не полагайтесь на вещи, которые стандарт не обещает!   -  person n. 1.8e9-where's-my-share m.    schedule 23.10.2014
comment
@Herbert: Говоря цинично, я считаю, что небольшой здоровый скептицизм в отношении рабочей группы ISO — это не всегда плохо ;)   -  person Lightness Races in Orbit    schedule 23.10.2014
comment
@н.м. Невозможно выполнить случайную перестановку членов, только групп членов, разделенных ключевыми словами управления доступом. Существует гарантия того, что элементы в каждой секции располагаются по возрастающим адресам. Тем не менее, разделы могут быть переупорядочены, а между элементами может быть вставлено случайное заполнение (при условии соблюдения требований выравнивания).   -  person Igor Tandetnik    schedule 23.10.2014


Ответы (1)


Три Foo совместимы по макету, поскольку они имеют один и тот же тип, struct ::Foo.

[основные.типы]

11 - Если два типа T1 и T2 одного типа, то T1 и T2 являются типами, совместимыми с компоновкой.

Классы одного типа, потому что они имеют одно и то же (полное) имя и имеют внешнюю связь:

[базовый]

9. Имя, используемое более чем в одной единице перевода, потенциально может относиться к одному и тому же объекту в этих единицах перевода в зависимости от связи (3.5) имени, указанного в каждой единице перевода.

Имена классов, объявленные в области пространства имен, которые не объявлены (рекурсивно) в безымянном пространстве имен, имеют внешнюю связь:

[основная.ссылка]

2. Говорят, что имя имеет связь, если оно может обозначать [...] тот же тип [...], что и имя, введенное объявлением в другой области:
— когда имя имеет внешнюю связь, на объект, который он обозначает, можно ссылаться по именам из областей действия других единиц перевода или из других областей действия той же единицы перевода. [...]
4 - Безымянное пространство имен или пространство имен, объявленное прямо или косвенно в безымянном пространстве имен, имеет внутреннюю связь. Все остальные пространства имен имеют внешнюю связь. Имя, имеющее область пространства имен, для которой не была задана внутренняя связь выше, имеет ту же связь, что и окружающее пространство имен, если оно является именем [...]
— именованного класса (пункт 9) или определенного безымянного класса. в объявлении typedef, в котором класс имеет имя typedef для целей компоновки (7.1.3) [...]

Обратите внимание, что разрешено иметь несколько определений типа класса, появляющихся в разных единицах перевода, если определения состоят из одной и той же последовательности токенов:

[базовый.def.odr]

6 - В программе может быть более одного определения типа класса (пункт 9) [...] при условии, что каждое определение появляется в другой единице перевода, и при условии, что [...] каждое определение [...] должны состоять из той же последовательности токенов [...]

Таким образом, если бы у Foo были разные имена, они не были бы одного типа; если бы они появились в анонимном пространстве имен или в определении функции (кроме встроенной функции; см. [dcl.fct.spec]/4), у них не было бы внешней связи и так что не будет того же типа. В любом случае они будут совместимы с макетом только в том случае, если они будут стандартного макета.


Некоторые примеры:

// tu1.cpp
struct Foo { private: int i; public: int j; };

// tu2.cpp
struct Foo { private: int i; public: int j; };

Два Foo одного типа.

// tu1.cpp
struct Foo { private: int i; public: int j; };

// tu2.cpp
struct Foo { private: int i; public: int k; };

нарушение ОДР; неопределенное поведение.

// tu1.cpp
struct Foo { private: int i; public: int j; };

// tu2.cpp
struct Bar { private: int i; public: int j; };

Разные имена, такие разные типы. Не совместим с макетом.

// tu1.cpp
struct Foo { int i; int j; };

// tu2.cpp
struct Bar { int i; int j; };

Разные имена, разные типы, но совместимые с макетом (начиная со стандартного макета).

// tu1.cpp
namespace { struct Foo { private: int i; public: int j; }; }

// tu2.cpp
namespace { struct Foo { private: int i; public: int j; }; }

Внутренняя связь; различные виды.

// tu1.cpp
static void f() { struct Foo { private: int i; public: int j; }; }

// tu2.cpp
static void f() { struct Foo { private: int i; public: int j; }; }

Нет связи; различные виды.

// tu1.cpp
inline void f() { struct Foo { private: int i; public: int j; }; }

// tu2.cpp
inline void f() { struct Foo { private: int i; public: int j; }; }

Тот же тип по [dcl.fct.spec]/4.

person ecatmur    schedule 23.10.2014
comment
Не могли бы вы добавить, что очень важно, чтобы оба класса имели одинаковое имя, чтобы они были одного типа? (см. комментарий Н.М.) - person Herbert; 23.10.2014
comment
Итак, оберните Foo анонимным пространством имен, и все ставки сняты? - person Yakk - Adam Nevraumont; 23.10.2014
comment
@Yakk абсолютно, если только Foo не имеют стандартного макета. - person ecatmur; 23.10.2014