The C++ logo, by Jeremy Kratz, licensed under CC0 1.0 Universal

decltype on a temporary lambda


3 - 4 minutes to read, 780 words
Categories: c++
Keywords: c++ global variable macro

To propose a new feature for C++, one needs to write a paper. Most of the time, such papers have examples of why a new feature is proposed, but strangely, I could not find any for P0315R4.

It is a minor addition to the language being able to write

decltype([/*capture*/*](/*params*/*){/*body*/})

instead of

auto lambda = [/*capture*/*](/*params*/*){/*body*/};
decltype(lambda)

But there is at least one obvious place (for me) where it removes a lot of boilerplate code: custom deleters for std::unique_ptr.

The main use case I have for custom deleters is when using third-party C libraries.

For example, OpenSSL defines a lot of custom types.

One of those is BIGNUM, with all associated function, in particular BN_new and BN_free.

As managing memory by hand is error-prone, std::unique_ptr is a fundamental construct for simplifying C++ code.

But writing

std::unique_ptr<BIGNUM, void(*)(BIGNUM*)> ptr(BN_new(), &BN_free);

is also error-prone.

One needs to store the pointer in std::unique_ptr, and also add as an additional parameter during construction a deleter.

I’ve already explored some ideas on how to avoid those pitfalls and defined some guidelines about function poisoning (first presented on Fluent C++).

std::unique_ptr can be customized to have an alternate deleter by default, for example by writing

struct BIGNUM_deleter{ void operator(BIGNUM* bn){BN_free(bn);} };
using unique_BIGNUM = std::unique_ptr<BIGNUM, BIGNUM_deleter>;

One can then write auto ptr = unique_BIGNUM(BN_new());, without having to remeber to pass BN_free as additional parameter.

Then, after poisoning the corresponding OpenSSL functions, one is forced to use an appropriate factory class:

unique_BIGNUM make_BIGNUM(){
    return unique_BIGNUM(BN_new());
    #pragma GCC poison BN_new
}

auto bn = make_BIGNUM();

and both issues are resolved, as it is not possible to forget to store a BIGNUM obtained with BN_new in a std::unique_ptr. Note, that it can be leaked explicitly by using the function std::unique_ptr::relese, but it is easier to grep for release than questioning every function that handles a pointer.

While this solves the problem I had initially, it required writing some wrapper code.

For example, one needs to declare a structure, and then an alias for unique_ptr. Granted, this job can be automated with macros (OpenSSL defines more than 130 custom types and factory functions(!)), but it does not make the job simpler.

One could use a lambda instead of a structure to avoid writing some code, as in

constexpr auto BIGNUM_deleter = [](BIGNUM* bn){BN_free(bn);}
using unique_BIGNUM = std::unique_ptr<BIGNUM, decltype(BIGNUM_deleter)>;

But it has two problems.

The first is that it creates a global, probably unused, object. Yes, at least it is initialized at compile time.

The second is that if someones drop, by accident, a +, the lambda decays to a function, which has another meaning for std::unique_ptr

Being able to use decltype on an anonymous lambda alleviate the first issue. Now we can simply write

using unique_BIGNUM = std::unique_ptr<BIGNUM, decltype([](BIGNUM* bn){BN_free(bn);})>;

Again, writing a + changes the meaning and is an error, unfortunately not one at compile time.

One might ask himself, why not use decltype on a temporary/unevaluated structure?

Mainly because it does not compile:

using unique_BIGNUM = std::unique_ptr<BIGNUM, decltype(struct {void operator()(BIGNUM* bn){BN_free(bn);}};)>;

The follow-up question is why do we make a special rule for a lambda inside decltype?

It turns out, that in fact, it is not as special as it seems.

Writing

auto a = [/*capture*/](/*params*/){/*body*/};

is equivalent to

struct compiler_specific{
    /*capture*/
    auto operator(/*params*/){/*body*/}
};
auto a = compiler_specific(/*capture*/);

and thus writing

using unique_BIGNUM = std::unique_ptr<BIGNUM, decltype([](BIGNUM* bn){BN_free(bn);})>;
struct compiler_specific{
    auto operator(BIGNUM* bn){BN_free(bn);}
};
using unique_BIGNUM = std::unique_ptr<BIGNUM, decltype(compiler_specific())>;

The workaround ist thus to wrap the "temporary" structure in a lambda:⁠[1]

using unique_BIGNUM3 = std::unique_ptr<BIGNUM, decltype(
    [](){ struct BN_deleter{ void operator()(BIGNUM* bn){BN_free(bn);} }; return BN_deleter{}; }()
)>;

This snippet is rejected by GCC (it’s a bug), and currently accepted by clang and msvc.

Either way, too bad that using a lambda in this context is only possible from C++20, but certainly a welcome minor change for simplifying code.


Do you want to share your opinion? Or is there an error, some parts that are not clear enough?

You can contact me here.