Разрешают ли строгие правила псевдонимов в C++11 доступ к uint64_t через char *, char(&)[N], даже std:: array<char, N>& с -fstrict-aliasing -Wstrict-aliasing=2?

Согласно этому стеку потока ответ о строгих правилах псевдонимов C++ 11/14:

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

  • динамический тип объекта,

  • cv-квалифицированная версия динамического типа объекта,

  • тип, подобный (как определено в 4.4) динамическому типу объекта,
  • тип, который является типом со знаком или без знака, соответствующим динамическому типу объекта,
  • тип, который является типом со знаком или без знака, соответствующим cv-квалифицированной версии динамического типа объекта,
  • агрегатный или объединенный тип, который включает в себя один из вышеупомянутых типов среди своих элементов или нестатических элементов данных (включая, рекурсивно, элемент или нестатический элемент данных субагрегата или содержащего объединения),
  • тип, который является (возможно, квалифицированным cv) типом базового класса динамического типа объекта,
  • char или же unsigned char тип.

можем ли мы получить доступ к хранилищу другого типа, используя

(1) char *

(2) char(&)[N]

(3) std::array<char, N> &

без зависимости от неопределенного поведения?

constexpr uint64_t lil_endian = 0x65'6e'64'69'61'6e; 
    // a.k.a. Clockwise-Rotated Endian which allocates like
    // char[8] = { n,a,i,d,n,e,\0,\0 }

const auto& arr =   // std::array<char,8> &
    reinterpret_cast<const std::array<char,8> &> (lil_endian);

const auto& carr =  // char(&)[8]>
    reinterpret_cast<const char(&)[8]>           (lil_endian);

const auto* p =     // char *
    reinterpret_cast<const char *>(std::addressof(lil_endian));

int main()
{
    const auto str1  = std::string(arr.crbegin()+2, arr.crend() );

    const auto str2  = std::string(std::crbegin(carr)+2, std::crend(carr) );

    const auto sv3r  = std::string_view(p, 8);
    const auto str3  = std::string(sv3r.crbegin()+2, sv3r.crend() );

    auto lam = [](const auto& str) {
        std::cout << str << '\n'
                  << str.size() << '\n' << '\n' << std::hex;
        for (const auto ch : str) {
            std::cout << ch << " : " << static_cast<uint32_t>(ch) << '\n';
        }
        std::cout << '\n' << '\n' << std::dec;
    };

    lam(str1);
    lam(str2);
    lam(str3);
}

все лямбда-вызовы производят:

endian
6

e : 65
n : 6e
d : 64
i : 69
a : 61
n : 6e

https://gcc.godbolt.org/g/cdDTAM (включить -fstrict-aliasing -Wstrict-aliasing=2)

https://wandbox.org/permlink/pGvPCzNJURGfEki7

2 ответа

char(&)[N] случай и std::array<char, N> оба случая приводят к неопределенному поведению. Причина уже указана вами. Обратите внимание, ни char(&)[N] ни std::array<char, N> тот же тип, что и char,

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

В любом случае, если вы хотите получить доступ к базовым байтам объекта, используйте std::memcpy, как прямо сказано в стандарте в [basic.types] / 2:

Для любого объекта (кроме подобъекта базового класса) тривиально копируемого типа T, независимо от того, содержит ли объект допустимое значение типа T, базовые байты ([intro.memory]), составляющие объект, могут быть скопированы в массив символов char, unsigned char или std​::​byte ([Cstddef.syn]). Если содержимое этого массива копируется обратно в объект, объект должен впоследствии сохранить свое первоначальное значение. [ Пример:

#define N sizeof(T)
char buf[N];
T obj;                          // obj initialized to its original value
std::memcpy(buf, &obj, N);      // between these two calls to std​::​memcpy, obj might be modified
std::memcpy(&obj, buf, N);      // at this point, each subobject of obj of scalar type holds its original value

- конец примера]

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

Тем не менее, разрешено читать представление объекта в памяти. Представление объекта в памяти представляет собой последовательностьunsigned char [Basic.types]/4:

Объектное представление объекта типа T является последовательностью N unsigned char объекты, занятые объектом типа T, где N равно sizeof(T), Представление значения объекта - это набор битов, которые содержат значение типа T.

Соответственно в вашем примере:

  • lam(str1) UB (неопределенное поведение);
  • lam(str2) UB (массив и его первый элемент не являются взаимозаменяемыми по указателю);
  • lam(str3) не указано как UB в стандарте, если вы замените char от unsigned char Можно утверждать, что вы читаете представление объекта. (он также не определен, но он должен работать на всех компиляторах)

Таким образом, используя третий случай и изменяя объявление p в const unsigned char* должен всегда давать ожидаемый результат. В остальных двух случаях он может работать с этим простым примером, но может сломаться, если код более сложный или в более новой версии компилятора.


(*) Из этого правила есть два исключения: одно для членов профсоюзов с общей последовательностью инициализации; и один для массива unsigned char или же std::byte это обеспечивает хранение для другого объекта.

Другие вопросы по тегам