0,0
рейтинг
3 августа 2009 в 01:46

Разработка → C++0x (С++11). Лямбда-выражения tutorial

C++*
Буквально на днях случайно наткнулся на Хабре на статью о лямбда-выражениях из нового (будущего) стандарта C++. Статья хорошая и даёт понять преимущества лямбда-выражений, однако, мне показалось, что статья недостаточно полная, поэтому я решил попробовать более детально изложить материал.



Вспомним основы



Лямбда-выражения — одна из фич функциональных языков, которую в последнее время начали добавлять также в императивные языки типа C#, C++ etc. Лямбда-выражениями называются безымянные локальные функции, которые можно создавать прямо внутри какого-либо выражения.

В прошлой статье лямбда-выражения сравнивали с указателями на функции и с функторами. Так вот первое, что следует уяснить: лямбда-выражения в C++ — это краткая форма записи анонимных функторов. Рассмотрим пример:

// Листинг 1
#include <algorithm>
#include <cstdlib>
#include <iostream>
#include <vector>
 
using namespace std;
 
int main()
{
	vector<int> srcVec;
	for (int val = 0; val < 10; val++)
	{
		srcVec.push_back(val);
	}
 
	for_each(srcVec.begin(), srcVec.end(), [](int _n)
	{
		cout << _n << " ";
	});
	cout << endl;
 
	return EXIT_SUCCESS;
}


Фактически данный код целиком соответствует такому:

// Листинг 2
#include <algorithm> 
#include <cstdlib> 
#include <iostream> 
#include <vector>
 
using namespace std;
 
class MyLambda 
{
	public: void operator ()(int _x) const { cout << _x << " "; } 
};
 
int main() 
{
	vector<int> srcVec; 
	for (int val = 0; val < 10; val++)
	{ 
		srcVec.push_back(val); 
	}
 
	for_each(srcVec.begin(), srcVec.end(), MyLambda()); 
	cout << endl;
 
	return EXIT_SUCCESS; 
}


Вывод соответственно будет следующим:
0 1 2 3 4 5 6 7 8 9


На что здесь стоит обратить внимание. Во-первых, из Листинга 1 мы видим, что лямбда-выражение всегда начинается с [] (скобки могут быть непустыми — об этом позже), затем идет необязательный список параметров, а затем непосредственно тело функции. Во-вторых, тип возвращаемого значения мы не указывали, и по умолчанию лямбда возвращает void (далее мы увидим, как и зачем можно указать возвращаемый тип явно). В-третьих, как видно по Листингу 2, по умолчанию генерируется константный метод (к этому тоже еще вернемся).

Не знаю, как вам, но мне for_each, записанный с помощью лямбда-выражения, нравится гораздо больше. Попробуем написать немного усложненный пример:

// Листинг 3
#include <algorithm>
#include <cstdlib>
#include <iostream>
#include <vector>
 
using namespace std;
 
int main()
{
	vector<int> srcVec;
	for (int val = 0; val < 10; val++)
	{
		srcVec.push_back(val);
	}
 
	int result =
		count_if(srcVec.begin(), srcVec.end(), [] (int _n)
		{
			return (_n % 2) == 0;
		});
 
	cout << result << endl;
 
	return EXIT_SUCCESS;
}


В данном случае лямбда играет роль унарного предиката, то есть тип возвращаемого значения bool, хотя мы нигде этого не указывали. При наличии одного return в лямбда-выражении, компилятор вычисляет тип возвращаемого значения самостоятельно. Если же в лямбда-выражении присутствует if или switch (или другие сложные конструкции), как в приведенном ниже коде, то на компилятор полагаться уже нельзя:

// Листинг 4
#include <algorithm>
#include <cstdlib>
#include <iostream>
#include <iterator>
#include <vector>
 
using namespace std;
 
int main()
{
	vector<int> srcVec;
	for (int val = 0; val < 10; val++)
	{
		srcVec.push_back(val);
	}
 
	vector<double> destVec;
	transform(srcVec.begin(), srcVec.end(),
	          back_inserter(destVec), [] (int _n)
	{
		if (_n < 5)
			return _n + 1.0;
		else if (_n % 2 == 0)
			return _n / 2.0;
		else
			return _n * _n;
	});
 
	ostream_iterator<double> outIt(cout, " ");
	copy(destVec.begin(), destVec.end(), outIt);
	cout << endl;
 
	return EXIT_SUCCESS;
}


Код из Листинга 4 не компилируется, а, к примеру, Visual Studio пишет ошибку на каждый return такого содержания:
«error C3499: a lambda that has been specified to have a void return type cannot return a value»

Компилятор не может самостоятельно вычислить тип возвращаемого значения, поэтому мы должны его указать явно:

// Листинг 5
#include <algorithm>
#include <cstdlib>
#include <iostream>
#include <iterator>
#include <vector>
 
using namespace std;
 
