У меня есть дело где друг преобразует объект неосновного класса типа «Base» в объект типа класса «Derived», где «Derived» является производным классом «Base» и добавляет только функции, но без данных. В приведенном ниже коде я сделал добавить элемент данных x
в производный класс
struct A {
int a;
};
struct B : A {
// int x;
int x;
};
A a;
int g(B *b) {
a.a = 10;
b->a++;
return a.a;
}
При строгом анализе псевдонимов GCC (также Clang) всегда возвращается 10
, и не 11
, так как b
никогда не может указывать на a
в четко определенном коде. Однако, если я удалю B::x
(как на самом деле в коде моего друга), выходной код ассемблера GCC делает не оптимизировать обратный доступ a.a
и перезагружает значение из памяти. Так код моего друга, который называет g
«работает» в GCC (как он и предполагал), хотя я думаю, что он все еще имеет неопределенное поведение
g((B*)&a);
Таким образом, по сути, в тех же двух случаях GCC оптимизирует один случай, а другой — нет. Это потому что b
может тогда юридически указать на a
? Или это потому, что GCC просто не хочет нарушать реальный код?
Я проверил ответ, который утверждает
Если вы удалите B :: x, то B удовлетворяет требованиям в 9p7 для класса стандартной компоновки, и доступ становится совершенно четко определенным, поскольку эти два типа совместимы с компоновкой, 9.2p17.
С двумя совместимыми с макетом перечислениями
enum A : int { X, Y };
enum B : int { Z };
A a;
int g(B *b) {
a = Y;
*b = Z;
return a;
}
Выход ассемблера для g
возвращается 1
не 0
, даже если A
а также B
совместимы с макетом (7.2p8).
Итак, мой следующий вопрос (цитируя ответ): «Два класса с абсолютно одинаковым расположением могут считаться« почти одинаковыми », и они исключены из оптимизации».. Может ли кто-нибудь предоставить доказательство этого для GCC или Clang?
Если вы удалите B::x
, затем B
соответствует требованиям в 9p7 для стандартная компоновка и доступ становится совершенно четким, потому что два типа макет-совместимый, 9.2p17 и члены имеют одинаковый тип.
Класс стандартного макета — это класс, который:
- не имеет нестатических членов-данных типа нестандартного класса макета (или массива таких типов) или ссылки,
- не имеет виртуальных функций (10.3) и виртуальных базовых классов (10.1),
- имеет одинаковый контроль доступа (пункт 11) для всех нестатических элементов данных,
- не имеет базовых классов нестандартной компоновки,
- или не содержит нестатических членов данных в самом производном классе и не более одного базового класса с нестатическими членами данных, или не имеет базовых классов с нестатическими членами данных, и
- не имеет базовых классов того же типа, что и первый нестатический элемент данных.
Два типа структуры стандартного макета совместимы с макетом, если они имеют одинаковое количество нестатических элементов данных, а соответствующие нестатические элементы данных (в порядке объявления) имеют совместимые с макетом типы.
Неопределенное поведение включает в себя случай, когда оно делает работать, даже если это не должно.
В соответствии со стандартным использованием это объединение позволяет получить доступ к полям типа и размера заголовка или членов данных:
union Packet {
struct Header {
short type;
short size;
} header;
struct Data {
short type;
short size;
unsigned char data[MAX_DATA_SIZE];
} data;
}
Это строго ограничено объединениями, но многие компиляторы поддерживают это как своего рода расширение, при условии, что «неполный» тип будет заканчиваться массивом неопределенного размера. Если вы удаляете лишний статический не член из дочернего класса, он действительно становится тривиальным и совместимым с макетом, что позволяет создавать псевдонимы?
struct A {
int a;
};
struct B {
int a;
//int x;
};
A a;
int g(B *b) {
a.a = 10;
b->a++;
return a.a;
}
Тем не менее, по-прежнему выполняет оптимизацию псевдонимов. В вашем случае с таким же количеством нестатических членов предполагается, что наиболее производный класс совпадает с базовым классом. Давайте изменим порядок:
#include <vector>
#include <iostream>
struct A {
int a;
};
struct B : A {
int x;
};
B a;
int g(A *b) {
a.a = 10;
b->a++;
return a.a;
}
int main()
{
std::cout << g((A*)&a);
}
Это возвращает 11, как и ожидалось, поскольку B явно также является A, в отличие от первоначальной попытки. Давай играть дальше
struct A {
int a;
};
struct B : A {
int foo() { return a;}
};
Не приведет к оптимизации псевдонимов, если foo () не является виртуальным. Добавление нестатического или константного члена к B приведет к ответу «10», добавление нетривиального конструктора или статического нет.
PS.
Во втором примере
enum A : int { X, Y };
enum B : int { Z };
Совместимость макетов между этими двумя здесь определяется C ++ 14, и они не совместимы с базовым типом (но конвертируемы). хотя что то типа
enum A a = Y;
enum B b = (B*)a;
может привести к неопределенному поведению, как если бы вы пытались отобразить float с произвольным 32-битным значением.
Я думаю, что ваш код UB, так как вы разыменовываете указатель, полученный из преобразования, нарушающего правила наложения типов.
Теперь, если вы активируете флаг строгого алиасинга, вы позволяете компилятору оптимизировать код для UB. Как использовать этот UB зависит от компилятора. Вы можете увидеть ответы на этот вопрос.
Что касается GCC, то документация для -fstrict-aliasing показывает, что он может оптимизировать на основе:
(…) предполагается, что объект одного типа никогда не будет находиться в одном месте
адрес как объект другого типа, если типы не являются почти
тот же самый.
Мне не удалось найти определение для «почти одинакового», но два класса с абсолютно одинаковым макетом можно считать «почти одинаковыми», и они не включены в оптимизацию.
Я считаю, что следующее допустимо в C ++ (без вызова UB):
#include <new>
struct A {
int a;
};
struct B : A {
// int x;
};
static A a;
int g(B *b);
int g(B *b) {
a.a = 10;
b->a++;
return a.a;
}
int f();
int f() {
auto p = new (&a) B{};
return g(p);
}
потому что (глобальный) a
всегда относится к объекту типа A
(хотя это подобъект B
-объект после звонка f()
) а также p
указывает на объект типа B
,
Если вы отметите a
иметь static
длительность хранения (как я делал выше), все протестированные мной компиляторы с радостью применят строгое псевдонимы и оптимизируют для возврата 10
,
С другой стороны, если вы отметите g()
с __attribute__((noinline))
или добавить функцию h()
который возвращает указатель на a
A* h();
A* h() { return &a; }
компиляторы, которые я тестировал, предполагают, что &a
и параметр b
может псевдоним и перезагрузить значение.