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

Включение между структурами из разных областей

Мне интересны кастинг между указателями на потенциально совместимые структуры. Они будут использовать один и тот же тег, те же элементы в том же порядке. Хотя целевая кодовая база скомпилирована как C или С++, для упрощения этого вопроса я хотел бы ограничить это только на С++.

Это ситуация, когда я уверен, что компилятор будет вести себя разумно, но я не могу найти подтверждающие доказательства того, что это необходимо для этого.

Пример мотивирующего кода:

#include <cstdio>

void foo(void * arg)
{
    struct example
    {
        int a;
        const char * b;
    };

    example * myarg = static_cast<example *>(arg);
    printf("meaning of %s is %d\n",myarg->b,myarg->a);
}

void bar(void)
{
    struct example
    {
        int a;
        const char * b;
    };

    example on_stack {42, "life"};
    foo(&on_stack);
}

int main(int,char**)
{
    bar();
}

Мне не повезло со стандартом С++ 11. В разделе 9 по классам предполагается, что примеры будут "совместимы с макетами", что звучит обнадеживающе, но я не могу найти описание последствий того, что структуры "совместимы с макетами". В частности, могу ли я наложить указатель на один на указатель другого без последствий?

По мнению коллеги, "совместимость с макетами" означает, что memcpy будет работать так, как ожидалось. Учитывая, что рассматриваемая структура также всегда тривиально копируема, возможно, что следующий номинально неэффективный код избежит UB:

#include <cstdio>
#include <cstring>

void foo(void * arg)
{
    struct example
    {
        int a;
        const char * b;
    };

    example local;
    std::memcpy(&local, arg, sizeof(example));
    printf("meaning of %s is %d\n", local.b, local.a);
}

// bar and main as before

Фактическая мотивация для этого состоит в том, чтобы получить определение структуры из глобальной области, когда оно используется только для связи между небольшим количеством функций. Я понимаю, что это спорно, является ли это хорошая идея.

4b9b3361

Ответ 1

Теперь ясно (спасибо Николь Болас ответ), что прямое наложение между двумя структурами, которые просто совместимы с макетами, вызовет UB из-за строгого правила псевдонимов.

Конечно, вы можете memcopy содержимого, но:

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

Но... вы можете создать на С++ структуру ссылок, указывающую на исходные значения. Он будет переименовывать непосредственно членов в их оригинальный тип, который теперь полностью определен стандартом.

Код для foo может стать:

void foo(void * arg)
{
    struct example // only used to declare the layout
    {
        int a;
        const char * b;
    };
    struct r_example {
    int &a;
    const char *&b;
    r_example(void *ext): a(*(static_cast<int*>(ext))),
        b(*(reinterpret_cast<const char **>(
            static_cast<char*>(ext) + offsetof(example, b)))) {}
    };


    r_example myarg(arg);
    printf("in foo meaning of %s is %d\n",myarg.b,myarg.a);
    myarg.a /= 2;
}

И изменение, введенное в последней строке, видимо без UB в вызывающем абоненте:

void bar(void)
{
    struct example
    {
        int a;
        const char * b;
    };

    example on_stack {42, "life"};
    foo(&on_stack);
    printf("after foo meaning of %s is %d\n",on_stack.b,on_stack.a);
}

Отобразится:

in foo meaning of life is 42
after foo meaning of life is 21

C-копия C будет использовать указатели вместо refs:

    struct p_example {
        int *a;
        const char **b;
    } my_arg;
    my_arg.a = (int *) ext;
    my_arg.b = (const char **)(((char*)ext) + offsetof(example, b));

    printf("in foo meaning of %s is %d\n",*(myarg.b),*(myarg.a));
    *(myarg.a) /= 2;

Ответ 2

Предоставляет ли [basic.lval] 10.6 возможность сглаживания между совместимыми форматами? Нет. В этом разделе говорится:

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

Вспомним, что "вышеупомянутые типы" являются фактическим типом T, динамическим типом T, типом, похожим на динамический тип, некоторой константной или изменчивой версией динамического типа или подписанной /unsigned версия динамического типа.

Теперь рассмотрим этот код:

struct T {int i;};
struct U {int i;};

T t;
U *pu = (U*)&t;
pu->i = 5;

Теперь посмотрим на 10.6 в этом свете. Вопрос 10.6 спрашивает, является ли тип glval U членом, который соответствует квалификациям 10.1-10.5. Имеет ли это? Помните, что динамический тип объекта T равен T.

  • Включает ли U элемент типа T? Нет.
  • Включает ли U элемент, который является версией T для команды const/volatile? Нет.
  • Включает ли U элемент, который имеет тип, похожий на T? Нет.
  • Включает ли U член, который является подписанной/неподписанной версией T? Нет.
  • Включает ли U член, который является константной/изменчивой версией версии подписанного/неподписанного T? Нет.

Поскольку все эти ошибки терпят неудачу, компилятору разрешено предположить, что изменение объекта, на которое указывает pu, не будет изменять объект T.


FYI:

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

Нет, это не так. Правила для тривиальной совместимости с копиями и совместимости макетов совсем не совпадают с правилами для сглаживания.

Тривиальная копируемость - это разумность копирования представления ценности объекта и является ли такая копия законным объектом. Правила совместимости макета состоят в том, совместимо ли представление значений A с B, так что значение A может быть скопировано в объект типа B.

Алиасинг говорит о возможности доступа к объекту через указатель/ссылку на A и указатель/ссылку на B в то же время. Строгое правило сглаживания гласит, что если компилятор видит A& a и B& b, компилятору разрешено предположить, что изменения, сделанные с помощью A, не будут влиять на объект, на который ссылается через B, и наоборот. [basic.lval] 10 описывает случаи, когда компилятору не разрешено это считать.

Ответ 3

Я согласен с Николом Боласом в ответе, что вы не можете использовать тип доступа через другой тип, даже если они совместимы с макетами. Я просто добавлю, что означает совместимость с макетами.

N3337 9.2/17

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


Теперь, чтобы объяснить все термины здесь:

(Обратите внимание, что совместимые с макетами типы и макеты структуры стандартного макета) - это две разные вещи)

1. Совместимый с макета тип

макет-совместимый тип означает тот же тип:

N3337 3.9/11:

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

2. Структура стандартного макета:

N3337 9/8

Структура стандартного макета - это класс стандартного макета, определенный с помощью структуры класса или класса класса.

(Или, другими словами, потому что С++ относится к структурам, объединениям и классам как к классу), это класс стандартного макета, который не является объединением)

Стандартный класс макета:

N3337 9/7

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

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

108) Это гарантирует, что два подобъекта, которые имеют один и тот же тип класса и которые принадлежат одному и тому же самому производному объекту, не являются выделенных по тому же адресу (5.10)