Nội dung bài viết
Đăng ký học lập trình C++
Tại STDIO bạn được dạy nền tảng lập trình tốt nhất.
Đăng ký học
La Kiến Vinh Tìm hiểu và ứng dụng Lambda - Hàm nặc danh hay Anonymous function trong C++11. Lambda là một loại triển khai hàm với hàm không tên gọi, lambda tiện lợi cho việc sử dụng hàm chuyên biệt cho một tính năng.

Giới thiệu

Có nhiều ngôn ngữ triển khai hàm Nặc Danh (Anonymous Function) như Python, Perl, C#, JavaScript, ... và ở phiên bản C++11 Lambda chính là tên gọi của kỹ thuật này.

Hàm nặc danh là hàm có triển khai thân hàm nhưng khác với các hàm khác là nó không có tên hàm.

Tiền đề bài viết

Bài viết này cùng với các bài viết về C++11 khác nhằm giới thiệu một số tính năng mới của C++11.

Đối tượng hướng đến

Để hiểu được bài viết này, bạn cần có các kiến thức sau:

  • Con trỏ hàm hoặc tương đương như Delegates.

Danh sách bài viết về C++11

Khái niệm Lambda

Lambda hay còn gọi là hàm nặc danh, nó có thể dùng để truyền vào một hàm khác và sử dụng một lần. Khác với các cách thông thường, ta định nghĩa hàm sau đó dùng tên hàm truyền vào một hàm khác.

Lambda đã được giới thiệu ở nhiều ngôn ngữ như Java, C#, JavaScript, và ở phiên bản C++11 đã có tính năng này.

Lợi ích của lambda là không nhất thiết phải khai báo tên hàm ở một nơi khác, mà có thể tạo ngay một hàm (dùng 1 lần hay hiểu chính xác hơn là chỉ có 1 chỗ gọi 1 số tác vụ nhỏ). Như vậy, ta sẽ giảm được thời gian khai báo 1 hàm. Để làm rõ hơn về khái niệm này, ta sẽ xét 2 ví dụ sau.

Ví dụ 1 - Khai báo hàm sau đó truyền hàm vào hàm khác

#include <iostream>
#include <vector>

using namespace std;

void stdio_doing(int n)
{
	n = n + 1;
	cout << n;
}

void for_each (vector<int> v1, void (*func)(int a))
{
	for (auto i = v1.begin(); i != v1.end(); i++)
	{
		func(*i);
	}
}

void main()
{
	vector<int> v;
	v.push_back(0);
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);

	for_each(v, stdio_doing);
}

Ví dụ 2 - Truyền thẳng biểu thức lambda (hàm) vào hàm khác

#include <iostream>
#include <vector>

using namespace std;

void for_each (vector<int> v1, void (*func)(int a))
{
	for (auto i = v1.begin(); i != v1.end(); i++)
	{
		func(*i);
	}
}

void main()
{
	vector<int> v;
	v.push_back(0);
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);

	for_each(v, [](int a){
		a = a + 1;
		cout << a;
	});
}

Xem xét ví dụ 2: ta đã không cần hiện thực trước hàm stdio_doing mà đến khi thực sự sử dụng for_each, ta mới cần hiện thực stdio_doing này.

Tác vụ của stdio_doing khá nhỏ và giả sử chỉ dành cho việc truyền vào for_each, ta sẽ có cảm giác hơi phí thời gian để  hiện thực stdio_doing. Tuy nhiên phải nói rõ rằng, việc hiện thực stdio_doing hay sử dụng lambda là do cảm giác và kinh nghiệm của bạn. Bản thân tôi cũng sẽ suy nghĩ xem có nên dùng Lambda không, nếu như tác vụ đó có thể phải sử dụng chỗ khác trong code, có thể tôi sẽ tạo ra hàm.

Điều thú vị nữa ở trên là tôi đã tặng cho bạn một mẹo nhỏ hiện thực "vòng lặp" for_each.

Cú pháp

[capture](parameters){
	block_statements
}

Cú pháp của Lambda cực kỳ đơn giản, ta chỉ cần bắt đầu với

