An implementation model for lambda functions in C++


Posted by Diego Assencio on 2016.11.29 under Programming (C/C++)

In this post, I will show that every lambda function in C++ (or "lambda expression", or just "lambda" for short) is equivalent to some functor class, i.e., to a class which has an overloaded operator(); objects which are instances of such classes are callable as if they were functions. I will illustrate this fact by explicitly constructing, for various lambda functions, equivalent functor classes. The advantage of using lambda functions over such equivalent functor classes comes from the fact that the required amount of code for a lambda can be significantly shorter and cleaner provided that the lambda itself is not too long and complicated.

In order to understand this post, the reader must have basic knowledge of how a lambda function is defined and at least some awareness of what a capture list is.

Let us first start with a very simple example:

#include <iostream>

int main()
{
	auto add_one = [](const int x) { return x + 1; };

	int x = 1;

	std::cout << add_one(x) << "\n";   /* prints 2 */
	std::cout << add_one(2) << "\n";   /* prints 3 */

	return 0;
}

This program contains a trivial lambda function add_one which takes a single integer value x and returns x+1. As the following program illustrates, the same task can be performed through a functor class AddOne:

#include <iostream>

class AddOne
{
public:
	int operator()(const int x) const
	{
		return x + 1;
	}
};

int main()
{
	AddOne add_one;

	int x = 1;

	std::cout << add_one(x) << "\n";   /* prints 2 */
	std::cout << add_one(2) << "\n";   /* prints 3 */

	return 0;
}

The code above was valid even before C++11 came into play. Notice that it is significantly longer than the first version.

Let us now consider an example in which the lambda function contains a non-empty capture list:

#include <iostream>

int main()
{
	int a = 3;
	int b = 2;

	/* for a line y(x) = a*x + b, determine y given x */
	auto compute_y = [=](const int x) { return a*x + b; };

	int x = 1;

	std::cout << compute_y(x) << "\n";   /* prints 5 */
	std::cout << compute_y(2) << "\n";   /* prints 8 */

	return 0;
}

The lambda function compute_y has a capture list which copies all variables in the local scope of the main function which appear before its definition, i.e., copies of both a and b are made available for compute_y to use internally. The equivalent version using a functor class achieves the same result by using a constructor which copies the values of a and a into private data members a_ and b_ respectively:

#include <iostream>

class ComputeY
{
public:
	ComputeY(int a, int b): a_(a), b_(b)
	{
		/* nothing needs to be done here */
	}

	int operator()(const int x) const
	{
		return a_*x + b_;
	}

private:
	int a_;
	int b_;
};

int main()
{
	int a = 3;
	int b = 2;

	ComputeY compute_y(a,b);

	int x = 1;

	std::cout << compute_y(x) << "\n";   /* prints 5 */
	std::cout << compute_y(2) << "\n";   /* prints 8 */

	return 0;
}

Since a capture list can have variables in the local scope captured by reference as well, here is an example illustrating this case:

#include <array>
#include <iostream>

int main()
{
	int a = 2;
	std::array<int,3> v = { 1, 2, 3 };

	/*
	 * for an input array x with 3 elements, return a + x*v (where
	 * '*' here is used to represent a dot product operation); this
	 * lambda captures a by value and v by reference
	 */
	auto affine = [a,&v](const std::array<int,3>& x)
	              {
	                  return a + x[0]*v[0] + x[1]*v[1] + x[2]*v[2];
	              };

	std::array<int,3> x = { 1, -1, 2 };

	std::cout << affine(x) << "\n";   /* prints 7 */

	return 0;
}

The version using a functor class now has a private data member which is a reference to an input array:

#include <array>
#include <iostream>

class Affine
{
public:
	Affine(int a, std::array<int,3>& v): a_(a), v_(v)
	{
		/* nothing needs to be done here */
	}

	int operator()(const std::array<int,3>& x) const
	{
		return a_ + x[0]*v_[0] + x[1]*v_[1] + x[2]*v_[2];
	}

private:
	int a_;
	std::array<int,3>& v_;
};

int main()
{
	int a = 2;
	std::array<int,3> v = { 1, 2, 3 };

	Affine affine(a,v);

	std::array<int,3> x = { 1, -1, 2 };

	std::cout << affine(x) << "\n";   /* prints 7 */

	return 0;
}

