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

Строгое выравнивание и выравнивание

Мне нужен безопасный способ псевдонимов между произвольными типами POD, соответствующими ISO-С++ 11, явно рассматривающим 3.10/10 и 3.11 из n3242 или более поздних версий. Здесь много вопросов о строгом псевдониме, большинство из которых касается C, а не С++. Я нашел "решение" для C, которое использует союзы, возможно, используя этот раздел

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

Из этого я построил это.

#include <iostream>

template <typename T, typename U>
T& access_as(U* p)
{
    union dummy_union
    {
        U dummy;
        T destination;
    };

    dummy_union* u = (dummy_union*)p;

    return u->destination;
}

struct test
{
    short s;
    int i;
};

int main()
{
    int buf[2];

    static_assert(sizeof(buf) >= sizeof(double), "");
    static_assert(sizeof(buf) >= sizeof(test), "");

    access_as<double>(buf) = 42.1337;
    std::cout << access_as<double>(buf) << '\n';

    access_as<test>(buf).s = 42;
    access_as<test>(buf).i = 1234;

    std::cout << access_as<test>(buf).s << '\n';
    std::cout << access_as<test>(buf).i << '\n';
}

Мой вопрос в том, что, конечно, эта программа легальна в соответствии со стандартом? *

Он не дает никаких предупреждений и отлично работает при компиляции с MinGW/GCC 4.6.2, используя:

g++ -std=c++0x -Wall -Wextra -O3 -fstrict-aliasing -o alias.exe alias.cpp

* Edit: И если нет, как можно изменить это, чтобы быть законным?

4b9b3361

Ответ 1

Это никогда не будет законным, независимо от того, какие искажения вы выполняете со странными отбрасываниями и объединениями и еще чего-то.

Фундаментальный факт таков: два объекта различного типа никогда не могут быть псевдонимом в памяти, с несколькими особыми исключениями (см. далее).

Пример

Рассмотрим следующий код:

void sum(double& out, float* in, int count) {
    for(int i = 0; i < count; ++i) {
        out += *in++;
    }
}

Позвольте разбить это на локальные переменные регистра, чтобы более точно моделировать фактическое выполнение:

void sum(double& out, float* in, int count) {
    for(int i = 0; i < count; ++i) {
        register double out_val = out; // (1)
        register double in_val = *in; // (2)
        register double tmp = out_val + in_val;
        out = tmp; // (3)
        in++;
    }
}

Предположим, что (1), (2) и (3) представляют собой чтение, чтение и запись памяти соответственно, что может быть очень дорогостоящими операциями в таком плотном внутреннем цикле. Разумная оптимизация для этого цикла будет следующей:

void sum(double& out, float* in, int count) {
    register double tmp = out; // (1)
    for(int i = 0; i < count; ++i) {
        register double in_val = *in; // (2)
        tmp = tmp + in_val;
        in++;
    }
    out = tmp; // (3)
}

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

Теперь предположим, что у нас нет строгого сглаживания. Предположим, что запись в объект любого типа может повлиять на любой другой объект. Предположим, что запись в double может повлиять на значение float. Это делает вышеупомянутую оптимизацию подозрительной, поскольку, возможно, программист на самом деле предназначен для выхода и в псевдоним, так что результат функции суммы является более сложным и зависит от процесса. Звучит глупо? Тем не менее, компилятор не может различать "глупый" и "умный" код. Компилятор может различать только хорошо сформированный и плохо сформированный код. Если мы разрешим свободное наложение, тогда компилятор должен быть консервативным в своих оптимизациях и должен выполнять дополнительное хранилище (3) на каждой итерации цикла.

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

Исключения при строгом псевдониме

Стандарты на C и С++ специально предусматривают сглаживание любого типа с помощью char и с любым "родственным типом", который, среди прочего, включает производные и базовые типы и члены, поскольку он может использовать адрес члена класса независимо, так важно. Вы можете найти исчерпывающий список этих положений в этом ответе.

Кроме того, GCC делает специальное положение для чтения от другого члена союза, чем то, что было написано последним. Обратите внимание, что такой тип преобразования через соединение на самом деле не позволяет вам нарушать псевдонимы. Только одному члену союза разрешено быть активным в любой момент времени, так, например, даже с GCC следующее поведение undefined:

union {
    double d;
    float f[2];
};
f[0] = 3.0f;
f[1] = 5.0f;
sum(d, f, 2); // UB: attempt to treat two members of
              // a union as simultaneously active

Обходные

Единственный стандартный способ интерпретировать биты одного объекта как биты объекта другого типа - использовать эквивалент memcpy. Это использует специальное положение для псевдонимов с объектами char, что позволяет вам читать и изменять представление базового объекта на уровне байта. Например, следующее является законным и не нарушает строгие правила псевдонимов:

