Lambda hackery: Overloading, SFINAE and copyrights

Intro

 A “bad marketing” regarding lambdas in C++ was the use of the term “functions”. Ofcourse connaisseurs talked about “lambdas” “closures” or “lambda expressions” since lambdas are never quite functions in the codegen sense and we certainly can’t think of them as such in language semantics terms; yet this misconception can trigger a quest for features we normally find in plain or templated functions; the first and second sections of this post elaborate on this.

1. Lambda Overloading 

If we think of lambdas as functions we’d might make an attempt to overload them. This attempt is easy to rebut by stating that lambdas are closures ie runtime objects and well … objects, even callable ones, do not overload! What is interesting here though, is that even lambdas “translatable” to functions (usually non capturing closures) when compiled have an intermediate representation of an object with at least 3 member functions : a function call operator, a conversion operator and one static member function, that simply invokes the lambda with this set to zero; it would take some preconditions being satisfied (like the actual closure object not being used) to optimize the closure away and end up with a plain function call. What I mean by “can’t think of them as such (functions) in language semantics terms” is that even in this case we can’t expect this to work :

auto f = []( int){ cout << __PRETTY_FUNCTION__ << endl; }; 
auto f = [](char){ cout << __PRETTY_FUNCTION__ << endl; }; 

f( 1 ); 
f('a'); 

much like we can’t overload a function pointer: it’s always an ‘already declared‘ error. Since the title promises overloading, we’d better not call it a day yet. The closest thing to a lambda that can overload is its function call operator, so you might already had your “aha!” moment by now. If not, here it is :

Create an object out of the lambdas you want to overload and make their function call operators overloaded versions of the superobject’s function call operator.

The idea is often attributed to Mathias Gaunard as well as the following implementation:

template <class F1, class F2>
struct overload_set : F1, F2
{
    overload_set(F1 x1, F2 x2) : F1(x1), F2(x2) {}
    using F1::operator();
    using F2::operator();
};

template <class F1, class F2>
overload_set<F1,F2> overload(F1 x1, F2 x2)
{
    return overload_set<F1,F2>(x1,x2);
} 

The code can be described in a few simple steps

  • overload_set is the superobject’s type; it inherits from the closure types we want to overload
  • the closure types’ function call operators are explicitly made visible to the scope of the class
  • overload is a convenience function that returns an overload set

usage is straighforward :

auto f = overload (
    []( int) { cout << __PRETTY_FUNCTION__ << endl; }, 
    [](char) { cout << __PRETTY_FUNCTION__ << endl; }
);

f('k');
f( 2 );

The names of the function call operators can be made available through ADL (argument dependent lookup) but the using declarations of the original implementation were left unharmed since they make the code more explanatory. We’ll be removing them when we want to scale to arbitrary number of overload candidates (industrial-strength version is provided in the References section, link No 2):

template <class... F>
struct overload_set : F... 
{
  overload_set(F... f) : F(f)... {}  
};
 
template <class... F>
auto overload(F... f) 
{
  return overload_set<F...>(f...);   
}

Overloading lambdas gives you the full power of closures plus Ad-hoc polymorphism ie make lambdas (actually the superobject) behave differently for each type. You can imagine this being useful when writing a for_each algorithm for tuples of different types; then the functor you pass to the algorithm can handle the different elements and be defined right at the location where it is called. In the following example we assume we have a function for_each_in_tuple that visits every element in a tuple; we can then define “in situ” a mutator func (an overload set of lambdas) that will transform the tuple in ways that vary depending on the element type:

int main()
{
    auto func = overload (
        [](int &val)    { val *= 2;   }, 
        [](string &arg) { arg += arg; }, 
        [](char &c)     { c    = 'x'; }
    );
    
    tuple<int, string, char> tup {1, "boom", 'a'};
    for_each_in_tuple(func, tup); 
    
    cout << get<0>(tup) << endl 
         << get<1>(tup) << endl 
         << get<2>(tup); 
}

(Run the example here)

2. SFINAE

 According to common knowledge generic lambdas are function objects with a templated function call operator. Back in the day when we mistook lambdas for functions we might attempt to use SFINAE on them, yet another language feature that works for function templates only. Using the above mechanism, expression SFINAE works out of the box for generic lambdas:

auto f = overload (
  [](auto arg) -> enable_if_t< 
    is_integral<decltype(arg)>::value> {cout << "1";}, 
  [](auto arg) -> enable_if_t<
    !is_integral<decltype(arg)>::value>{cout << "2";}
);

Easy peasy: Since function call operators are templated for generic lambdas they fall under SFINAE rules when the superobject’s function call operator triggers creating candidates for overload resolution.

3. Copyrights

 Closures may generally be copied. There are cases though, where you want to prohibit this. For instance you may want to limit the ways a lambda can escape a scope in order to avoid dangling references. Silly example follows:

struct Locall
{
    int val;
    ~Locall() { 
        cout << "destroyed"; 
    }
};

