14

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?

463035818_is_not_an_ai
  • 109,796
  • 11
  • 89
  • 185
Fedor
  • 17,146
  • 13
  • 40
  • 131

1 Answers1

15

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) is true;
  • (1.2) is_­trivially_­copyable_­v<To> is true; and
  • (1.3) is_­trivially_­copyable_­v<From> is true.

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.

dfrib
  • 70,367
  • 12
  • 127
  • 192
  • I don't think this is correct? Given that standard quote, if the lambda type has its size changed then the code is *also* broken. – user202729 Aug 18 '21 at 08:40
  • 3
    @user202729 Afaict an implementation-sized change in the lambas closure's size could make a (seemingly) well-formed program (as OP's) become ill-formed diagnosable, or worse, be undefined. Given the leeway for implementors to implement a lambda's closure type, using a lowel-level operation such as `std::bit_cast` on instances of these types seems inherently dangerous. If my interpretation the above is indeed correct, even if the (dangerous) exercise of using `std::bit_cast` on lamba objects could be well-formed, it is unreasonable. – dfrib Aug 18 '21 at 09:00
  • 1
    "implementation-defined" has a very specific meaning ([intro.abstract]/2); this isn't a case of that. – T.C. Aug 18 '21 at 20:27
  • @T.C. What would be the correct term to describe this case? As it (as per the discussion in my final paragraph) may be UB conditionally on details that the implementation is free to choose, is it then UB altogether? – dfrib Aug 18 '21 at 21:12
  • @T.C. I thought it was an interesting question by itself, so I broke it out from the comments: [Is a program where undefined behavior (UB) is conditional on implementation leeway a program with unconditional UB?](https://stackoverflow.com/questions/68851113/is-a-program-where-undefined-behavior-ub-is-conditional-on-implementation-leew). – dfrib Aug 19 '21 at 16:01