Filling a std::array at compile time and possible undefined behaviour with const_cast
Asked Answered
Y

3

6

It is known that std::array::operator[] since C++14 is constexpr, see declaration below:

constexpr const_reference operator[]( size_type pos ) const; 

However, it is also const qualified. This causes implications if you want to use the subscript operator of a std::array in order to assign values to your array at compile time. For example consider the following user literal:

template<typename T, int N>
struct FooLiteral {
  std::array<T, N> arr;
  constexpr FooLiteral() : arr {} { for(int i(0); i < N; ++i) arr[i] = T{42 + i}; }
};

The above code won't compile if you try to declare a constexpr variable of type FooLiteral. This is attributed to the fact that overload resolution rules qualify the non-const qualified, non-constexpr overload of the array's subscript operator as a better match. Thus the compiler complains about calling a non-constexpr function.

Live Demo

I can't figure out what was the reason for the commitee to declare this overload as const qualified for C++14, however it seems that the implication is being noticed and there's also a proposal p0107R0 to fix this in the upcomming C++17.

My natural though to overcome this for C++14 was to somehow hack the expression, in order to evoke the correct subscript operator. What I did is the following:

template<typename T, int N>
struct FooLiteral {
  std::array<T, N> arr;
  constexpr FooLiteral() : arr {} { 
    for(int i(0); i < N; ++i) {
      const_cast<T&>(static_cast<const std::array<T, N>&>(arr)[i]) = T{42 + i};
    }
  }
};

Live Demo

That is I casted the array to const reference to evoke the correct subscript operator overload and then I const_cast the returned object of the overloaded subscript operator to T& in order remove its const-ness and be able to assign to it.

This works fine, but I know that const_cast should be used with caution and to be frank I have second thoughts about if this hack can cause undefined behaviour.

Intuitively, I don't think there's a problem, since this const_cast is taking place at compile time initialization thus, I can't think of an implication that can arise at this state.

But is that so, or am I wrong and this introduce UB to the program?

Q:

Can someone justify if this is a UB or not?

Yevetteyew answered 17/12, 2015 at 15:27 Comment(0)
G
7

As far as I can tell this is not undefined behavior. The proposal that added constexpr to operator[] happened before the changes that removed the implicit const from constexpr member functions. So it looks like they just added on constexpr without reflecting on the need for keeping const or not.

We can see form an earlier version of Relaxing constraints on constexpr functions that it says the following about mutating literals within a constant expression:

Objects created within a constant expression can be modified within the evalution of that constant expression (including the evaluation of any constexpr function calls it makes), until the evaluation of that constant expression ends, or the lifetime of the object ends, whichever happens sooner. They cannot be modified by later constant expression evaluations. [...]

This approach allows arbitrary variable mutations within an evaluation, while still preserving the essential property that constant expression evaluation is independent of the mutable global state of the program. Thus a constant expression evaluates to the same value no matter when it is evaluated, excepting when the value is unspecified (for instance, floating-point calculations can give different results and, with these changes, differing orders of evaluation can also give different results).

and we can see the earlier proposal I referenced points out the const_cast hack and it says:

In C++11, constexpr member functions are implicitly const. This creates problems for literal class types which desire to be usable both within constant expressions and outside them:

[...]

Several alternatives have been suggested to resolve this problem:

  • Accept the status quo, and require users to work around this minor embarrassment with const_cast.
Glower answered 17/12, 2015 at 18:13 Comment(0)
B
1

No UB here, your member arr is non constant, you can "play" with its constness at will (well sort of, you get what I mean)

If your member was a constant expression then you'd have UB, because you have already initialized in the initalizer list and post creation you are not allowed to assume it has mutable value. Do whatever metaprogramming mumbo jumbo you wish inside the initializer list.

Brei answered 17/12, 2015 at 15:42 Comment(1)
It seems that you say OP cannot use it to initialize a constexpr FooLiteral, as for const FooLiteral, member cannot be modified...Renata
D
1

