Why static-sized array type cannot be a container type?

Viewed 170

I have an alias to a static-sized array, using it is easy and straightforward:

using triplet_t = std::uint8_t[3];

//           vvvvvvvvvvvvvvvvvv <--- easier than std::uint8_t(&triplet)[3]
void f(const triplet_t &triplet) { /* whatever */ }

triplet_t t{}; // As good as std::uint8_t t[3]{};

t[0] = '0';
t[1] = '1';
t[2] = '2';
for (auto &v : t) std::cout << v << ' ';
std::cout << '\n';

// So far so good...
triplet_t t3[3]{};
for (auto &r : t3)
    for(auto &v : r)
        v = 42;

I can even use the alias in containers:

std::vector<triplet_t> vt;

Or so I used to think, because as soon as you use vt it fails:

vt.push_back({});

GCC 8.0.0 201711

error: parenthesized initializer in array new [-fpermissive]
{ ::new((void *)__p) _Up(std::forward<_Args>(__args)...); }
  ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

error: request for member '~unsigned char [3]' in '* __p', which is of non-class type 'unsigned char [3]'
destroy(_Up* __p) { __p->~_Up(); }
                    ~~~~~~^~~

The issue seems to be that after unrolling all the template trickery a placement-new is called forwarding all the parameters provided parenthesized, and obviously this is not the way to initialize a static-sized array.

Also, somehow the container thinks of triplet_t as an object and hence is asking for a destructor, failing again to compile. The issue is obviously the same without the alias:

std::vector<std::uint8_t[3]> vt;
vt.push_back({});          // Boom!
vt.push_back({255, 0, 0}); // Ouch!

But no problem using a struct with the same memory layout:

struct rgb { std::uint8_t r, g, b; };
std::vector<rgb> vt;
vt.push_back({});          // Nice!
vt.push_back({255, 0, 0}); // Cool!

I wonder why this happens, is there a way to use static-sized arrays as contained type in containers?

2 Answers

Reading std::vector documentaion, you can find that T must meet the requirements of CopyAssignable and CopyConstructible.

This means (simplifying): with v and t two instances of type T, the expression t = v must be legal. Clearly, if T is a native array, this is not the case (you cannot assign a C-array to another), and certain functions of std::vector<T> would be ill-formed.

A solution would be to define triplet_t as:

using triplet_t = std::array<std::uint8_t, 3>;

void f(const triplet_t &triplet) { /* whatever */ }

triplet_t t{};

t[0] = '0';
t[1] = '1';
t[2] = '2';
for (auto &v : t) std::cout << v << ' ';
std::cout << '\n';

// So far so good...
triplet_t t3[3]{};
for (auto &r : t3)
    for(auto &v : r)
        v = 42;

std::vector<triplet_t> vt;

vt.push_back({});

According to the documentation, push_back requires the value type to be either CopyInsertable or MoveInsertable. Let's see the definition:

The type T is CopyInsertable into the container X whose value_type is identical to T if T is MoveInsertable into X, and, given [...] the following expression is well-formed:

std::allocator_traits<A>::construct(m, p, v);

So in the case of a C-array, one has, for the standard allocator, an expression like

::new((void *)p) int[3](std::forward<int[3]>(v))

where v is of the array type. This is ill-formed, according to the specification of new:

  • If type is an array type, an array of objects is initialized.
    • If initializer is absent, each element is default-initialized
    • If initializer is an empty pair of parentheses, each element is value-initialized.
    • If initializer is a brace-enclosed list of arguments, the array is aggregate-initialized.

There is no syntax for array types allowing for non-empty parantheses.

The argumentation goes quite similarily with the MoveInsertable category. All in all, the proposed solution would be the (already mentioned) use of std::array which is itself not of array type and therefore can be initialized properly by the syntax taken by standard allocators' construct function.

One final note: the strict aliasing rules only allow for conversions of any type with unsigned char, signed char or char. While almost certain that std::uint8_t is just an alias typedef on one of those in your implementation, there is no guarantee in the standard for this.

Related