Array is deceptively simple, but how are its constructors and destructors implicitly declared? Why is array constexpr but vector isn’t, and what is aggregate-initialization? Understanding Array requires a strong comprehension of these often over-looked language features.

Array is a container encapsulating fixed sized arrays and do not decay to T* unlike C arrays. It provides iterators, element-wise access, member types, everything you would expect from a standard library container. Its templated on the type, typename T, and the size, std::size_t N. But it has a few oddities; the constructor and assignment operator are implicitly declared, and how are begin() and end() defined when the size is zero?

How its used

To construct an array we use aggregate-initialization, but this leads to a few potential problems. We can initialize values with less than three elements. In this scenario, the unspecified elements are value-initialized, which in the case of double falls back to zero-initialization.

template <typename Value, std::size_t N>
void print(const learn::array<Value, N>& values) {
    for (const auto& value: values) {
        std::cout << value << ' ';
    }

    std::cout << '\n';
}

learn::array<int, 3> values = {0, -2, 3};
print(values); // 0 -2 3

values = {3, 1};
print(values) // 3 1 0

This means that when we construct an array with no values, value-initialization is performed on all elements.

learn::array<double, 3> values2;

print(values) // 0.0 0.0 0.0

We can then access the values with get or operator[]. It also has iterators which lets it be used in range-for loops. As get is templated on the index, it will fail at compile-time if its accessed out of bounds. Modern compilers and versions of std::array will do this with operator[].

const auto& a = std::get<0>(values);
const auto& b = values[1];

for (const auto& c: values) {
    print(c);
}

How it works

array is a struct, containing a C-style array. We can perform aggregate-initialization as array has,

  • no private or protected non-static data members
  • no user-provided, inherited, or explicit constructors (explicitly defaulted or deleted constructors are allowed)
  • no virtual, private, or protected (since C++17) base classes
  • no virtual member functions

. We define internal C-style array so that it contains one element when the size is zero. This lets array have a size of zero and have defined behavior. This is in contrast to ISO C / C++ which does not allow arrays of size zero.

template <typename Value, std::size_t Size>
struct array {
    using value_type = Value;
    
    // some type declerations... 
    // some member function declerations...

    value_type elems_[Size > 0 ? Size : 1];
};

By defining the size of elems_ this way we can define the begin and end iterators, which are valid even when Size == 0.

    // some member function declerations...

    constexpr iterator begin() noexcept { return iterator(elems_); }

    constexpr iterator end() { return iterator(elems_ + Size); }
    value_type elems_[Size > 0 ? Size : 1];
};