int main()
{
	vector<int> srcVec;
	for (int val = 0; val < 10; val++)
	{
		srcVec.push_back(val);
	}
 
	vector<double> destVec;
	transform(srcVec.begin(), srcVec.end(),
	          back_inserter(destVec), [] (int _n) -> double
	{
		if (_n < 5)
			return _n + 1.0;
		else if (_n % 2 == 0)
			return _n / 2.0;
		else
			return _n * _n;
	});
 
	ostream_iterator<double> outIt(cout, " ");
	copy(destVec.begin(), destVec.end(), outIt);
	cout << endl;
 
	return EXIT_SUCCESS;
}


Теперь компиляция проходит успешно, а вывод, как и ожидалось, будет следующим:
1 2 3 4 5 25 3 49 4 81


Единственное, что мы добавили в Листинге 5, это тип возвращаемого значения для лямбда-выражения в виде -> double. Синтаксис немного странноват и смахивает больше на Haskell, чем на C++. Но указывать возвращаемый тип «слева» (как в функциях) не получилось бы, потому что лямбда должна начинаться с [], чтобы компилятор смог её различить.

Захват переменных из внешнего контекста



Все лямбда-выражения, приведенные выше, выглядели как анонимные функции, потому что не хранили никакого промежуточного состояния. Но лямбда-выражения в C++ — это анонимные функторы, а значит состояние они хранить могут! Используя лямбда-выражения, напишем программу, которая выводит количество чисел, попадающих в заданный пользователем интервал [lower; upper):

// Листинг 6
#include <algorithm>
#include <cstdlib>
#include <iostream>
#include <iterator>
#include <numeric>
#include <vector>
 
using namespace std;
 
int main()
{
	vector<int> srcVec;
	for (int val = 0; val < 10; val++)
	{
		srcVec.push_back(val);
	}
 
	int lowerBound = 0, upperBound = 0;
	cout << "Enter the value range: ";
	cin >> lowerBound >> upperBound;
 
	int result = 
		count_if(srcVec.begin(), srcVec.end(),
		         [lowerBound, upperBound] (int _n)
		{
			return lowerBound <= _n && _n < upperBound;
		});
	cout << result << endl;
 
	return EXIT_SUCCESS;
}


Наконец, мы добрались до того момента, когда лямбда-выражение начинается не с пустых скобок. Как видно в Листинге 6, внутри квадратных скобок могут указываться переменные. Это называется… эээм… «список захвата» (capture list). Для чего это нужно? На первый взгляд может показаться, что внешней областью видимости для лямбда-выражения является функция main() и мы можем беспрепятственно использовать переменные, объявленные в ней, внутри тела лямбда-выражения, однако это не так. Почему? Потому что фактически тело лямбды — это тело перегруженного operator()() (как бы это назвать… оператора функционального вызова что ли) внутри анонимного функтора, то есть для кода из Листинга 6 компилятор неявно сгенерирует примерно такой код:

// Листинг 7
#include <algorithm>
#include <cstdlib>
#include <iostream>
#include <iterator>
#include <vector>
 
using namespace std;
 
class MyLambda
{
public:
	MyLambda(int _lowerBound, int _upperBound)
		: m_lowerBound(_lowerBound)
		, m_upperBound(_upperBound)
	{}
 
	bool operator ()(int _n) const
	{
		return m_lowerBound <= _n && _n < m_upperBound;
	}
 
private:
	int m_lowerBound, m_upperBound;
};
 
int main()
{
	vector<int> srcVec;
	for (int val = 0; val < 10; val++)
	{
		srcVec.push_back(val);
	}
 
	int lowerBound = 0, upperBound = 0;
	cout << "Enter the value range: ";
	cin >> lowerBound >> upperBound;
 
	int result = count_if(srcVec.begin(),
	                      srcVec.end(),
	                      MyLambda(lowerBound, upperBound));
	cout << result << endl;
 
	return EXIT_SUCCESS;
}


Листинг 7 немного всё разъясняет. Наша лямбда превратилась в функтор, внутри тела которого мы не можем напрямую использовать переменные, объявленные в main(), так как это непересекающиеся области видимости. Для того чтобы доступ к lowerBound и upperBound все-таки был, эти переменные сохраняются внутри самого функтора (происходит тот самый «захват»): конструктор их инициализирует, а внутри operator()() они используются. Я специально дал этим переменным имена, начинающиеся с префикса «m_», чтобы подчеркнуть различие.

Если мы попытаемся изменить «захваченные» переменные внутри лямбды, нас ждет неудача, потому что по умолчанию генерируемый operator()() объявлен как const. Для того чтобы это обойти, мы можем указать спецификатор mutable, как в следующем примере:

// Листинг 8
#include <algorithm>
#include <cstdlib>
#include <iostream>
#include <iterator>
#include <numeric>
#include <vector>
 
using namespace std;
 
int main()
{
	vector<int> srcVec;
	int init = 0;
	generate_n(back_inserter(srcVec), 10, [init] () mutable
	{
		return init++;
	});
 
	ostream_iterator<int> outIt(cout, " ");
	copy(srcVec.begin(), srcVec.end(), outIt);
	cout << endl << "init: " << init << endl;
 
	return EXIT_SUCCESS;
}


