1. Introduction
2. Motivations
3. Triviality of special member functions
4. Proposed wording
5.1.5 Lambda expressions [expr.prim.lambda]
5. References
This paper proposes one more small step in the long walk to make lambdas a full-fledged
replacement for "functor objects" (class types with overloaded operator()
).
Namely, it proposes that the type of a captureless closure should suffice to
reconstruct that closure's behavior — i.e., that, given a captureless closure type,
we should be able to default-construct a closure of that type.
Default-constructing a closure with captures remains nonsensical as far as I can tell, so I don't propose any change in the semantics of "captureful" closure types.
Although I foresee this default-construction feature being most useful when combined with the ability to use lambda-expressions in unevaluated contexts (such as template arguments), this paper does not propose anything new along those lines.
Ideally, any time a C++03 programmer would be tempted to write a one-off helper
class with an overloaded operator()
, a C++17 programmer should be
tempted to use a lambda. Lambdas are nicer than named classes for cases where
the only relevant thing about a value is the behavior of its
operator()
; for example, the "deleter" objects used in the descriptions
of unique_ptr
and shared_ptr
(and perhaps soon in
the descriptions of the classes concerned with RCU and hazard-pointer memory
management). Consider this old-style code:
struct Freer { template<class T> void operator()(T *p) const { free(p); } }; template<class T> using unique_c_ptr = std::unique_ptr<T, Freer>; int main() { unique_c_ptr<int> p((int *)malloc(sizeof(int))); }
Anecdotally, programmers are often surprised to find that there is no way to rewrite the above code to use a lambda. Sometimes this slowly dawning realization leads to awkward or buggy code such as
template<class T> using unique_c_ptr = std::unique_ptr<T, decltype(&free)>; int main() { unique_c_ptr<int> p((int *)malloc(sizeof(int)), free); }
A subtlety of the above code is that if you accidentally omit the tokens ", free
"
from the final line, you'll get a program that compiles and runs... but dereferences a
null function pointer the first time a unique_c_ptr
is destructed! This is
needlessly unfriendly to the programmer.
Skipping to the end for just a minute: Here's the code I'm aiming to legalize with this proposal.
auto freer = [](auto *p){ free(p); }; template<class T> using unique_c_ptr = std::unique_ptr<T, decltype(freer)>; int main() { unique_c_ptr<int> p((int *)malloc(sizeof(int))); }
The above doesn't compile today because the closure type produced by a lambda (captureless or captureful) is never default-constructible.
Okay, returning to motivation. —
In September 2016, Yakk posted a workaround for the current state of affairs on StackOverflow. [Yakk]
His code for stateless_lambda_t<F>
, here renamed stateless1
for clarity, actually created a singleton object of closure type F
so that
there would be a legitimate object on which to call operator()
:
template<class F> struct stateless1 { static void *data() { static std::aligned_storage_t< sizeof(F), alignof(F) > instance; return static_cast<void*>(&instance); }; template<class Fin, std::enable_if_t, stateless1>, int> = 0> stateless1(Fin&& f) { new (data()) F( std::forward<Fin>(f) ); } template<class... Args> decltype(auto) operator()(Args&&...args) const { return (*static_cast<F*>(data()))(std::forward<Args>(args)...); } stateless1() = default; }; template<class F> stateless1<std::decay_t<F>> make_stateless1(F&& f) { return {std::forward<F>(f)}; } auto freer = [](auto *p) { free(p); }; int i = 42; auto g = make_stateless1(freer); // create the required singleton template<class T> using unique_c_ptr = std::unique_ptr<T, stateless2<decltype(g)>>; int main() { unique_c_ptr<int> p((int *)malloc(sizeof(int))); }
If you're willing to risk some undefined behavior, you can accomplish the same goal with much less code:
template<class F> struct stateless2 { template<class... Args> decltype(auto) operator()(Args&&... args) const { return (*reinterpret_cast<const F*>(this))(std::forward<Args>(args)...); } }; auto freer = [](auto *p) { free(p); }; template<class T> using unique_c_ptr = std::unique_ptr<T, stateless2<decltype(freer)>>; int main() { unique_c_ptr<int> p((int *)malloc(sizeof(int))); }
This paper proposes that we cut out all these arcane hacks (especially the seductively convenient one above, with the undefined behavior!) and allow programmers to write what they mean directly. Namely:
auto freer = [](auto *p){ free(p); }; template<class T> using unique_c_ptr = std::unique_ptr<T, decltype(freer)>; int main() { unique_c_ptr<int> p((int *)malloc(sizeof(int))); }
This paper does not propose anything new as far as the "unevaluated context-ability" of closure types, not even captureless ones. Therefore, the following code (while desirable, in my opinion) will continue to be ill-formed:
using ptr = std::unique_ptr<int, decltype([](auto *p) { free(p); })>; int main() { ptr p((int *)malloc(sizeof(int))); }For additional StackOverflow discussion on this topic, see:
The first draft of the proposed new paragraph below contained a longer non-normative note: "[The captureless closure type's] copy constructor, move constructor, copy assignment operator, and move assignment operator are implicitly defined as usual. Since the closure type has no non-static data members, all of its special member functions are trivial."
However, this turns out not to be guaranteed by the normative wording in
the current Standard. Closure types' special member functions are not
currently
guaranteed to be trivial, even when there are no captures, or even when the
only thing captured is this
. Implementations are explicitly
permitted to provide non-trivially-copyable closure types via 5.1.5 (4.2):
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:
- the size and/or alignment of the closure type,
- whether the closure type is trivially copyable (Clause 9),
- whether the closure type is a standard-layout class (Clause 9), or
- whether the closure type is a POD class (Clause 9).
I argue that there is no useful purpose in permitting implementations to provide closure types with unexpectedly non-trivial special member functions. A separate paper might reasonably propose that the wording be tightened up to require triviality of most closure types' special member functions. However, the only thing in scope for this paper is to propose that the brand-new "defaulted default constructor for captureless closure types" should be trivial.
The wording in this section is relative to WG21 draft N4618 [N4618], that is, the current draft of the C++17 standard.
Add a new paragraph between paragraphs 20 and 21, and edit paragraph 21 as follows.
The closure type associated with a lambda-expression with no lambda-capture has a defaulted, trivial default constructor. [Note: Its copy constructor, move constructor, copy assignment operator, and move assignment operator are implicitly defined as usual. — end note]
The closure type associated with a lambda-expression with a lambda-capture has no default constructor and a deleted copy assignment operator. It has a defaulted copy constructor and a defaulted move constructor (12.8). [Note: These special member functions are implicitly defined as usual, and might therefore be defined as deleted. — end note]