Подтвердить что ты не робот

Почему GCC и Clang не делают эту оптимизацию псевдонимов?

У меня есть случай, когда друг бросает объект не базового класса типа "Base" на объект типа класса "Производные", где "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 (как он и предполагал), хотя я думаю, что он по-прежнему имеет поведение undefined

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?

4b9b3361

Ответ 1

Если вы удалите B::x, то B удовлетворяет требованиям в 9p7 для класса стандартного макета, и доступ становится совершенно корректным, поскольку два типа являются совместимыми с макетами, 9.2p17, а члены имеют тот же тип.


Класс стандартного макета - это класс, который:

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

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

Ответ 2

Undefined поведение включает в себя случай, когда он работает, даже если он не должен.

В соответствии со стандартным использованием этого союза можно получить доступ к полям типа и размера любого из заголовков или элементов данных:

union Packet {
   struct Header {
   short type;
   short size;  
   } header;
   struct Data {
   short type;
   short size;  
   unsigned char data[MAX_DATA_SIZE];
   } data;
}

Это строго ограничивается объединениями, но многие компиляторы поддерживают это как расширение, при условии, что "неполный" тип будет завершен массивом размера undefined. Если вы удаляете дополнительный статический не-член из дочернего класса, он действительно становится тривиальным и совместимым с макетами, что позволяет наложение псевдонимов?

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 };

Совместимость макетов между этими двумя здесь определяется С++ 14, и они не совместимы с базовым типом (но конвертируемым). хотя что-то вроде

 enum A a = Y;
 enum B b = (B*)a;

может вызывать поведение undefined, как если бы вы попытались отобразить float с произвольным 32-битным значением.

Ответ 3

Я думаю, что ваш код UB, поскольку вы разыскиваете указатель, который исходит от кастинга, который нарушает правила типа псевдонимов.

Теперь, если вы активируете строгий флаг псевдонимов, вы разрешаете компилятору оптимизировать код для UB. Как использовать этот UB до компилятора. Вы можете найти ответы на этот вопрос.

Что касается gcc, документация для -fstrict-aliasing показывает, что она может оптимизироваться на основе:

(...) предполагается, что объект одного типа никогда не будет находиться в одном и том же адрес как объект другого типа, если только типы то же самое.

Мне не удалось найти определение для "почти того же", но два класса с точно такой же компоновкой можно считать "почти одинаковыми", и они не учитывают оптимизацию.

Ответ 4

Я считаю, что это законный С++ (без вызова 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 могут использовать псевдоним и перезагружать значение.