Ранее я упоминал, что список параметров лямбды можно опускать, когда он пустой, однако для того чтобы компилятор правильно распарсил применение слова mutable, мы должны явно указать пустой список параметров.
При выполнении программы из Листинга 8 получаем следующее:
0 1 2 3 4 5 6 7 8 9
init: 0


Как видим, благодаря ключевому слову mutable, мы можем менять значение «захваченной» переменной внутри тела лямбда-выражения, но, как и следовало ожидать, эти изменения не отражаются на локальной переменной, так как захват происходит по значению. C++ позволяет нам захватывать переменные по ссылке и даже указывать «режим захвата», используемый по умолчанию. Что это означает? Мы можем не указывать каждую переменную в списке захвата по отдельности: вместо этого можно просто указать режим по умолчанию для захвата, и тогда все переменные из внешнего контекста, которые используются внутри лямбды, будут захвачены компилятором автоматически. Для указания режима захвата по умолчанию существует специальный синтаксис: [=] или [&] для захвата по значению и по ссылке соответственно. При этом для каждой переменной можно указать свой режим захвата, однако режим по умолчанию, естественно, указывается только единожды, причем в самом начале списка захвата. Вот варианты использования:

[]                      // без захвата переменных из внешней области видимости
[=]                     // все переменные захватываются по значению
[&]                     // все переменные захватываются по ссылке
[x, y]                  // захват x и y по значению
[&x, &y]                // захват x и y по ссылке
[in, &out]              // захват in по значению, а out — по ссылке
[=, &out1, &out2]       // захват всех переменных по значению, кроме out1 и out2,	
                        // которые захватываются по ссылке
[&, x, &y]              // захват всех переменных по ссылке, кроме x…


Следует отметить, что синтаксис наподобие &out в данном случае не означает взятие адреса. Его следует читать скорее как SomeType & out, то есть это просто передача параметра по ссылке. Рассмотрим пример:

// Листинг 9
#include <algorithm>
#include <cstdlib>
#include <iostream>
#include <iterator>
#include <vector>
 
using namespace std;
 
int main()
{
	vector<int> srcVec;
	int init = 0;
	generate_n(back_inserter(srcVec), 10, [&] () mutable
	{
		return init++;
	});
 
	ostream_iterator<int> outIt(cout, " ");
	copy(srcVec.begin(), srcVec.end(), outIt);
	cout << endl << "init: " << init << endl;
 
	return EXIT_SUCCESS;
}


В этот раз вместо явного захвата переменной init, я указал режим захвата по умолчанию: [&]. Теперь когда компилятор встречает внутри тела лямбды переменную из внешнего контекста, он автоматически захватывает её по ссылке. Вот эквивалентный Листингу 9 код:

// Листинг 10
#include <algorithm>
#include <cstdlib>
#include <iostream>
#include <iterator>
#include <vector>
 
using namespace std;
 
class MyLambda
{
public:
	explicit MyLambda(int & _init) : init(_init) { }
 
	int operator ()() { return init++; }
 
private:
	int & init;
};
 
int main()
{
	vector<int> srcVec;
	int init = 0;
	generate_n(back_inserter(srcVec), 10, MyLambda(init));
 
	ostream_iterator<int> outIt(cout, " ");
	copy(srcVec.begin(), srcVec.end(), outIt);
	cout << endl << "init: " << init << endl;
 
	return EXIT_SUCCESS;
}


И соответственно вывод будет следующим:
0 1 2 3 4 5 6 7 8 9
init: 10


Теперь вам главное не запутаться, что, где и когда передавать по ссылке. Фактически, если мы указываем [&] и не указываем mutable, то все равно сможем менять значение захваченной переменной и это отразится на локальной, потому что operator()() const подразумевает, что мы не можем менять, на что указывает ссылка, а это и так невозможно.

Если лямбда-выражение имеет вид [=] (int & _val) mutable { … }, то переменные захватываются по значению, но меняться будет только их внутренняя копия, а вот параметр передается по ссылке, то бишь изменения отразятся и на оригинале. Если [] (const SomeBigObject & _val) { … }, то ничего не захватывается, а параметр принимается по константной ссылке и т.д.

Я так понял, что выполнить захват «по константной ссылке» невозможно. Ну, может, оно нам и не надо.

А что будет, если мы напишем такое, слегка надуманное лямбда-выражение внутри метода класса:

// Листинг 11
#include <algorithm>
#include <cstdlib>
#include <iostream>
#include <iterator>
#include <vector>
 
using namespace std;
 
class MyMegaInitializer
{
public:
	MyMegaInitializer(int _base, int _power)
		: m_val(_base)
		, m_power(_power)
	{}
 
	void initializeVector(vector<int> & _vec)
	{
		for_each(_vec.begin(), _vec.end(),
		         [m_val, m_power] (int & _val) mutable
		{
			_val = m_val;
			m_val *= m_power;
		});
	}
 
private:
	int m_val, m_power;
};
 
