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 
Advertisements