Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Integral constant passed by value, treated as constexpr?

Although I've used code like this before, and it's clear that the compiler has enough information to work, I don't really understand why this compiles:

template <class T, class I>
auto foo(const T& t, I i) {
    return std::get<i>(t);
}

int main()
{
    std::cerr << foo(std::make_tuple(3,4), std::integral_constant<std::size_t, 0>{});
    return 0;
}

Live example: http://coliru.stacked-crooked.com/a/fc9cc6b954912bc5.

Seems to work with both gcc and clang. The thing is that while integral_constant has a constexpr conversion to the stored integer, constexpr member functions implicitly take the object itself as an argument, and therefore such a function cannot be used in a constexpr context unless the object we're calling the member function itself can be treated as constexpr.

Here, i is an argument passed to foo, and therefore i most certainly cannot be treated as constexpr. Yet, it is. An even simpler example:

template <class I>
void foo(I i) {
    constexpr std::size_t j = i;
}

This compiles too, as long as std::integral_constant<std::size_t, 0>{} is passed to foo.

I feel like I'm missing something obvious about the constexpr rules. Is there an exception for stateless types, or something else? (or, maybe, a compiler bug in two major compilers? This code seems to work on clang 5 and gcc 7.2).

Edit: an answer has been posted, but I don't think it's quite sufficient. In particular, given the last definition of foo, why does:

foo(std::integral_constant<std::size_t, 0>{});

Compile, but not:

foo(0);

Both 0 and std::integral_constant<std::size_t, 0>{} are constant expressions.

Edit 2: It seems like it boils down to the fact that calling a constexpr member function even on an object that is not a constant expression, can itself be regarded as a constant expression, as long as this is unused. This is being taken as obvious. I don't consider this obvious:

constexpr int foo(int x, int y) { return x; }

constexpr void bar(int y) { constexpr auto x = foo(0, y); }

This does not compile, because y as passed into foo is not a constant expression. It's being unused does not matter. Therefore, a complete answer needs to show some kind of language from the standard to justify the fact that a constexpr member function can be used as a constant expression, even on a non-constant expression object, as long as this is unused.

like image 535
Nir Friedman Avatar asked Sep 18 '17 22:09

Nir Friedman


2 Answers

The rules for compile time constant expressions changed with constexpr, A LOT, but they aren't new. Before constexpr, there were already compile time constant expressions... and the old rules are preserved as special cases in the new specification, to avoid breaking huge amounts of existing code.

For the most part, the old rules dealt with compile-time constants of integral type aka integral constant expression... which is exactly the situation you're dealing with. So no, there isn't anything weird in the constexpr rules... it's the other older rules kicking in that have nothing to do with constexpr.

A conditional-expression e is a core constant expression unless the evaluation of e, following the rules of the abstract machine, would evaluate one of the following expressions:

...

  • an lvalue-to-rvalue conversion unless it is applied to
    • a non-volatile glvalue of integral or enumeration type that refers to a complete non-volatile const object with a preceding initialization, initialized with a constant expression, or
    • a non-volatile glvalue that refers to a subobject of a string literal, or
    • a non-volatile glvalue that refers to a non-volatile object defined with constexpr, or that refers to a non-mutable subobject of such an object, or
    • a non-volatile glvalue of literal type that refers to a non-volatile object whose lifetime began within the evaluation of e;

You're right that the third subbullet does not apply. But the first one does.

So you have an interesting interplay between new rules which allow function returns to be compile-time constant, depending on the rules for evaluation on the abstract machine, and the legacy behavior allowing integral values to be compile-time constant even if not marked as such.


Here's a quick example why it doesn't matter that this is an implicit argument. Being an argument doesn't mean an object is evaluated:

constexpr int blorg(bool const flag, int const& input)
{
    return flag? 42: input;
}

int i = 5; // 5 is an integral constant expression, but `i` is not
constexpr int x = blorg(true, i); // ok, `i` was an argument but never evaluated
constexpr int y = blorg(false, i); // no way

For std::integral_constant member functions, you can consider *this like i in the blorg function -- if execution doesn't dereference it, it's ok for it to be passed around without being a compile-time constant.

like image 153
Ben Voigt Avatar answered Oct 07 '22 13:10

Ben Voigt


The reason this works:

template <class T, class I>
auto foo(const T& t, I i) {
    return std::get<i>(t);
}

is because none of the reasons that it would fail apply. When i is a std::integral_constant<size_t, S>, i can be used as a converted constant expression of type size_t because that expression goes through constexpr operator size_t(), which simply returns a template parameter (which is a prvalue) as a value. That's a perfectly valid constant expression. Note that this is not referenced - just because it's a member function does not, in of itself, violate the constant expression constraint.

Basically, there's nothing "runtimey" about i here.

On the flip side, if i were an int (by way of foo(0)), then calling std::get<i> would involve an lvalue-to-rvalue conversion for i, but this case would not meet any of the criteria since i does not have preceding initialization with a constant-expression, it is not a string literal, it wasn't defined with constexpr, and it didn't begin its lifetime in this expression.

like image 40
Barry Avatar answered Oct 07 '22 12:10

Barry