Logo

Blog


C++ Insights: Lambdas in unevaluated contexts

About two weeks ago, I added support for P0315R4: Lambdas in unevaluated contexts, to C++ Insights.

What can out do with this new ability of lambdas?

One example I personally find very helpful is a scenario of a unique_ptr with a custom deleter. The classic example is the following:

1
2
3
auto p =
  std::unique_ptr<FILE, decltype(&fclose)>{fopen("SomeFile.txt", "r"),
                                           fclose};

With this naive approach, I have two issues. First, the repetition, we have to say fclose two times. Second, the efficiency. The code as presented increases the size of the unique_ptr by the size of another point, the function pointer.

The more efficient way, which also requires less repetition, is the following:

1
2
3
4
5
struct FClose {
  void operator()(FILE* f) { fclose(f); }
};

auto p = std::unique_ptr<FILE, FClose>{fopen("SomeFile.txt", "r")};

The using-part looks much better, but yeah, I hear you saying that creating a class, or more precisely a callable for each special close or free function, is not that much better.

This brings me to the next option. Still, without C++20, we use templates to at least reduce the need for writing a class like FClose for each destroy function.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
namespace details {
  template<auto DeleteFn>
  struct UniquePtrDeleter {
    template<class T>
    void operator()(T* ptr) const
    {
      DeleteFn(ptr);
    }
  };
}  // namespace details

template<typename T, auto DeleteFn>
using unique_ptr_deleter =
  std::unique_ptr<T, details::UniquePtrDeleter<DeleteFn>>;

auto p = unique_ptr_deleter<FILE, fclose>{fopen("SomeFile.txt", "r")};

This is an improvement, especially if you're locked to C++17. But what can C++20 do? Since we can have captureless lambdas in unevaluated contexts in C++20, we can eliminate the implementation for UniquePtrDeleter entirely but let the lambda do this job:

1
2
3
4
5
template<typename T, auto DeleteFn>
using unique_ptr_deleter =
  std::unique_ptr<T, decltype([](T* obj) { DeleteFn(obj); })>;

auto p = unique_ptr_deleter<FILE, fclose>{fopen("SomeFile.txt", "r")};

Nice, isn't it?

Implementation in C++ Insights

The implementation in C++ Insights was a challenge. Lambdas are always difficult as the closure type that the lambda generates must be placed before it actually is used. For the parsing, that means going down the AST and storing an insert location before more or less each declaration where the closure type is then inserted.

Before C++20, the number of instances where we could create a lambda was already a lot, simply everywhere where an expression was possible.

C++20 now increases the options, as we can now also have lambdas where we declare a type. For example:

1
2
3
4
struct Test
{
   decltype([] { }) a;
};

This example creates a nice function pointer as a member in our struct Test. I'm not saying this is the code you should write, but it is code you can write.

A place where this use is more sensible is issue 468, which made me aware of the missing implementation in C++ Insights:

1
2
3
4
5
6
7
template<class F = decltype([]() -> bool { return true; })>
bool test(F f = {})
{
    return f();
}

int main() { return test(); }

Here the lambda is used as a default type template parameter. Another new place for a lambda.

Then there is a requires expression:

1
2
3
4
5
template<typename T>
concept X = requires(T t)
{
    decltype([]() { }){};
};

Again potentially useless in this form, but valid.

C++ Insights lambda hunt

Last week I asked you to find issues with the implementation of lambdas in unevaluated contexts. Thank you all for your participation! As expected, the challenge revealed a few things I hadn't thought about.

Thanks to the challenge, C++ Insights now matches alias declarations at the TU scope. Another patch is for functions with a trailing return type at TU scope. Unbelievable, but I failed to find an easy way to get the source location of the trailing return type in Clang. However, in the end, I got inspiration from a clang-tidy check. Still, the implementation feels like a hack.

The next nice finding was in the lambda captures when a lambda captures another lambda which decays to a function pointer.

Before the challenge, the matchers at TU scope got already improved. All in all, I hope that a couple more statements get expanded now, without the need to put them into a namespace (which was a trick before).

Andreas