Document number: PxxxxR0
Date: 2017-03-07
Project: ISO JTC1/SC22/WG21, Programming Language C++
Audience: Evolution Working Group
Reply to: Arthur O'Dwyer <arthur.j.odwyer@gmail.com>

Captureless lambdas should be default-constructible

1. Introduction
2. Motivations
3. Triviality of special member functions
4. Proposed wording
    5.1.5 Lambda expressions [expr.prim.lambda]
5. References

1. Introduction

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.

2. Motivations

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:

3. Triviality of special member functions

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:

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.

4. Proposed wording for C++20

The wording in this section is relative to WG21 draft N4618 [N4618], that is, the current draft of the C++17 standard.

5.1.5 Lambda expressions [expr.prim.lambda]

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]

5. References

[N4618]
"Working Draft, Standard for Programming Language C++" (Nov 2016).
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/n4618.pdf.
[Yakk]
Yakk on StackOverflow. "Instantiating C++ lambda by its type" (September 2016).
http://stackoverflow.com/a/39771222/1424877.