Search…

C++11 - Lambda

15/09/20205 min read
Tìm hiểu và ứng dụng Lambda - Hàm nặc danh hay Anonymous function trong C++11.

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 1 hàm khác và sử dụng 1 lần. Khác với các cách thông thường, định nghĩa hàm sau đó dùng tên hàm truyền vào 1 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 ở 1 nơi khác, mà có thể tạo ngay 1 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 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, 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: 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 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, 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, nếu như tác vụ đó có thể phải sử dụng chỗ khác trong code, có thể sẽ tạo ra hàm.

Cú pháp

[capture](parameters){
	block_statements
}

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

[](){ // Code ở đây }

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" gán địa chỉ nó vào 1 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 đó gọi "hàm" này vận hành. Nâng cấp hơn cho biểu thức trên, 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. Tương tự, 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], sẽ giải thích thêm về capture.

Xét ví dụ bên dưới để hiểu rõ hơn về capture, thêm int stdio = 5 và sử dụng nó trong biểu thức lambda. Điều đơn giản nhất có thể nghĩ ra vài cách để sử dụng stdio trong biểu thức là xem nó như 1 tham số và truyền vào biểu thức tại cặp ngoặc (). Tuy nhiên sẽ dùng cách khác, 1 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 1 lập trình viên khác viết, hoặc cụ thể hơn 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 không thể sử được phương pháp này. Vấn đề là 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[] 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 có các cách thức nào để "xen" vào? Có thể sửa biểu thức trên như sau (thêm dấu = 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à 1 dạng nâng cao, 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ư 1 bản sao, có thể thử các dạng capture trên với OOP để khám phá thêm.

Phần bổ sung

Trong trường hợp như đã nêu của bạn Lộc Thọ (dưới thảo luận) có if (biểu thức), trong ý định này tức là 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, cần làm như sau.

[] {// Codes ở đây } ( ) 

Cần lưu ý vị trí của dấu ngoặc tròn () khi tạo ra 1 biểu thức.

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

Có thể đọc thêm về C++14 - Generic Lambda để hiểu thêm về sự tiến hóa của Lambda.

IO Stream

IO Stream Co., Ltd

30 Trinh Dinh Thao, Hoa Thanh ward, Tan Phu district, Ho Chi Minh city, Vietnam
+84 28 22 00 11 12
developer@iostream.co

383/1 Quang Trung, ward 10, Go Vap district, Ho Chi Minh city
Business license number: 0311563559 issued by the Department of Planning and Investment of Ho Chi Minh City on February 23, 2012

©IO Stream, 2013 - 2024