How to solve the issue of "read of non-constexpr variable 'a' is not allowed in a constant expression" with boost.hana
Asked Answered
C

3

10

I'm using c++17 with Boost.hana to write some meta-programming programs. One issue stuck me is what kind of expression can be used in a constexpr context like static_assert. Here is an example:

#include <boost/hana.hpp>

using namespace boost::hana::literals;

template <typename T>
class X {
public:
    T data;

    constexpr explicit X(T x) : data(x) {}

    constexpr T getData() {
        return data;
    }
};


int main() {
    {   // test1
        auto x1 = X(1_c);
        static_assert(x1.data == 1_c);
        static_assert(x1.getData() == 1_c);
    }
    {   //test2.1
        auto x2 = X(boost::hana::make_tuple(1_c, 2_c));
        static_assert(x2.data[0_c] == 1_c);

        // static_assert(x2.getData()[0_c] == 1_c); // read of non-constexpr variable 'x2' is not allowed in a constant expression
    }
    {   //test2.2
        auto x2 = X(boost::hana::make_tuple(1_c, 2_c));
        auto data = x2.getData();
        static_assert(data[0_c] == 1_c);
    }
}

First I write a class X with a field data and an accessor getData(). In the main()'s test1 part, x1.data and x1.getData() behave same as I expected. But in the test2 part, changing the argument to a boost::hana's tuple, static_assert(x2.data[0_c] == 1_c) still behaves fine but static_assert(x2.getData()[0_c] == 1_c) fails compilation, with error of 'read of non-constexpr variable 'x2' is not allowed in a constant expression'. What weired is if I split x2.getData()[0_c] into auto data = x2.getData(); and static_assert(data[0_c] == 1_c); it compiles fine again. I'd expect they behave the same. So can anyone help explain why x2.getData()[0_c] can not be used in static_assert in this example?

To reproduce: clang++8.0 -I/path/to/hana-1.5.0/include -std=c++17 Test.cpp

Crankle answered 25/2, 2020 at 3:33 Comment(4)
Interestingly, GCC compiles it successfully. I have reduced the code to this shorter one.Carlow
constexpr is missing on x2 and data, const on getData. godbolt.org/z/ZNL2BKJamey
@Carlow Unfortunately I have only Clang to use for my target.Crankle
Related: https://mcmap.net/q/1166983/-templated-delegating-copy-constructor-in-constant-expressions/5376789Carlow
D
6

The problem is that boost::hana::tuple does not have a copy constructor.

It has a constructor that looks like a copy constructor:

template <typename ...dummy, typename = typename std::enable_if<
    detail::fast_and<BOOST_HANA_TT_IS_CONSTRUCTIBLE(Xn, Xn const&, dummy...)...>::value
>::type>
constexpr tuple(tuple const& other)
    : tuple(detail::from_index_sequence_t{},
            std::make_index_sequence<sizeof...(Xn)>{},
            other.storage_)
{ }

But since this is a template, it is not a copy constructor.

Since boost::hana::tuple does not have a copy constructor, one is declared implicitly and defined as defaulted (it is not suppressed since boost::hana::tuple does not have any copy or move constructors or assignment operators, because, you guessed it, they cannot be templates).

Here we see implementation divergence, demonstrated in the behavior of the following program:

struct A {
    struct B {} b;
    constexpr A() {};
    // constexpr A(A const& a) : b{a.b} {}    // #1
};
int main() {
    auto a = A{};
    constexpr int i = (A{a}, 0);
}

gcc accepts, while Clang and MSVC reject, but accept if line #1 is uncommented. That is, the compilers disagree on whether the implicitly-defined copy constructor of a non-(directly-)empty class is permissible to use within constant-evaluation context.

Per the definition of the implicitly-defined copy constructor there is no way that #1 is any different to constexpr A(A const&) = default; so gcc is correct. Note also that if we give B a user-defined constexpr copy constructor Clang and MSVC again accept, so the issue appears to be that these compilers are unable to track the constexpr copy constructibility of recursively empty implicitly copyable classes. Filed bugs for MSVC and Clang (fixed for Clang 11).

Note that the use of operator[] is a red herring; the issue is whether the compilers allow the call to getData() (which copy-constructs T) within a constant-evaluation context such as static_assert.

Obviously, the ideal solution would be for Boost.Hana to correct boost::hana::tuple such that it has actual copy/move constructors and copy/move assignment operators. (This would fix your use case since the code would be calling user-provided copy constructors, which are permissible in constant-evaluation context.) As a workaround, you could consider hacking getData() to detect the case of non-stateful T:

constexpr T getData() {
    if (data == T{})
        return T{};
    else
        return data;
}
Disadvantage answered 2/3, 2020 at 17:57 Comment(7)
I don't understand why the default copy constructor does not work... Can you try to reexplain it to me?Border
@AntoineMorrier I believe this to be a bug in clang; it appears to have difficulty recognizing that recursively empty classes can be copied in constexpr context.Disadvantage
Thanks. Very helpful answer! So can you further explain why the test2.2 part accepted by Clang? (I've edited the original question and splited test2 into test2.1 and test2.2) I'd expected they behave the same.Crankle
@Crankle the bit that Clang is unhappy with is the copy-construction of hana::tuple that occurs in the return from getData. In test2.2 the copy occurs outside constant-evaluation context, so Clang is fine with it.Disadvantage
er.. it's a bit difficult, at least for me to understand it.. getData() is not allowed here but get out and introducing a temperal then accepted..Crankle
@Crankle well, suppose that getData did something obviously non-constexpr like calling printf. It would still be OK to call getData() outside the static_assert and use the return value inside the static_assert, because the operator[] and operator== that you call inside the static_assert are usable in constant-evaluation context.Disadvantage
@Disadvantage amazing :)Crankle
B
1