[](){ // Code của bạn ở đây }

Ta thử tạo 1 biểu thức lambda đơn giản nhất có thể như codes bên dưới.

#include <iostream>

using namespace std;

void main()
{
	void (*f)() = []() {
		cout << "lambda ";
		cout << "expression";
	};

	f();
}

Sau khi tạo xong biểu thức, "sẵn tiện" tôi gán địa chỉ nó vào một con trỏ hàm khác (vì nếu không có đối tượng nào kiểm soát địa chỉ của hàm này, thì không có cách nào sử dụng được). sau đó tôi gọi "hàm" này vận hành. Nâng cấp hơn cho biểu thức trên, ta có thể thử trả về 1 giá trị, dĩ nhiên là con trỏ hàm f cũng phải có kiểu trả về là kiểu tương ứng với kiểu trả về trong biểu thức lambda (các bạn có thể tự trải nghiệm điều này). Tương tự, bạn có thể thử nghiệm truyền các parameters vào biểu thức này. Riêng giữa 2 dấu ngoặc vuông [capture], tôi sẽ giải thích thêm về capture.

Xét ví dụ bên dưới để hiểu rõ hơn về capture, ta thêm int stdio = 5 và muốn sử dụng nó trong biểu thức lambda. Điều đơn giản nhất bạn có thể nghĩ ra vài cách để sử dụng stdio trong biểu thức là xem nó như một tham số và truyền vào biểu thức tại cặp ngoặc (). Tuy nhiên, ta không muốn sử dụng như vậy, ta sẽ dùng cách khác, một sự thật hiển nhiên là đôi lúc khung sườn (kiểu trả về và tham số đầu vào) của hàm do một lập trình viên khác viết, hoặc cụ thể hơn ta khảo sát cách thức hoạt động của for_each phía trên sẽ thấy được sự bất tiện này nên ta không thể sử được phương pháp này. Vấn đề là ta sẽ sử dụng [capture] để cho phép biểu thức lambda sử dụng các biến bên ngoài và sử dụng nó như thế nào cũng do capture quy định.

#include <iostream>

using namespace std;

void main()
{
	int stdio = 5;

	int (*f)() = []() {
		cout << "lambda ";
		cout << "expression";
		cout << stdio;
		return 1;
	};

	cout << f();
}

Codes trên sẽ được trình biên dịch báo lỗi tại dòng cout << stdio vì [] không có gì, tức là không cho phép bất cứ giá trị nào được "xen" vào biểu thức từ phía ngoài. Vậy ta có các cách thức nào để "xen" vào? Ta có thể sửa biểu thức trên như sau (tôi thêm dấu bằng vào cặp ngoặc vuông, ám chỉ việc cho phép các lambda sử dụng bản sao của các giá trị bên ngoài).

#include <iostream>

using namespace std;

void main()
{
	int stdio = 5;

	int (*f)() = [=]() {
		cout << "lambda ";
		cout << "expression";
		cout << stdio;
		return 1;
	};

	cout << f();
}

Các kiểu capture

[] Không được phép sử dụng bất kỳ biến bên ngoài
[=] Được phép sử dụng các biến bên ngoài, nhưng là dạng sao chép giá trị của biến đó (by value)
[&] Được phép sử dụng chính biến đó (by reference)
[=,&a] Đây là một dạng nâng cao, bạn có thể cho phép tất cả các biến khác được sử dụng giá trị, và chỉ định thêm cho biến a được sử dụng chính nó (by reference)
[this] Cho phép sử dụng this (OOP) như một bản sao, bạn có thể thử các dạng capture trên với OOP để khám phá thêm.

Phần bổ sung

Từ thắc mắc của bạn Lộc Thọ dưới phần trao đổi, tôi bổ sung thêm cách tạo ra lambda express và gọi thực thi tại nơi tạo ngay.

Trong trường hợp như đã nêu của bạn Lộc Thọ ta có if (biểu thức), trong ý định này tức là ta khai báo biểu thức trong if và mong đợi nó sẽ thực thi ngay thay vì phải chờ gọi, ta cần làm như sau. [] {// Codes của bạn ở đây } ( ) Bạn cần lưu ý vị trí của dấu ngoặc tròn () khi tạo ra một biểu thức.

[](){} sẽ tạo ra 1 hàm, nhưng nó chưa được gọi (ta phải gọi nó sau).
[]{}() sẽ tạo ra 1 hàm, và gọi thực thi hàm này ngay.

Bạn có thể đọc thêm về C++14 - Generic Lambda :: www.stdio.vn/articles/read/349/c14-generic-lambda để hiểu thêm về sự tiến hóa của Lambda.

Lời cám ơn

Cám ơn Lộc Thộ đã giúp STDIO :: www.stdio.vn hoàn thiện bài viết này.

THẢO LUẬN
ĐÓNG