int main()
{
	function<void()> f; 
	
	{
		Locall a{ 101 }; 
		auto k = [re = ref(a)] {
            cout << "... but using value " 
            << re.get().val;
        }; 
		f = k;
	}

	f(); 
	
	return 0;
}

(run the example here)

If we marked k as non copyable, the compiler wouldn’t allow f to “absorb” a reference to an object about to die; it’s a way for the coder to convey intents that may later be forgotten (or can’t be grasped by another person working on your code). To make a non copyable lambda use copyrights :

auto k = [nocopy, re = ref(a)] { /* ... */ };

Since the obvious way for a lambda to be non copyable is to capture a move only type, I use a macro that generates an “init capture” (a C++14 generalized capture) with a unique name and a light(est)weight captured object. This is how:

#define CONCATENATE_DETAIL(x, y) x##y
#define CONCATENATE(x, y) CONCATENATE_DETAIL(x, y)
#define UNIQUE_IDEXPR(x) CONCATENATE(x, __COUNTER__)

struct NC
{
    NC()                     = default; 
    NC(NC const&)            = delete; 
    NC& operator=(NC const&) = delete; 
    NC(NC&&)                 = default; 
};

#define nocopy UNIQUE_IDEXPR(x) = NC()

Note that what we get from this is a unique expression (auto)xi = N() (where auto isn’t actually spelled and i equals 1, 2, 3…), a move initialization due to the xvalue produced by NC() ie requires the plain and move constructors to be available (auto generated or defined). Also xi is in the scope of the closure class so we don’t need to worry about collisions with the outer scope

Quiz

 Something is redundant in the nocopy macro, can you spot it? Answer in the comments section or look at the code in Github (link No 2) to find out.

References

  1. Mathias Gaunard on Lambda the Ultimate
  2. The code on the blog’s repository
  3. Fun with lambdas 

15 thoughts on “Lambda hackery: Overloading, SFINAE and copyrights

    1. Worry not because if you click link No 2 in the References you’ll find that I use a more advanced implementation in the blog’s repository; that code is up for grabs! And it works wth gcc, clang and cl as well : http://coliru.stacked-crooked.com/a/932f6c367c641f2a . The code shown here was preffered for its brevity and simplicity; it has more disadvantages, like inability to perfect forward temporary lambdas (also addressed by the code in Github) but sure looks clear as crystal (ok and it works for clang and cl, I mean it’s not that bad – I should maybe edit to mention all this)

      Like

  1. Hi there,
    just one point: instead of using the macro, what about writing:

    struct NC
    {
        NC()                     = default; 
        NC(NC const&)            = delete; 
        NC& operator=(NC const&) = delete; 
        NC(NC&&)                 = default; 
    } nocopy; // global one
    
    struct rights
    {
      template<typename... T>
      rights(T...){} // other rights if you need them...
    };
    
    //...
    
    auto k = [rights(nocopy), re = ref(a)] { /* ... */ };
    

    It’s just more readable and you don’t need macros at all. What do you think?

    Liked by 2 people

    1. I’m personally ok with macros that take no arguments. What I aimed for was a composable tag system eg have nocopy, nomove, nodel with the ability to mix and match. That’s also achievable with your code so I think I consider your solution equivalent. On the upside the way you tweaked the code would be more easily acceptable as part of the standard library. What about a joined proposal on this ?

      Like

      1. @MarcoArena Full disclosure, I didn’t scrutinize the code. My reply was on your general view of composibility and the idea to achieve this through a single object mechanism, the rights structure, which I found pretty cool. What you wrote could be easily fixed in multiple ways, eg make the tags template arguments to your struct

        template<class... R>
        struct rights : R...
        {
          rights() = default; 
        };
        

        and then go

        auto k = [a = rights<nocopy>()] {};
        

        like in this example http://coliru.stacked-crooked.com/a/0e7fb5c978cfb2da

        Like

      2. Thanks Nick, your fix is fine and I think it’s even more readable than my original code. In general I think we still need the macro because of the need for a unique variable name in the lambda capture, but I don’t see any problem on this.

        Like

    2. Hello Marco,
      I feel sorry if I got your idea wrong but for me your code looks a bit broken.

      The authors idea is a preventing lambda copying so error should happen on
      f = k; line.
      So we could fix your code using this:

      struct rights
      {
      template
      rights(T&&…){} // other rights if you need them… <- so we are OK to move r/xvalues
      };

      If we will stay with rights(T…) the deduced dype will be lvalue ref to NC (nocopy object) what is useless.

      auto k = [rights(std::move(nocopy)), re = ref(a)] { /* … */ }; <- cast it to rvalue so rights structure will be perfectly fine fitted.

      now in the flow is changed and we are OK with using k in nested scope but we will not be able to assign our lambda to function object.

      Like

    3. Marco,
      and by the way you don`t need struct rights definition, because what you are doing with this syntax is a defining a rights field of type NC inside lambda using init-capture syntax See 5.1.2.11 inside N4296

      Like

Leave a comment