int main()
{
	vector<int> myVec(11);
	MyMegaInitializer initializer(1, 2);
	initializer.initializeVector(myVec);
 
	return EXIT_SUCCESS;
}


Несмотря на все наши ожидания, код не будет скомпилирован, так как компилятор не сможет захватить m_val и m_power: эти переменные вне области видимости. Вот что говорит на это Visual Studio:
«error C3480: 'MyMegaInitializer::m_power': a lambda capture variable must be from an enclosing function scope»

Как же быть? Чтобы получить доступ к членам класса, в capture-list нужно поместить this:

// Листинг 12
#include <algorithm>
#include <cstdlib>
#include <iostream>
#include <iterator>
#include <vector>
 
using namespace std;
 
class MyMegaInitializer
{
public:
	MyMegaInitializer(int _base, int _power)
		: m_val(_base)
		, m_power(_power)
	{}
 
	void initializeVector(vector<int> & _vec)
	{
		for_each(_vec.begin(), _vec.end(), [this] (int & _val) mutable
		{
			_val = m_val;
			m_val *= m_power; 
		});
	}
 
private:
	int m_val, m_power;
};
 
int main()
{
	vector<int> myVec(11);
	MyMegaInitializer initializer(1, 2);
	initializer.initializeVector(myVec);
 
	for_each(myVec.begin(), myVec.end(), [] (int _val)
	{
		cout << _val << " ";
	});
	cout << endl;
 
	return EXIT_SUCCESS;
}


Данная программа делает именно то, чего мы ожидали:
1 2 4 8 16 32 64 128 256 512 1024


Следует заметить, что this можно захватить только по значению, и если вы попытаетесь произвести захват по ссылке, компилятор выдаст ошибку. Даже если вы в коде из Листинга 12 напишете [&] вместо [this], то this будет все равно захвачен по значению.

Прочее



Помимо всего вышеперечисленного, в заголовке лямбда-выражения можно указать throw-list — список исключений, которые лямбда может сгенерировать. Например, такая лямбда не может генерировать исключения:
[] (int _n) throw() { … }

А такая генерирует только bad_alloc:
[=] (const std::string & _str) mutable throw(std::bad_alloc) -> bool { … }

И т.п.

Естественно, если его не указывать, то лямбда может генерировать любое исключение.

К счастью, в финальном варианте стандарта throw-спецификации объявлены устаревшими. Вместо этого оставили ключевое слово noexcept, которое говорит, что функция не должна генерировать исключение вообще.

Таким образом, общий вид лямбда-выражения следующий (сорри за такой «вольный вид» грамматики):
    lambda-expression ::=
                  ‘[’  [<список_захвата>]  ‘]’
                [ ‘(’  <список_параметров> ‘)’  [‘mutable’ ] ]
                [ ‘noexcept’ ]
                [ ‘->’  <тип_возвращаемого_значения> ]
                  ‘{’  [<тело_лямбды>]  ‘}’


Повторное использование лямбда-выражений. Генерация лямбда-выражений.



Все вышеперечисленное довольно удобно, но основная мощь лямбда-выражений приходится на то, что мы можем сохранить лямбду в переменной или передавать как параметр в функцию. В Boost для этого есть класс Function, который, если я не ошибаюсь, войдет в новый стандарт STL (возможно, в немного измененном виде). На данный момент уже можно поюзать фичи из обновленного STL, однако, пока что эти фичи находятся в подпространстве имен std::tr1.

Возможность сохранения лямбда-выражений позволяет нам не только повторно использовать лямбды, но и писать функции, которые генерируют лямбда-выражения, и даже лямбды, которые генерируют лямбды.

Рассмотрим следующий пример:

// Листинг 13
#include <algorithm>
#include <cstdlib>
#include <functional>
#include <iostream>
#include <iterator>
#include <vector>
 
using namespace std;
using std::tr1::function;
 
int main()
{
	vector<int> myVec;
	int init = 0;
	generate_n(back_inserter(myVec), 10, [&]
	{
		return init++;
	});
 
	function<void (int)> traceLambda = [] (int _val) -> void
		                           {
			                            cout << _val << " ";
		                           };
 
	for_each(myVec.begin(), myVec.end(), traceLambda);
	cout << endl;
 
	function<function<int (int)> (int)> lambdaGen =
		[] (int _val) -> function<int (int)>
		{
			return [_val] (int _n) -> int { return _n + _val; };
		};
 
	transform(myVec.begin(), myVec.end(), myVec.begin(), lambdaGen(2));
	for_each(myVec.begin(), myVec.end(), traceLambda);
	cout << endl;
 
	return EXIT_SUCCESS;
}


Данная программа выводит:
0 1 2 3 4 5 6 7 8 9
2 3 4 5 6 7 8 9 10 11


