decltype on a temporary lambda
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 remember to pass BN_free
as an 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 solved 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 alleviates 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 is 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 anytime.