Твърде слаба ли е съвместимостта на оформлението в стандарта c++11 (работна чернова)?

Разбира се, отговорът е „не“, защото хората, които са го написали, много са го обмислили, но искам да знам защо.

Като се има предвид, че класовете (без шаблони) често се декларират в заглавни файлове, които след това се включват в няколко файла, които се компилират отделно, разгледайте тези два файла под:

file1.c

#include <cstddef>

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

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

file2.c

#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. Размерите са еднакви за всяка компилация на file1.cc, file2.cc и main.cc

Но дали стандартът c++11 гарантира това, наистина добре ли е да очакваме да имаме съвместимост на оформлението с всичките 3 Foo? Foo съдържа както частни, така и публични полета, следователно не е структура със стандартно оформление, както е дефинирано в клауза 9, параграф 7 от стандарта c++11 (работен проект):

Клас със стандартно оформление е клас, който:

  • няма нестатични членове на данни от тип клас с нестандартно оформление (или масив от такива типове) или референция,
  • няма виртуални функции (10.3) и виртуални базови класове (10.1),
  • има един и същ контрол на достъпа (Клауза 11) за всички членове на нестатични данни,
  • няма базови класове с нестандартно оформление,
  • или няма нестатични членове на данни в най-производния клас и най-много един основен клас с нестатични членове на данни, или няма базови класове с нестатични членове на данни, и
  • няма базови класове от същия тип като първия нестатичен член на данните.

Тъй като използваме структури и за да бъдем изчерпателни, следващият параграф казва:

Структурата със стандартно оформление е клас със стандартно оформление, дефиниран със структурата на класовия ключ или класовия ключов клас. Обединение със стандартно оформление е клас със стандартно оформление, дефиниран с обединението на класовия ключ.

Доколкото ми е известно, стандартът дефинира само съвместимост на оформление между структури в стандартно оформление (клауза 9.2, параграф 18).

Два типа структура със стандартно оформление (клауза 9) са съвместими с оформлението, ако имат еднакъв брой нестатични членове с данни и съответните нестатични членове с данни (в ред на деклариране) имат типове, съвместими с оформлението (3.9).

Така че гарантирано ли е, че и трите Foo са съвместими с оформлението и по-важното защо?

Защо (недетерминиран) компилатор, който създава различни оформления за Foo по време на компилация, не би бил c++11 компилатор?


person Herbert    schedule 23.10.2014    source източник
comment
Не мога да дам авторитетен или категоричен отговор, но бих казал, че да, очаква се да има същото оформление на трите различни (но равни) класа, в противен случай наличието им в заглавния файл няма да работи (както забелязвате) .   -  person Some programmer dude    schedule 23.10.2014
comment
Sense би предложил това, но аз търсех нещо в стандарта c++11, което го налага. Същото е, че е хубаво различните версии на компилатора да имат един и същ ABI, но технически тази гаранция не съществува; формално нарушаване на правилността на идеята, която имат всички видове мениджъри на двоични пакети. Въпреки това, в този случай разработчикът на компилатора носи отговорност за това, случаят, описан в OP, трябва да се обработва, IMHO, от езика.   -  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
@n.m. Единиците за превод не са „наясно“ една с друга, така че за да има различни изходни резултати за компилация на два класа, които са еднакви по отношение на лексемите, с изключение на името, компилаторът ще трябва да използва името на класовете намек за различно оформление на класа и имплементация, което е абсурдно, но позволено :) Не може да използва произволност, поради 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
@n.m. Не може да прави произволна пермутация на членовете, само на групите членове, разделени от ключови думи за контрол на достъпа. Има гаранция, че членовете във всеки раздел са разположени на нарастващи адреси. Секциите обаче могат да бъдат пренаредени и може да се вмъкне произволна подложка между членовете (стига изискванията за подравняване да са изпълнени).   -  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) [...]

Обърнете внимание, че е позволено да има множество дефиниции на тип клас, появяващи се в различни единици за превод, стига дефинициите да се състоят от една и съща последователност на токени:

[basic.def.odr]

6 - Може да има повече от една дефиниция на тип клас (клауза 9) [...] в програма, при условие че всяка дефиниция се появява в различна единица за превод и при условие, че [...] всяка дефиниция [...] се състои от една и съща последователност от символи [...]

Така че, ако Foos имаха различни имена, те нямаше да са от един и същи тип; ако се появяват в анонимно пространство от имена или в дефиниция на функция (освен вградена функция; вижте [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; };

нарушение на ODR; неопределено поведение.

// 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
Бихте ли добавили, че е изключително важно двата класа да имат едно и също име, за да бъдат от един и същи тип? (вижте коментара на n.m.) - person Herbert; 23.10.2014
comment
И така, увийте Foo в анонимно пространство от имена и всички залози са изключени? - person Yakk - Adam Nevraumont; 23.10.2014
comment
@Yakk абсолютно, освен ако Foos не са със стандартно оформление. - person ecatmur; 23.10.2014