Рассмотрим подробнее. Вначале у нас инициализируется вектор с помощью generate_n(). Тут всё просто. Далее мы создаем переменную traceLambda типа function<void (int)> (то есть функция, принимающая int и возвращающая void) и присваиваем ей лямбда-выражение, которое выводит на консоль значение и пробел. Далее мы используем только что сохраненную лямбду для вывода всех элементов вектора.

После этого мы видим немаленькое объявление lambdaGen, которая является лямбда-выражением, принимающим один параметр int и возвращающим другую лямбду, принимающую int и возвращающую int.

Следом за этим мы ко всем элементам вектора применяем transform(), в качестве мутационной функции для которого указываем lambdaGen(2). Фактически lambdaGen(2) возвращает другую лямбду, которая прибавляет к переданному параметру число 2 и возвращает результат. Этот код, естественно, немного надуманный, ибо то же самое можно было записать как
transform(myVec.begin(), myVec.end(), myVec.begin(), bind2nd(plus<int>(), 2));

однако в качестве примера довольно показательно.

Затем мы снова выводим значения всех элементов вектора, используя для этого сохраненную ранее лямбду traceLambda.

На самом деле, данный код можно было записать еще короче. В новом стандарте C++ значение ключевого слова auto будет заменено. Если раньше auto означало, что переменная создается в стеке, и подразумевалось неявно в случае, если вы не указали что-либо другое (register, к примеру), то сейчас это такой себе аналог var в C# (то есть тип переменной, объявленной как auto, определяется компилятором самостоятельно на основе того, чем эта переменная инициализируется).
Следует заметить, что auto-переменная не сможет хранить значения разных типов в течение одного запуска программы. C++ как был, так и остается статически типизированным языком, и указание auto лишь говорит компилятору самостоятельно позаботиться об определении типа: после инициализации сменить тип переменной будет уже нельзя.

Кроме того что ключевое слово auto весьма полезно при работе с циклами вида
for (auto it = vec.begin(); it != vec.end(); ++it)
{
	// ...
}

его очень удобно использовать с лямбда-выражениями. Теперь код из Листинга 13 можно переписать так:

// Листинг 14
#include <algorithm>
#include <cstdlib>
#include <functional>
#include <iostream>
#include <iterator>
#include <vector>
 
using namespace std;
using std::tr1::function;
 
int main()
{
	vector<int> myVec;
	int init = 0;
	generate_n(back_inserter(myVec), 10, [&]
	{
		return init++;
	});
 
	auto traceLambda = [] (int _val) -> void { cout << _val << " "; };
 
	for_each(myVec.begin(), myVec.end(), traceLambda);
	cout << endl;
 
	auto lambdaGen = [] (int _val) -> function<int (int)>
	{
		return [_val] (int _n) -> int { return _n + _val; };
	};
 
	transform(myVec.begin(), myVec.end(), myVec.begin(), lambdaGen(2));
	for_each(myVec.begin(), myVec.end(), traceLambda);
	cout << endl;
 
	return EXIT_SUCCESS;
}


Пожалуй, на этом я закончу описание лямбда-выражений. Если будут вопросы, поправки или замечания, с удовольствием выслушаю.

PROFIT!

Progg it