int a[2];
double d;
static_assert(sizeof(a) == sizeof(d));
memcpy(a, &d, sizeof(d));

Это семантически эквивалентно следующему коду:

int a[2];
double d;
static_assert(sizeof(a) == sizeof(d));
for(size_t i = 0; i < sizeof(a); ++i)
   ((char*)a)[i] = ((char*)&d)[i];

GCC создает условия для чтения из неактивного члена профсоюза, неявно делая его активным. Из Документация GCC:

Практика чтения из другого члена профсоюза, чем тот, который был недавно написан (называемый "пингом типа" ), является обычным явлением. Даже с -fstrict-aliasing допускается использование типа punning при условии, что доступ к памяти осуществляется через тип объединения. Таким образом, приведенный выше код будет работать так, как ожидалось. См. Структуры перечислений объединений и реализация битовых полей. Однако этот код может не быть:

int f() {
    union a_union t;
    int* ip;
    t.d = 3.0;
    ip = &t.i;
    return *ip;
}

Аналогично, доступ, взяв адрес, набрав результирующий указатель и разыменовывая результат, имеет поведение undefined, даже если cast использует тип объединения, например:

int f() {
    double d = 3.0;
    return ((union a_union *) &d)->i;
} 

Размещение нового

(Примечание: я собираюсь по памяти здесь, так как сейчас у меня нет доступа к стандарту). После размещения - новый объект в буфер хранения, срок жизни базовых объектов хранения заканчивается неявно. Это похоже на то, что происходит, когда вы пишете члену союза:

union {
    int i;
    float f;
} u;

// No member of u is active. Neither i nor f refer to an lvalue of any type.
u.i = 5;
// The member u.i is now active, and there exists an lvalue (object)
// of type int with the value 5. No float object exists.
u.f = 5.0f;
// The member u.i is no longer active,
// as its lifetime has ended with the assignment.
// The member u.f is now active, and there exists an lvalue (object)
// of type float with the value 5.0f. No int object exists.

Теперь посмотрим на что-то подобное с местом размещения new:

#define MAX_(x, y) ((x) > (y) ? (x) : (y))
// new returns suitably aligned memory
char* buffer = new char[MAX_(sizeof(int), sizeof(float))];
// Currently, only char objects exist in the buffer.
new (buffer) int(5);
// An object of type int has been constructed in the memory pointed to by buffer,
// implicitly ending the lifetime of the underlying storage objects.
new (buffer) float(5.0f);
// An object of type int has been constructed in the memory pointed to by buffer,
// implicitly ending the lifetime of the int object that previously occupied the same memory.

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

Ответ 2

Помимо ошибки при sizeof(T) > sizeof(U), проблема может заключаться в том, что объединение имеет соответствующее и, возможно, более высокое выравнивание, чем U, из-за T. Если вы не создаете экземпляр этого объединения, чтобы его блок памяти был выровнен (и достаточно большой!), А затем вытащил элемент с типом адресата T, он будет разбиваться тихо в худшем случае.

Например, возникает ошибка выравнивания, если вы выполняете листинг C-стиля U*, где U требуется выравнивание по 4 байта, до dummy_union*, где dummy_union требует выравнивания до 8 байтов, потому что alignof(T) == 8. После этого вы можете прочитать член объединения с типом T, выровненным на 4 вместо 8 байтов.


Псевдонимы (выравнивание и размер safe reinterpret_cast только для POD):

Это предложение явно нарушает строгий псевдоним, но со статическими утверждениями:

///@brief Compile time checked reinterpret_cast where destAlign <= srcAlign && destSize <= srcSize
template<typename _TargetPtrType, typename _ArgType>
inline _TargetPtrType alias_cast(_ArgType* const ptr)
{
    //assert argument alignment at runtime in debug builds
    assert(uintptr_t(ptr) % alignof(_ArgType) == 0);

    typedef typename std::tr1::remove_pointer<_TargetPtrType>::type target_type;
    static_assert(std::tr1::is_pointer<_TargetPtrType>::value && std::tr1::is_pod<target_type>::value, "Target type must be a pointer to POD");
    static_assert(std::tr1::is_pod<_ArgType>::value, "Argument must point to POD");
    static_assert(std::tr1::is_const<_ArgType>::value ? std::tr1::is_const<target_type>::value : true, "const argument must be cast to const target type");
    static_assert(alignof(_ArgType) % alignof(target_type) == 0, "Target alignment must be <= source alignment");
    static_assert(sizeof(_ArgType) >= sizeof(target_type), "Target size must be <= source size");

    //reinterpret cast doesn't remove a const qualifier either
    return reinterpret_cast<_TargetPtrType>(ptr);
}

