строгое наложение и выравнивание - PullRequest
19 голосов
/ 01 апреля 2012

Мне нужен безопасный способ для псевдонима между произвольными типами POD, в соответствии с ISO-C ++ 11, явно учитывая 3.10 / 10 и 3.11 из n3242 или более поздней версии.Здесь много вопросов о строгом псевдониме, большинство из которых касаются C, а не 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

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

Ответы [ 4 ]

14 голосов
/ 02 апреля 2012

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

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

Пример

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

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 и C ++.

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

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

Исключения из строгого алиасинга

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

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

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 допускается перетаскивание типов при условии, что доступ к памяти осуществляется через тип объединения. Таким образом, приведенный выше код будет работать как положено. См. Перечисления структурных объединений и реализация битовых полей. Тем не менее, этот код может не:

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

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

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.

Теперь давайте посмотрим на что-то похожее с новым размещением:

#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.

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

6 голосов
/ 01 апреля 2012

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

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


Приведение псевдонима (выравнивание и размер, безопасный 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, могут быть улучшены.
  • Удалены лишние помощники шаблонов, теперь используются только черты tr1
  • Статические утверждения дляразъяснение и запрещение константного значения (без указателя) тип возвращаемого значения
  • Утверждения времени выполнения для отладочных сборок
  • Добавлены квалификаторы const к некоторым аргументам функции
  • Другой тип функции штамповки с использованием memcpy
  • Рефакторинг
  • Маленький пример
4 голосов
/ 01 апреля 2012

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

2 голосов
/ 01 апреля 2012

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

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

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

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

...