Not a direct answer to the question but hopefully something useful.

Having been troubled by std::array for a while I decided to see if it can be done better using user-code only.

Turns out it can:

#include <iostream>
#include <utility>
#include <cassert>
#include <string>
#include <vector>
#include <iomanip>

// a fully constexpr version of array that allows incomplete
// construction
template<size_t N, class T>
struct better_array
{
    // public constructor defers to internal one for
    // conditional handling of missing arguments
    constexpr better_array(std::initializer_list<T> list)
    : better_array(list, std::make_index_sequence<N>())
    {

    }

    constexpr T& operator[](size_t i) noexcept {
        assert(i < N);
        return _data[i];
    }

    constexpr const T& operator[](size_t i) const noexcept {
        assert(i < N);
        return _data[i];
    }

    constexpr T* begin() {
        return std::addressof(_data[0]);
    }

    constexpr const T* begin() const {
        return std::addressof(_data[0]);
    }

    constexpr T* end() {
        // todo: maybe use std::addressof and disable compiler warnings
        // about array bounds that result
        return &_data[N];
    }

    constexpr const T* end() const {
        return &_data[N];
    }

    constexpr size_t size() const {
        return N;
    }

private:

    T _data[N];

private:

    // construct each element from the initialiser list if present
    // if not, default-construct
    template<size_t...Is>
    constexpr better_array(std::initializer_list<T> list, std::integer_sequence<size_t, Is...>)
    : _data {
        (
         Is >= list.size()
         ?
         T()
         :
         std::move(*(std::next(list.begin(), Is)))
         )...
    }
    {

    }
};

// compute a simple factorial as a constexpr
constexpr long factorial(long x)
{
    if (x <= 0) return 0;

    long result = 1;
    for (long i = 2 ; i <= x ; result *= i)
        ++i;
    return result;
}

// compute an array of factorials - deliberately mutating a default-
// constructed array
template<size_t N>
constexpr better_array<N, long> factorials()
{
    better_array<N, long> result({});
    for (long i = 0 ; i < N ; ++i)
    {
        result[i] = factorial(i);
    }
    return result;
}

// convenience printer
template<size_t N, class T>
inline std::ostream& operator<<(std::ostream& os, const better_array<N, T>& a)
{
    os << "[";
    auto sep = " ";
    for (const auto& i : a) {
        os << sep << i;
        sep = ", ";
    }
    return os << " ]";
}

// for testing non-integrals
struct big_object
{
    std::string s = "defaulted";
    std::vector<std::string> v = { "defaulted1", "defaulted2" };
};

inline std::ostream& operator<<(std::ostream& os, const big_object& a)
{
    os << "{ s=" << quoted(a.s);
    os << ", v = [";
    auto sep = " ";
    for (const auto& s : a.v) {
        os << sep << quoted(s);
        sep = ", ";
    }
    return os << " ] }";
}

// test various uses of our new array
auto main() -> int
{
    using namespace std;

    // quick test
    better_array<3, int> x { 0, 3, 2 };
    cout << x << endl;

    // test that incomplete initialiser list results in a default-constructed object
    better_array<2, big_object> y { big_object { "a", { "b", "c" } } };
    cout << y << endl;

    // test constexpr construction using mutable array
    // question: how good is this optimiser anyway?
    cout << factorials<10>()[5] << endl;

    // answer: with apple clang7, -O3 the above line
    // compiles to:
    //  movq    __ZNSt3__14coutE@GOTPCREL(%rip), %rdi
    //  movl    $360, %esi              ## imm = 0x168
    //  callq   __ZNSt3__113basic_ostreamIcNS_11char_traitsIcEEElsEl
    // so that's pretty good!


    return 0;
}
Dominions answered 20/12, 2015 at 12:2 Comment(1)
Why not indeed, very nice! If groups.google.com/a/isocpp.org/d/msg/sg14/GpbifA4v9Z4/… gets accepted it provides a generic alternativeEncamp

© 2022 - 2024 — McMap. All rights reserved.