Использование аргумента типа указателя (например, стандартные операторы литья, такие как reinterpret_cast):

int* x = alias_cast<int*>(any_ptr);

Другой подход (обходит проблемы выравнивания и сглаживания с использованием временного объединения):

template<typename ReturnType, typename ArgType>
inline ReturnType alias_value(const ArgType& x)
{
    //test argument alignment at runtime in debug builds
    assert(uintptr_t(&x) % alignof(ArgType) == 0);

    static_assert(!std::tr1::is_pointer<ReturnType>::value ? !std::tr1::is_const<ReturnType>::value : true, "Target type can't be a const value type");
    static_assert(std::tr1::is_pod<ReturnType>::value, "Target type must be POD");
    static_assert(std::tr1::is_pod<ArgType>::value, "Argument must be of POD type");

    //assure, that we don't read garbage
    static_assert(sizeof(ReturnType) <= sizeof(ArgType),"Target size must be <= argument size");

    union dummy_union
    {
        ArgType x;
        ReturnType r;
    };

    dummy_union dummy;
    dummy.x = x;

    return dummy.r;
}

Использование:

struct characters
{
    char c[5];
};

//.....

characters chars;

chars.c[0] = 'a';
chars.c[1] = 'b';
chars.c[2] = 'c';
chars.c[3] = 'd';
chars.c[4] = '\0';

int r = alias_value<int>(chars);

Недостатком этого является то, что объединение может потребовать больше памяти, чем фактически необходимо для ReturnType


Обернутая memcpy (обходит проблемы выравнивания и сглаживания с помощью memcpy):

template<typename ReturnType, typename ArgType>
inline ReturnType alias_value(const ArgType& x)
{
    //assert argument alignment at runtime in debug builds
    assert(uintptr_t(&x) % alignof(ArgType) == 0);

    static_assert(!std::tr1::is_pointer<ReturnType>::value ? !std::tr1::is_const<ReturnType>::value : true, "Target type can't be a const value type");
    static_assert(std::tr1::is_pod<ReturnType>::value, "Target type must be POD");
    static_assert(std::tr1::is_pod<ArgType>::value, "Argument must be of POD type");

    //assure, that we don't read garbage
    static_assert(sizeof(ReturnType) <= sizeof(ArgType),"Target size must be <= argument size");

    ReturnType r;
    memcpy(&r,&x,sizeof(ReturnType));

    return r;
}

Для массивов с динамическим размером любого типа POD:

template<typename ReturnType, typename ElementType>
ReturnType alias_value(const ElementType* const array,const size_t size)
{
    //assert argument alignment at runtime in debug builds
    assert(uintptr_t(array) % alignof(ElementType) == 0);

    static const size_t min_element_count = (sizeof(ReturnType) / sizeof(ElementType)) + (sizeof(ReturnType) % sizeof(ElementType) != 0 ? 1 : 0);

    static_assert(!std::tr1::is_pointer<ReturnType>::value ? !std::tr1::is_const<ReturnType>::value : true, "Target type can't be a const value type");
    static_assert(std::tr1::is_pod<ReturnType>::value, "Target type must be POD");
    static_assert(std::tr1::is_pod<ElementType>::value, "Array elements must be of POD type");

    //check for minimum element count in array
    if(size < min_element_count)
        throw std::invalid_argument("insufficient array size");

    ReturnType r;
    memcpy(&r,array,sizeof(ReturnType));
    return r;
}

Более эффективные подходы могут делать явные невыровненные чтения с внутренними функциями, такими как те, что используются в SSE, для извлечения примитивов.


Примеры:

struct sample_struct
{
    char c[4];
    int _aligner;
};

int test(void)
{
    const sample_struct constPOD    = {};
    sample_struct pod               = {};
    const char* str                 = "abcd";

    const int* constIntPtr  = alias_cast<const int*>(&constPOD);
    void* voidPtr           = alias_value<void*>(pod);
    int intValue            = alias_value<int>(str,strlen(str));

    return 0;
}

редактирует:

  • Утверждения для обеспечения преобразования только POD могут быть улучшены.
  • Удалены избыточные помощники шаблонов, теперь только с использованием только триангулярных признаков
  • Статические утверждения для разъяснения и запрета типа возвращаемого значения const (non-pointer)
  • Требования времени выполнения для отладочных построек
  • Добавлены атрибуты const для некоторых аргументов функции
  • Функция другого типа, использующая memcpy
  • Рефакторинг
  • Маленький пример

Ответ 3

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

Ответ 4

Мой вопрос в том, чтобы убедиться, что эта программа законна в соответствии со стандартом?

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

И если нет, как можно изменить это, чтобы быть законным?

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