ETA (20.02.2012): Оказалось, что для кого-то эта статья до сих пор актуальна, поэтому поправил подсветку синтаксиса и подкорректировал информацию про throw-списки в объявлении лямбд. Помимо непосредственно лямбда-выражений другие фичи из нового стандарта С++11 (например, списки инициализации контейнеров) решил не добавлять, так что статья осталась практически в первозданном виде.
Сергей Оленда́ренко @GooRoo
карма
103,2
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (81)

  • +4
    Я только начал вникать в С++0x, но уже сожалею, что убрали концепты. Спасибо за разъяснение. Надеюсь на продолжение.
    • +4
      Спасибо Вам за оценку. Следующей статьёй я думал написать о ссылках на rvalue (rvalue references) также из будущего стандарта.
      • 0
        Да, да, обязательно пишите — у вас это прекрасно получается. Я хоть и почитал уже довольно много про это, но лишним такое описание, как тут, не будет. Ждём!
      • 0
        Обязательно пишите, статья получилась очень хорошая и толковая (и потом — желания курить новый стандарт я в себе так и не нашел, а тут все понятно и доступно).
      • 0
        Пишите пожалуйста. Из нового стандарта rvalue refs — это самое мутное для меня, пожалуй. С удовольствием прочитаю.
        • 0
          Для простоты можете представить, что это «ссылка на ссылку» (по аналогии «указатель на указатель»).

          Мне вот интересно, что GooRooнапишет, покольку я себе примерно представляю для чего это нужно, но он может что-то такое рассказать, что позволит эту фичу использовать ещё эффективней в будущем как говорится — «век живи — век учись» :)
    • НЛО прилетело и опубликовало эту надпись здесь
  • +5
    Слушай, чувак, респект и уважуха за столь подробное разжёвывание темы лямбда-выражений в С++. Очень порадовало, что ты не только рассказал всё более подробно, но и рассказал про подоснову всей этой кухни (это я про функторы). Теперь эти лямбда-выражения в C++ меня реально заинтересовали :) Обязательно теперь поэкспериментирую надосуге…
  • НЛО прилетело и опубликовало эту надпись здесь
    • НЛО прилетело и опубликовало эту надпись здесь
      • НЛО прилетело и опубликовало эту надпись здесь
        • +4
          В последнее время наращивание вычислительной мощности процессоров замедлило ход, и развитие компьютерной техники пошло по пути распараллеливания вычислений. Поэтому в моду начнут входить чистые функциональные языки программирования, такие как Haskell к примеру, ибо в них это самое распараллеливание делается крайне легко. Но так как на текущий момент одним из нюансов является не слишком высокое быстродействие программ, написанных на Haskell, в сравнении с их C++-аналогами, скорее всего в будущем появится новая, гибридная методология, которая будет включать как элементы ООП и функционального программирования, так и, возможно, что-то совершенно новое. IMHO :)
          • +1
            Очень часто самое интересное получается как раз на стыке технологий.
        • +4
          Каким образом (анонимные) функторы противоречат ООП?
          • 0
            Если функтор принимает в качестве параметра другой функтор, а возвращает третий — это смотрится несколько странновато и непривычно, хоть, возможно, и не противоречит ООП.
            • +1
              Для C++ — пожалуй.
              • +1
                Последнее время, по-моему, для С++ уже нет ничего странного и необычного.
          • НЛО прилетело и опубликовало эту надпись здесь
            • 0
              Суть от этого не меняется.
              • НЛО прилетело и опубликовало эту надпись здесь
                • 0
                  Если не злоупотреблять размерами лямбд, то ничего не усложняется. А так, в С++ полно мест, где можно очень неплохо на голову себе навесить проблем. Принципиально лямбды ситуацию не ухудшат.
        • 0
          Это не goto, это способ алгоритмической абстракции (здесь в статье хороших примеров нету, но подошло бы объяснение fold/map).

          В ООП (которое message passing) такие «костыли» (лол) тоже есть, ибо сами сообщения — объекты первого класса.

          Как раз таки *отсутствие* функций/сообщений первого класса приводит к костылям.
          • 0
            По сути, map — это transform, а fold (он же свёртка списка) — это accumulate, если я всё правильно понял. Это, конечно, очень грубо говоря…

            Я только недавно начал учить Haskell, поэтому пока что не могу привести хорошие примеры :(
        • 0
          А то, что само ООП в С++ немного костыльное, вас не смущает?
  • 0
    да у меня то же что-то ругается на этом месте
    • 0
      Эм… Дело в том, что это фича из будущего стандарта C++, и сейчас она реализована только в Visual Studio 2010 Beta и отдельных бранчах gcc вроде как.
  • 0
    $ gcc-config -c
    x86_64-pc-linux-gnu-4.4.1
    получается в последнем gcc эта фича еще не реализована, очень жаль.

    P.S. Хотя, с другой стороный, второй листинг скомпилировался без поблем.
    • 0
      Эта фича еще не вошла в trunk проекта gcc, потому что новый стандарт C++ по сути еще не вышел, но в отдельных сборках, я слышал, уже работает.

      А второй листинг скомпилировался потому, что это как бы аналог первого листинга, но записанный исключительно средствами текущего стандарта C++.
  • 0
    извиняюсь, туплю ) там же нет никаких лямбд.

    А так спасибо, очень интересно, в общем-то как и все новое (:
    • +3
      Учите Haskell ;) — там всё новое…
      • 0
        хах, буквально вчера за него принялся, действительно интересно :)
  • 0
    Статья действительно хорошая. А вот лямбды в c++ конечно выглядят громоздко. По той же причине, я использую замыкания в javascript но не использую анонимных функций т.к. это выглядит не так мило как в Lisp/Haskell/ML и проще объявить их рядышком.
  • 0
    суровая статья, спасибо
    у меня вопрос: как все это будет работать без автоматической сборки мусора? я псал на С++ достаточно давно и уже не в теме, но не принесут ли лямбды в С++ лишней головной боли с утечками памяти? и вообще они каким-то образом влияют в этом вопросе или бояться нечего? мне было бы интересно узнать

    за статью спасибо, жду продолжений и подобных статей, автор, пиши еще :-)
    • 0
      Ну если фактически компилятором это будет разворачиваться в функтор, то, мне кажется, утечек памяти это за собой не повлечет. В любом ведь случае эти лямбды будут создаваться совсем не динамически.
  • +1
    std::copy( a.begin(), a.end(), std::ostream_iterator( std::cout, " " ) );

    а не надуманные трейслямбды
    • +2
      и опять ни слова про действие лямбды вне области видимости переменных.
    • 0
      Там есть и такое. Трейслямбда чисто для примера. Давайте тогда так:

      int elementsInRow = 0;
      auto traceLambda = [=] (int _val) mutable
      {
              cout << _val << " ";
              if (++elementsInRow % 5 == 0)
              {
                      elementsInRow = 0;
                      cout << endl;
              }
      };

      • 0
        А зачем elementsInRow обнулять если % 5 написано? Можно либо написать if (++elementsInRow == 5) либо убрать обнуление, без изменения смысла
        • 0
          Да, сорри, это у меня гибрид из двух разных версий :)

          Без обнуления не пойдет… Мало ли, сколько у нас там элементов. Должно быть вот так:

          int elementsInRow = 0;
          auto traceLambda = [=] (int _val) mutable
          {
                  cout << _val << " ";
                  if (++elementsInRow == 5)
                  {
                          elementsInRow = 0;
                          cout << endl;
                  }
          };
          • 0
            Да, my fault. Хотя маловероятно что мы будем обрабатывать коллекцию с 2 миллиардами интов, это же 8 гигов памяти. Или в stl можно будет конструировать бесконечные списки как в haskell?
            • –1
              Насколько я знаю, бесконечных списков не предвидится. А 8 гигов памяти в наше время — не так и много…
  • +1
    Громоздко слишком. Вот в том же ATS (ats-lang.org) это делается проще и практически также эффективно.

    Да-да, я понимаю, что это исследовательский язык и все такое, но С++никам следует обратить на него пристальное внимание.
  • НЛО прилетело и опубликовало эту надпись здесь
    • 0
      Скорее всего, это просто тренды, не следовать которым — отставать в практической применимости. С++ изначально представлялся конструктором, с помощью которого можно стоить не только дома, но и инструмент, с помощью которого эти дома делаются. Но когда количество самодельного инструмента превысило отметку в 9000, все-таки решили причесать этот зоопарк и ввести наиболее употребимое непосредственно в язык. Временами получается несколько неуклюже (как раз из-за начальной ориентации в перпендикулярном направлении): и синтаксис лямб — не верх изящества.
    • +1
      Только монад и pattern matching'а не хватает)
      • 0
        Не переживайте :) C++ — мультипарадигменный язык… Будут :))) гг
        • НЛО прилетело и опубликовало эту надпись здесь
  • 0
    В листинге 13 необходимо явно указывать возвращаемый тип первой лямбды -> void?
    • 0
      как сказал автор статьи — это тип возвразщаемый по умолчанию. Поэтому указывать его явно не обязательно
      • 0
        ну кроме случаев, когда тама есть один retun, который и определяет возвращаемый тип.
        • 0
          Да, -> void можно не указывать, а вот дальше -> function<int (int)> все-таки пришлось указать, потому что компилятор сам не смог вывести тип.
  • НЛО прилетело и опубликовало эту надпись здесь
    • НЛО прилетело и опубликовало эту надпись здесь
  • +1
    Есть неосвещённое белое пятно.
    Что происходит со ссылками на переменные, вышедшие из области видимости?

    function<void ()> foo()
    {
    int init = 0;
    return [&init]() { ++init; };
    }

    Подозреваю, что со ссылкой ничего не произойдёт, после возврата из foo она будет указывать на адрес на стеке и использование результата функции foo() будет разрушать значение в этой ячейке стека.

    Или я не прав?
    • 0
      > Есть неосвещённое белое пятно.
      Мда, ну и фразу написал…
      Grammar nazi негодуе.
    • +1
      Замените это на функторы и всё станет ясно.
      Вы сохранили ссылку на переменную, которой после выхода из функции нет. Что в таком случае положено? Не важно, лямбда там или нет, обращение по этой ссылке — undefined behaviour.
  • +1
    Прекрасная статья, снимаю шляпу вместе с волосами перед умением автора доносить мыслю. Отплюсовался где только мог :)
  • +2
    спасибо за статью. тема сисек лямбда функций почти раскрыта
    • +1
      Спасибо. В следующий раз постараюсь донести больше сисек подробностей о фичах нового стандарта…
  • 0
    респект автору. статья очень полезная.
  • 0
    Спасибо, было приятно ознакомиться, пока еще не приходилось читать такого внятного рассказа о лямбдах в C++.
  • 0
    Статья не такая глубокая, как у Вас (а прямо скажем — поверхностная :), но, возможно, Вам будет интересно: habrahabr.ru/blogs/macosxdev/66632/
    • +1
      Спасибо большое за ссылку! Почитал. Довольно интересно, но я так и не заставил себя всерьез заняться изучением Objective-C. На первый взгляд замыкания там действительно попроще, чем в C++ :)
      • 0
        Obj-C мне всегда казался попроще, это да.

        Но главное, что они и в чистом Си/Си++ работают — главное, компилировать средствами от Apple :)
        • +1
          Если нужно компилировать только средствами от Apple, то это уже не чистые C/C++ ;)

          P.S. Ей-богу, не осилил разобраться в XCode. Мне сказали, что XCode удобно использовать только для Obj-C, но не для C++. Посему с программингом под Mac у меня как-то вообще не сложилось.
          • 0
            XCode выглядит бедновато после Visual C, это правда… но оказывается, что им реально удобно пользоваться, вот что странно. Поддержка C++ и Obj-C там примерно на одинаковом уровне (ну разве что рефакторинга для C++, если не ошибаюсь, нет).

            Но по сути все равно все сводится к вызову gcc (и именно gcc и расширен apple'ом в первую очередь), так что можно все писать в emacs/vim, как обычно в unix. Interface Builder (чтобы рисовать GUI) мне больше нравится, чем в Visual C, кстати.

            В предстоящем в сентябре snow leopard't среда xcode стала еще мягче, функциональнее и приятнее — по-моим ощущениям.

            На тему «не чистый С++» — конечно! Я в своем хабратопике на эту тему ответил. Но это работает, и это удобно. Более того, миром Apple'у прощается даже редкостное свинство — ради программирования под айфон взять и поставить кучу разработчиков перед выбором «ObjC или ничего». Смело :)
            • +1
              Я касательно C++ в последнее время вообще подсел на Qt, поэтому предпочитаю Visual C++ (которому IMHO нет равных) и Qt Creator.

              А вообще сейчас пытаюсь переквалифицироваться в Haskell-программера :) Так что до Obj-C я точно не дойду.
              • 0
                Haskell — это то, к чему жизнь толкает меня последнее время со всех сторон. Дай Бог, доберусь. Obj-C мне более-менее близок, потому что я идейный поклонник Смоллтока, в котором растут ObjC-корни.

                Вот мне тут друг (идейный поклонник Haskell) написал сегодня, что GHC портировали под айфон. Правда, не знаю, как там с Cocoa Touch бридж есть? и работает ли? Было бы здорово.

                А то Squeak Smalltalk портировали, а бриджа нет… родный-то UI Сквика выглядит под iPhone, мягко говоря, убого…
                • 0
                  Не слышал, честно говоря. Мне продукты Apple нравятся, но я от них не настолько фанатею, чтобы все силы кидать на программинг под них.
  • 0
    а нетипизированные лямбда-функции существуют (в виде шаблонов)?
    • 0
      А зачем?
      • –1
        я же вас не спрашиваю, зачем ввели лямбду, когда спокойно обходилось обычными функторами. вот и вы, пожалуйста, тоже…

        ладно, так уж и быть, отвечу на ваш вопрос. без лямбда-шаблонов новшество будет уступать функторам в гибкости.
        • 0
          Если ввели, значит «обходились неспокойно», однако для программистов С++ (не знакомых с функциональным программированием) лямбда-выражения скорее всего будут не более чем синтаксическим сахаром, который иногда весьма удобно использовать. Но Вам же никто не говорит заменять все функторы на лямбды.

          А как Вы себе вообще представляете объявление шаблонной лямбды?
          • –1
            А так ли важно, как я их себе представляю? То, что паре программистов с хабра не удалось этот синтаксис себе вообразить, не должно наводить на мысль о невозможности или ненужности. Было бы желание, а синтаксис придумать можно. Да хоть включив объявление параметров шаблона в начало списка параметров функции:

            []...(typename T1, typename T2, int i, T1* pt1, const vector& v) {...}

            Но на самом деле мне не нравятся две вещи. Во-первых, синтаксис — недостаточно элегантно для меня, чтобы отдавать ему предпочтение. А во-вторых, вложенные лямбды будут отрицательно сказываться на удобочитаемости кода (впрочем, этот момент мне даже в лиспе не нравился, когда мы его изучали в университете).
            • 0
              Опечатка в последнем параметре шаблонной лямбды, хабраредактор съел параметры вектора из-за угловых скобок. Вектор должен быть инстанциирован типом T2.
  • +1
    После прочтения статьи волосы встали дыбом. C++ такой C++. В C# просто пишешь лямбду и не задумываешься, какой нафиг у переменной захват, mutable/не mutable, что такое анонимный функтор с перегруженным оператором и так далее. Неужели ЭТИМ будет кто-то всерьез пользоваться? Да тут можно таких дров наломать, что потом неделю отлаживать придется.
    • 0
      Программисты на С++ привыкли думать во время работы. Если для Вас это непривычно, то продолжайте C#, незачем терзать себя таким страшным языком, как С++.
      • 0
        *писать на C#
      • 0
        Зачем вы так) Я очень люблю C++ (и написал на нем много кода), просто утверждаю, что в языке стало больше на одно место, где можно эпически облажаться начинающим программистам.

        Думать это правильно, но ведь зачем-то придумали абстракции, чтобы упростить жизнь нашему брату. Можно писать на ассемблере например, там очень сильно думать надо. Введению auto я очень рад, он избавляет от рутинной работы. Лямбды по идее тоже избавляют от рутинной работы, но… Когда есть с чем сравнивать, становится неловко. Если бы они появились в середине девяностых, они произвели настоящий фурор. Но не в наше время.
        • 0
          И да, появился новый вопрос на собеседовании, соберется ли код
          int main{[](){}()}

          :)
          • 0
            Тьфу, то есть
            int main(){[](){}();}

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.