Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

GCC and Clang disagree about C++17 constexpr lambda captures

Tags:

Consider this example which declares a variable as constexpr, captures it by copy in a lambda, and declares another constexpr variable which is the result of a constexpr function unwrapping a non-type template parameter from the original variable.

#include <utility>

template<int I>
constexpr auto unwrap(std::integral_constant<int, I>) {
  return I;
}

int main() {
  constexpr auto i = std::integral_constant<int, 42>{};
  constexpr auto l = [i]() {
    constexpr int x = unwrap(i);
  };
}

Clang (trunk) accepts this code. (wandbox)

GCC (trunk) fails with the following error message (wandbox):

lambda_capture.cpp:11:31: error: the value of ‘i’ is not usable in a constant expression
     constexpr int x = unwrap(i);
                               ^
lambda_capture.cpp:10:28: note: ‘i’ was not declared ‘constexpr’
   constexpr auto l = [i]() {

Which compiler is correct? It seems to me that this is a GCC bug, where the constexpr-ness of lambda captures is not correctly propagated to the lambda context.

like image 440
Jackie Avatar asked Jun 06 '17 09:06

Jackie


People also ask

Can a lambda be constexpr?

Visual Studio 2017 version 15.3 and later (available in /std:c++17 mode and later): A lambda expression may be declared as constexpr or used in a constant expression when the initialization of each data member that it captures or introduces is allowed within a constant expression.

Is constexpr always compile time?

A constexpr (which is short for “constant expression”) variable can only be a compile-time constant. If the initialization value of a constexpr variable is not a constant expression, the compiler will error.

What is the point of constexpr?

constexpr indicates that the value, or return value, is constant and, where possible, is computed at compile time. A constexpr integral value can be used wherever a const integer is required, such as in template arguments and array declarations.

Is constexpr implicitly Const?

In C++11, constexpr member functions are implicitly const.


1 Answers

Both implementations are bugged, but I'm inclined to think that GCC got the right answer here.


Dropping the capture of i causes Clang to refuse to compile the code. That means it clearly has a bug somewhere.

[expr.const]/2.12:

An 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:

  • [...]
  • in a lambda-expression, a reference to [...] a variable with automatic storage duration defined outside that lambda-expression, where the reference would be an odr-use;
  • [...]

Clang's behavior is schizophrenic: if the use of i in the body is not an odr-use, then it doesn't need to be captured, yet it rejects the code in the OP if the explicit capture is removed; OTOH, if it is an odr-use, then by the above unwrap(i) isn't a constant expression, and so it should reject the initialization of x.


GCC's lambda implementation is woefully bad with respect to odr-use. It does constant-folding ultra-early, resulting in all kinds of subtle mischief. On the other hand, for explicit captures it transforms all uses, whether or not it's actually an odr-use. The aggressive constant folding means that it accepts OP's code if the capture of i is removed.

Assuming that unwrap(i) does odr-use i, then it is correct that, per [expr.const]/2.12, OP's code is ill-formed.


Does unwrap(i) actually odr-use i? That question boils down to whether copy-initializing the parameter object of unwrap counts as applying an lvalue-to-rvalue conversion to i. I don't see anything in the standard that explicitly says that an lvalue-to-rvalue conversion is applied here, and instead [dcl.init]/17.6.2 indicates that we call a constructor (in this case, the trivial implicitly defined copy constructor) passing i as the argument bound to its parameter, and reference binding is a classic example of odr-use.

To be sure, applying an l-to-r conversion would result in a copy-initialization of an integral_constant<int, 42> object from i, but the problem here is that nothing in the standard says the converse - that all copy-initializations of an integral_constant<int, 42> object from i count as l-to-r conversions.

like image 117
T.C. Avatar answered Sep 19 '22 12:09

T.C.