The issue is because you are trying to retrieve a run time value and test it at compilation.

What you can do is to force the expression at compile time through a decltype and it will work like a charm :).

static_assert(decltype(x2.getData()[0_c]){} == 1_c);

#include <boost/hana.hpp>

using namespace boost::hana::literals;

template <typename T>
class X {
public:
    T data;

   constexpr explicit X(T x) : data(x) {}

   constexpr T getData() {
        return data;
    }
};


int main() {
    {   // test1
        auto x1 = X(1_c);
        static_assert(x1.data == 1_c);
        static_assert(x1.getData() == 1_c);
    }
    {   //test2
        auto x2 = X(boost::hana::make_tuple(1_c, 2_c));
        static_assert(x2.data[0_c] == 1_c);

         static_assert(decltype(x2.getData()[0_c]){} == 1_c);

        auto data = x2.getData();
        static_assert(data[0_c] == 1_c);
    }
}

Now the expression is evaluated at compile time, so the type is known at compile time and since it is constructible at compute time also, it is possible to use it within a static_assert

Border answered 28/2, 2020 at 10:42 Comment(2)
Thanks for the answer. I can understand using static_assert(decltype(x2.getData()[0_c]){} == 1_c) can work well, but I still want to save the decltype because that would save a lot. I guess you are saying x2.getData() is retrieving a runtime value so it should not appear in a static_assert expression. Then I don't understand why the x1.getData() in test1 part, and the data in data[0_c] in test2 part, can work well. What's their differences?Crankle
There really isn't any run-time value being accessed in any case, but perhaps the standard doesn't allow the compiler to check that.Bannasch
S
0

So first of all, you're missing const qualifier in getData() method, so it should be:

constexpr T getData() const

No variable is promoted, at least from standard point of view, to be constexpr if its not marked as constexpr.

Note that this is not necessary for x1 which is of type X specialized with hana::integral_constant, since result of 1_c is a type without user defined copy constructor, which does not contain any data internally, so a copy operation in getData() is in fact a no-op, so expression: static_assert(x1.getData() == 1_c); is fine, since there is no actual copy done (nor access to non-const this pointer of x1 is necessary).

This is very different for your container with hana::tuple which contains factual copy construction of hana::tuple from data in x2.data field. This requires factual access to your this pointer - which wasn't necessary in case of x1, which also was not a constexpr variable.

This means that you are expressing your intent wrong with both x1 and x2, and it is necessary, at least for x2, to mark these variables as constexpr. Note also, that using empty tuple, which is an basically empty (no user defined copy constructors) specialization of general hana::tuple, does work seamlessly (test3 section):

#include <boost/hana.hpp>

using namespace boost::hana::literals;

template <typename T>
class X {
public:
    T data;

    constexpr explicit X(T x) : data(x) {}

    constexpr T getData() const {
        return data;
    }
};

template<typename V>
constexpr auto make_X(V value)
{
    return value;
}

int main() {
    {   // test1
        auto x1 = X(1_c);
        static_assert(x1.data == 1_c);
        static_assert(x1.getData() == 1_c);
    }
    {   //test2
        constexpr auto x2 = X(boost::hana::make_tuple(1_c, 2_c));
        static_assert(x2.data[0_c] == 1_c);

        static_assert(x2.getData()[0_c] == 1_c); // read of non-constexpr variable 'x2' is not allowed in a constant expression

        auto data = x2.getData();
        static_assert(data[0_c] == 1_c);
    }
    {   //test3
        auto x3 = X(boost::hana::make_tuple());
        static_assert(x3.data == boost::hana::make_tuple());

        static_assert(x3.getData() == boost::hana::make_tuple());
    }
}
Spitz answered 28/2, 2020 at 15:31 Comment(6)
I did not read all the answer yet, but a constexpr method can be non const normally.Border
I disagree that x1 is an empty type. Any instance of X has a data member. Also hana::tuple containing empty types is itself empty as it uses empty base optimization. You might be on target blaming the copy constructor because Clang or libc++ might be doing something wonky with std::integral_constant.Bannasch
And, is there a way that I don't have to add a constexpr for x2 declaration? I'd like X can be initialized with both constant value and runtime value. Such as: ``` int g = 1; int main() { { /* test3 */ auto x3 = X(g); } } ``` I hope it can also work perfectly. But adding a constexpr to x3 will not compile, with error: constexpr variable 'x3' must be initialized by a constant expressionCrankle
@AntoineMorrier: Yes, but this is ok as long as you are not using const this pointer, and you are unfortunately using it with x2 in static_assert case. (in case of x1 - it's a further discussion : ) ).Mono
@JasonRice: Yes, that's true, I'll tweak the answer, since I wasn't precise: both of course do not have any non-static fields. Still, and this is what my answer lacks, please note that while hana::integral_constant has default, compiler-defined constructor, hana::tuple does have user defined one. Also, since there is a specialization for empty tuple, which doesn't have any constructor, same code for empty tuple works: godbolt.org/z/ZeEVQNMono
@Long: you can't expect the compiler to know what is going on in runtime, if something is decided there. In your example: when g is non-constexpr, compiler cannot be 100% sure that nothing in between its initialization and access in main will change it, at least in general: what you've showed above is potentially a special case.Mono

© 2022 - 2024 — McMap. All rights reserved.