gcc/clang размещает поля производной структуры в отступах базовой структуры
Я запутался в том, как gcc и clang выстраивают структуры, когда задействованы и padding, и наследование. Вот пример программы:
#include <string.h>
#include <stdio.h>
struct A
{
void* m_a;
};
struct B: A
{
void* m_b1;
char m_b2;
};
struct B2
{
void* m_a;
void* m_b1;
char m_b2;
};
struct C: B
{
short m_c;
};
struct C2: B2
{
short m_c;
};
int main ()
{
C c;
memset (&c, 0, sizeof (C));
memset ((B*) &c, -1, sizeof (B));
printf (
"c.m_c = %d; sizeof (A) = %d sizeof (B) = %d sizeof (C) = %d\n",
c.m_c, sizeof (A), sizeof (B), sizeof (C)
);
C2 c2;
memset (&c2, 0, sizeof (C2));
memset ((B2*) &c2, -1, sizeof (B2));
printf (
"c2.m_c = %d; sizeof (A) = %d sizeof (B2) = %d sizeof (C2) = %d\n",
c2.m_c, sizeof (A), sizeof (B2), sizeof (C2)
);
return 0;
}
Выход:
$ ./a.out
c.m_c = -1; sizeof (A) = 8 sizeof (B) = 24 sizeof (C) = 24
c2.m_c = 0; sizeof (A) = 8 sizeof (B2) = 24 sizeof (C2) = 32
Структуры C1 и C2 расположены по-разному. В C1 m_c размещается в отступе структуры B1 и поэтому перезаписывается 2-м memset (); с С2 этого не происходит.
Используемые компиляторы:
$ clang --version
Ubuntu clang version 3.3-16ubuntu1 (branches/release_33) (based on LLVM 3.3)
Target: x86_64-pc-linux-gnu
Thread model: posix
$ c++ --version
c++ (Ubuntu 4.8.2-19ubuntu1) 4.8.2
Copyright (C) 2013 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
То же самое происходит с опцией -m32 (очевидно, что размеры на выходе будут другими).
Как в x86, так и в x86_64 версиях компилятора Microsoft Visual Studio 2010 C++ такой проблемы нет (т. Е. Они одинаково выкладывают структуры С1 и С2)
Если это не ошибка и это дизайн, то мои вопросы:
- Каковы точные правила для выделения или не выделения полей производной структуры в отступах (например, почему это не происходит с C2?)
- Есть ли способ переопределить это поведение с помощью переключателей / атрибутов (т.е. выложить так же, как MSVC)?
Заранее спасибо.
Владимир
4 ответа
Для всех, кто опускает этот вопрос и отвечает на вопросы ОП с самодовольным негодованием по поводу того, как ужасно UB его рукописные memcpy
было... учтите, что разработчики как libC++, так и libstdC++ попадают в одну и ту же яму. В обозримом будущем на самом деле очень важно понять, когда набивка хвоста используется повторно, а когда - нет. Хорошо на OP для постановки этого вопроса.
Правила Itanium ABI для разметки структуры здесь. Соответствующая формулировка
Если D является базовым классом, обновите sizeof(C) до max (sizeof(C), offset(D)+nvsize(D)).
Здесь "dsize, nvsize и nvalign [POD-типов] определены как их обычный размер и выравнивание", но nvsize не-POD-типа определяется как " не виртуальный размер объекта, который размер O без виртуальных баз [а также без хвостовой подкладки]. " Так что, если D - POD, мы никогда ничего не вкладываем в его хвостовую подкладку; в то время как если D не POD, нам разрешается втиснуть следующий элемент (или базу) в его хвостовую часть.
Следовательно, любой не POD-тип (даже тривиально копируемый!) Должен учитывать возможность того, что он имеет важные данные, вставленные в его хвостовую часть. Как правило, это нарушает предположения разработчиков о том, что допустимо делать с тривиально копируемыми типами (а именно, что вы можете тривиально их копировать).
#include <algorithm>
#include <stdio.h>
struct A {
int m_a;
};
struct B : A {
int m_b1;
char m_b2;
};
struct C : B {
short m_c;
};
int main() {
C c1 { 1, 2, 3, 4 };
B& b1 = c1;
B b2 { 5, 6, 7 };
printf("before operator=: %d\n", int(c1.m_c)); // 4
b1 = b2;
printf("after operator=: %d\n", int(c1.m_c)); // 4
printf("before std::copy: %d\n", int(c1.m_c)); // 4
std::copy(&b2, &b2 + 1, &b1);
printf("after std::copy: %d\n", int(c1.m_c)); // 64, or 0, or anything but 4
}
Ваш код демонстрирует неопределенное поведение, так как C и C2 не являются POD, и запоминание случайных битов их данных не допускается.
Однако в более отдаленной перспективе это сложный вопрос. Существующий C ABI на платформе (Unix) разрешал такое поведение (это для C++98, который разрешил это). Затем Комитет несовместимо изменил правила в C++03 и C++11. По крайней мере, у Clang есть возможность перейти на более новые правила. Конечно, C ABI в Unix не изменился, чтобы приспособиться к новым правилам C++ 11 для помещения вещей в отступы, поэтому компиляторы не могут просто обновить, так как это сломало бы весь ABI.
Я полагаю, что GCC хранит изменения, нарушающие ABI для 5.0, и это может быть одним из них.
Windows всегда запрещал эту практику в своем C ABI и поэтому не имеет проблем, насколько я знаю.
Разница в том, что компилятору разрешено использовать заполнение предыдущего объекта, если этот объект уже "не просто данные", и манипулировать им, скажем, с помощью memcpy
не поддерживается.
B
структура - это не просто данные, потому что это производный объект, и поэтому его свободное пространство может быть использовано, потому что если вы memcpy
-ин B
Например, вы уже нарушаете контракт.
B2
вместо этого это просто структура и обратная совместимость требует, чтобы его размер (включая свободное пространство) был просто памятью, с которой ваш код может играть, используя memcpy
,
Спасибо всем за вашу помощь.
Суть в том, что компиляторам C++ разрешено повторно использовать заполнение хвостом структур, отличных от POD, при разметке полей производных структур. И GCC, и Clang используют это разрешение, MSVC - нет. Похоже, что у GCC есть флаг предупреждения -Wabi, который должен помочь в выявлении случаев потенциальной несовместимости ABI, но он не выдал предупреждений в приведенном выше примере.
Похоже, что единственный способ предотвратить это - ввести явные поля заполнения хвоста.