The next case which must be considered is a lambda function which can modify its own state. Looking at the examples above, one can see that every definition of operator() is qualified as const. This is because by default, a lambda cannot change the value of a parameter captured by value and references can anyway not be modified after being initialized (but the object a reference refers to can be changed, as is the case for pointers). This is exemplified in the code below:

#include <iostream>

int main()
{
	int a = 0;
	int b = 0;

	/* error: copy of a cannot be changed */
	auto lambda_1 = [a](const int x) { ++a; return a*x; };

	/*
	 * OK: a reference cannot be changed after being initialized, but
	 * the value of the object it refers to can change (surprise?!)
	 */
	auto lambda_2 = [&b](const int x) { ++b; return b*x; };

	std::cout << lambda_2(1) << "\n";   /* prints 1 */
	std::cout << lambda_2(1) << "\n";   /* prints 2 */
	std::cout << lambda_2(1) << "\n";   /* prints 3 */

	std::cout << b << "\n";             /* prints 3 */

	return 0;
}

In order to modify the state of the variables which a lambda function captures by value, we must declare it as mutable:

#include <iostream>

int main()
{
	int a = 0;
	int b = 0;

	/* OK: copy of a can be changed since lambda_1 is mutable */
	auto lambda_1 = [a](const int x) mutable { ++a; return a*x; };

	std::cout << lambda_1(1) << "\n";   /* prints 1 */
	std::cout << lambda_1(1) << "\n";   /* prints 2 */
	std::cout << lambda_1(1) << "\n";   /* prints 3 */

	std::cout << a << "\n";             /* prints 0 */

	/* OK: b is captured by reference (no need for mutable) */
	auto lambda_2 = [&b](const int x) { ++b; return b*x; };

	std::cout << lambda_2(1) << "\n";   /* prints 1 */
	std::cout << lambda_2(1) << "\n";   /* prints 2 */
	std::cout << lambda_2(1) << "\n";   /* prints 3 */

	std::cout << b << "\n";             /* prints 3 */

	return 0;
}

Declaring a lambda function as mutable is equivalent to removing the const qualifier from the operator() of its equivalent functor class. In other words, this operator will be allowed to modify the data members of the class.

So far, all lambda functions defined are valid in C++11. Our final example will be a generic lambda function, i.e., a lambda which has at least one parameter with type auto (generic lambdas were introduced in C++14). There is nothing truly special about how an equivalent functor class is defined in this case; we merely use templates to accommodate the missing functionality:

#include <iostream>

int main()
{
	auto add_one = [](const auto x) { return x + 1; };

	int x = 1;

	std::cout << add_one(x) << "\n";   /* prints 2 */
	std::cout << add_one(2) << "\n";   /* prints 3 */

	return 0;
}

Here is the same program using a functor class:

#include <iostream>

class AddOne
{
public:
	template<typename T>
	T operator()(const T x) const
	{
		return x + 1;
	}
};

int main()
{
	AddOne add_one;

	int x = 1;

	std::cout << add_one(x) << "\n";   /* prints 2 */
	std::cout << add_one(2) << "\n";   /* prints 3 */

	return 0;
}

One last comment must be added before finishing this post: in all examples above, I showed how an equivalent functor class can be built for a given lambda. While the code version using a functor class is equivalent to the original one in the sense that the same results will be produced, it is not entirely equivalent in the sense that introducing a functor class means introducing a new type to replace the lambda function. However, notice that in all cases except for the generic lambda functions, we can declare the functor classes locally as anonymous classes, in which case no new type is introduced. To clarify, here is how we could do it for our very first example:

#include <iostream>

int main()
{
	/* anonymous class (no more AddOne) */
	class
	{
	public:
		int operator()(const int x) const
		{
			return x + 1;
		}
	} add_one;

	int x = 1;

	std::cout << add_one(x) << "\n";   /* prints 2 */
	std::cout << add_one(2) << "\n";   /* prints 3 */

	return 0;
}

Comments

No comments posted yet.

Leave a reply

NOTE: A name and a comment (max. 1024 characters) must be provided; all other fields are optional. Equations will be processed if surrounded with dollar signs (as in LaTeX). You can post up to 5 comments per day.