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?
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.