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.
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With