С++ 11: reinterpreting массив структур как массив элемента struct

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

struct S
{
    char v;
};

Учитывая массив const S, можно ли стандартным образом интерпретировать его как массив const char, элементы которого соответствуют значению члена v для каждого из исходных элементов массива, и наоборот? Например:

const S a1[] = { {'a'}, {'4'}, {'2'}, {'\0'} };
const char* a2 = reinterpret_cast< const char* >(a1);

for (int i = 0; i < 4; ++i)
    std::cout << std::boolalpha << (a1[i].v == a2[i]) << ' ';

Является ли приведенный выше код выше и будет ли он печатать true true true true? Если нет, есть ли другой способ достижения этого?

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

Ответ 1

Формально struct может иметь дополнение, чтобы его размер был больше 1.

I.e., формально вы не можете reinterpret_cast и иметь полностью переносимый код, за исключением массива ¹an только одного элемента.

Но для практики, несколько лет назад кто-то спросил, есть ли теперь какой-нибудь компилятор, который по умолчанию дал бы sizeof(T) > 1 для struct T{ char x; };. Я еще не видел ни одного примера. Поэтому на практике можно просто static_assert, чтобы размер был равен 1, и не волнуйтесь, что этот static_assert не будет работать в какой-либо системе.

т.е.

S const a1[] = { {'a'}, {'4'}, {'2'}, {'\0'} };
static_assert( sizeof( S ) == 1, "!" );

char const* const a2 = reinterpret_cast<char const*>( a1 );

for( int i = 0; i < 4; ++i )
{
    assert( a1[i].v == a2[i] );
}

Так как возможно интерпретировать С++ 14 и более поздние стандарты таким образом, что индексирование имеет Undefined Behavior, основанный на своеобразной интерпретации "массива" как относящийся к некоторому исходному массиву, можно вместо этого написать этот код в более неудобном и подробном, но гарантированном действительном способе:

// I do not recommend this, but it one way to avoid problems with some compiler that's
// based on an unreasonable, impractical interpretation of the C++14 standard.
#include <assert.h>
#include <new>

auto main() -> int
{
    struct S
    {
        char v;
    };

    int const compiler_specific_overhead    = 0;    // Redefine per compiler.
    // With value 0 for the overhead the internal workings here, what happens
    // in the machine code, is the same as /without/ this verbose work-around
    // for one impractical interpretation of the standard.
    int const n = 4;
    static_assert( sizeof( S ) == 1, "!" );
    char storage[n + compiler_specific_overhead]; 
    S* const a1 = ::new( storage ) S[n];
    assert( (void*)a1 == storage + compiler_specific_overhead );

    for( int i = 0; i < n; ++i ) { a1[i].v = "a42"[i]; }    //  Whatever

    // Here a2 points to items of the original `char` array, hence no indexing
    // UB even with impractical interpretation of the C++14 standard.
    // Note that the indexing-UB-free code from this point, is exactly the same
    // source code as the first code example that some claim has indexing UB.
    char const* const a2 = reinterpret_cast<char const*>( a1 );

    for( int i = 0; i < n; ++i )
    {
        assert( a1[i].v == a2[i] );
    }
}

Примечания:
¹ Стандарт гарантирует отсутствие прокладки в начале struct.

Ответ 2

Тривиально, no - struct может иметь отступы. И эта квартира нарушает любую интерпретацию как массив.

Ответ 3

Арифметика указателя в a2[i] равна undefined, см. С++ 14 5.7 [expr.add] p7:

Для сложения или вычитания, если выражения P или Q имеют тип "указатель на cv T", где T и тип элемента массива не похожи (4.5), поведение undefined. [Примечание. В частности, указатель на базовый класс не может использоваться для арифметики указателя, когда массив содержит объекты производного типа класса. - конечная нота]

Из-за этого правила, даже если нет прокладки и соответствия размеров, анализ псевдонимов на основе типов позволяет компилятору предположить, что a1[i] и a2[i] не перекрываются (поскольку арифметика указателя действительна только в том случае, если a2 действительно представляет собой массив char, а не только что-то с тем же размером и выравниванием, и если это действительно массив из char, он должен быть отдельным объектом из массива S).

Ответ 4

Я думаю, что я бы склонен использовать преобразование времени компиляции, если исходные данные постоянны:

#include <iostream>
#include <array>

struct S
{
    char v;
};

namespace detail {
    template<std::size_t...Is>
    constexpr auto to_cstring(const S* p, std::index_sequence<Is...>)
    {
        return std::array<char, sizeof...(Is)> {
            p[Is].v...
        };
    }
}

template<std::size_t N>
constexpr auto to_cstring(const S (&arr)[N])
{
    return detail::to_cstring(arr, std::make_index_sequence<N>());
}

int main()
{
    const /*expr if you wish*/ S a1[] = { {'a'}, {'4'}, {'2'}, {'\0'} };

    const /*expr if you wish*/ auto a2 = to_cstring(a1);


    for (int i = 0; i < 4; ++i)
        std::cout << std::boolalpha << (a1[i].v == a2[i]) << ' ';
}

выход:

true true true true

даже когда данные не являются constexpr, gcc и clang довольно хороши при постоянных сложностях сложения, подобных этому.