A colleague showed me a C++20 program where a closure object is virtually created using std::bit_cast
from the value that it captures:
#include <bit>
#include <iostream>
class A {
int v;
public:
A(int u) : v(u) {}
auto getter() const {
if ( v > 0 ) throw 0;
return [this](){ return v; };
}
};
int main() {
A x(42);
auto xgetter = std::bit_cast<decltype(x.getter())>(&x);
std::cout << xgetter();
}
Here main
function cannot call x.getter()
due to exception. Instead it calls std::bit_cast
taking as template argument the closure type decltype(x.getter())
and as ordinary argument the pointer &x
being captured for new closure object xgetter
. Then xgetter
is called to obtain the value of object x
, which is otherwise not accessible in main
.
The program is accepted by all compilers without any warnings and prints 42
, demo: https://gcc.godbolt.org/z/a479689Wa
But is the program well-formed according to the standard and is such 'construction' of lambda objects valid?
Lamda expression is a new tool in C++11 to create unnamed function object or closure. Defining a lamda expression is similar to a function without the function name. In addition lamda can use variables from the scoping environment.
Capture Clause A lambda can introduce new variables in its body (in C++14), and it can also access, or capture, variables from the surrounding scope. A lambda begins with the capture clause (lambda-introducer in the Standard syntax), which specifies which variables are captured, and whether the capture is by value or by reference.
Visual Studio 2017 version 15.3 and later (available in /std:c++17 mode and later): You may declare a lambda expression as constexpr (or use it in a constant expression) when the initialization of each captured or introduced data member is allowed within a constant expression.
Here's an example of using a function pointer with a capture-less lambda: This works because a lambda that doesn't have a capture group doesn't need its own class--it can be compiled to a regular old function, allowing it to be passed around just like a normal function.
But is the program well-formed according to the standard ...
The program has undefined behaviour conditional on leeway given to implementors. Particularly conditional on whether the closure type of the lambda
[this](){ return v; };
is trivially copyable from; as per [expr.prim.lambda.closure]/2:
The closure type is declared in the smallest block scope, class scope, or namespace scope that contains the corresponding lambda-expression. [...] The closure type is not an aggregate type. An implementation may define the closure type differently from what is described below provided this does not alter the observable behavior of the program other than by changing:
- (2.1) the size and/or alignment of the closure type,
- (2.2) whether the closure type is trivially copyable ([class.prop]), or
- (2.3) whether the closure type is a standard-layout class ([class.prop]). [...]
This means that whether the constraints of [bit.cast]/1 are fulfilled or not:
template<class To, class From> constexpr To bit_cast(const From& from) noexcept;
Constraints:
- (1.1)
sizeof(To) == sizeof(From)
istrue
;- (1.2)
is_trivially_copyable_v<To>
istrue
; and- (1.3)
is_trivially_copyable_v<From>
istrue
.
is implementation-defined.
... and is such 'construction' of lambda objects valid?
As [expr.prim.lambda.closure]/2.1 also states that the size and alignment of the closure type is implementation-defined, using std::bit_cast
to create an instance of the closure type may result in a program with undefined behavior, as per [bit.cast]/2:
Returns: An object of type
To
. Implicitly creates objects nested within the result ([intro.object]). Each bit of the value representation of the result is equal to the corresponding bit in the object representation of from. Padding bits of the result are unspecified. For the result and each object created within it, if there is no value of the object's type corresponding to the value representation produced, the behavior is undefined. If there are multiple such values, which value is produced is unspecified.
For any kind of practical use, however, I'd argue that if a construct has undefined behavior conditional on implementation leeway details (unless these can be queried with say traits), then the construct should reasonably be considered to have undefined behavior, except possibly for a compiler's internal C++ (e.g. Clang frontend) implementation, where these implementation details are known.
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