Giới thiệu về lambdas - Tin học đại cương (IT1110) | Trường Đại học Bách khoa Hà Nội

Các hàm lambda là một khái niệm khá trực quan của Modern C ++ được giới thiệu trong C ++ 11, vì vậy đã có rất nhiều bài viết về hướng dẫn hàm lambda trên internet. Nhưng vẫn còn một số điều chưa kể (như IIFE, các loại lambda, v.v.) còn sót lại mà không ai nói đến.

lOMoARcPSD| 27879799
Các hàm lambda là một khái niệm khá trực quan của Modern C ++ được giới thiệu trong C ++ 11, vì vy
đã có rất nhiều bài viết về ớng dẫn hàm lambda trên internet. Nhưng vẫn còn một sđiều chưa k
(như IIFE, các loại lambda, v.v.) còn sót lại mà không ai nói đến. Do đó, ở đây tôi không chỉ cho bạn thấy
hàm lambda trong C ++, mà chúng tôi sẽ đề cập đến cách hoạt động bên trong và các khía cạnh khác của
Lambda.
Tiêu đề của bài viết này là một chút sai lầm. Bởi vì lambda không phải lúc nào cũng tổng hợp thành con
trỏ hàm. Đó là một biểu thức (đóng chính xác là duy nhất). Nhưng tôi đã giữ nó theo cách đó cho đơn
giản. Vì vậy, từ bây giờ, tôi sẽ sử dụng hàm lambda và biểu thức thay thế cho nhau.
Hàm Lambda là gì?
Hàm lambda là một đoạn mã ngn:
Không đáng đặt tên (không tên, ẩn danh, dùng một lần, v.v. Bạn có thể gọi nó là gì), và
cũng sẽ không được sử dụng lại.
Nói cách khác, nó chỉ là đưng cú pháp. cú pháp hàm lambda được định nghĩa là:
[ capture list ] (parameters) -> return-type
{
method definition }
Thông thường, trình biên dịch đánh giá kiểu trả về của chính hàm lambda. Vì vậy, chúng ta không cần chỉ
định kiểu trả về theo sau một cách rõ ràng, tức là -> kiểu trả về.
Tuy nhiên, trong một s trường hợp phức tạp, trình biên dịch không thể suy ra kiểu trả về và chúng ta
cần xác định điều đó.
Tại sao sử dụng một hàm Lambda?
C ++ bao gồm nhiều hàm chung hữu ích, như std :: for_each, có thể hữu ích. Thật không may, chúng cũng
có thể khá cng kềnh để sử dụng, đặc biệt nếu functor bạn muốn áp dụng là duy nhất cho một chức
năng cụ thể. Hãy xem xét đoạn mã sau để làm ví dụ:
lOMoARcPSD| 27879799
struct print
{
void operator()(int element)
{
cout << element << endl;
}
};
int main(void)
{
std::vector<int> v = {1, 2, 3, 4, 5}; std::for_each(v.begin(),
v.end(), print()); return 0; }
Nếu bạn sử dụng bản in một lần, ở một nơi cụ thể, có vẻ như quá mức cần thiết để viết cả lớp chỉ để làm
một việc nhỏ nhặt và một lần.
Tuy nhiên, loại mã nội tuyến của nh huống này sẽ phù hợp hơn và thích hợp hơn có thể đạt được
bằng hàm lambda như sau:
std :: for_each (v.begin (), v.end (), [] (int element) {cout << element << endl;}); Các
chức năng của Lambda hoạt động nội bộ như thế nào?
[&i] ( ) { std::cout << i; }
// is equivalent to
struct anonymous
{
int &m_i;
anonymous(int &i) : m_i(i) {}
inline auto operator()() const
{
std::cout << i;
}
};
Trình biên dịch tạo ra bao đóng duy nhất như trên cho mỗi hàm lambda. Cuối cùng, bí mật cũng được
ết lộ. Danh sách Capture sẽ trở thành một đối số phương thức khởi tạo trong bao đóng. Nếu bạn nắm
bắt đối số ới dạng giá trị, thì thành viên dữ liệu kiểu tương ứng sẽ được tạo trong bao đóng.
Hơn nữa, bạn có thể khai báo một biến / đối tượng trong đối số hàm lambda, đối số này sẽ tr thành đối
số để gọi toán tử, tức là operator ().
Lợi ích của việc sử dụng một hàm Lambda
Chi phí trừu tượng bằng không. Đúng! Bạn đọc nó đúng. Lambda không tốn hiệu suất của bạn và nó
nhanh như một chức năng bình thường.
lOMoARcPSD| 27879799
Ngoài ra, mã trở nên nh gọn, có cấu trúc và biểu cảm.
Học biểu thức Lambda
Nắm bắt theo tham chiếu / giá trị
int main()
{
int x = 100, y = 200;
auto print = [&] { // Capturing object by reference
std::cout << __PRETTY_FUNCTION__ << " : " << x << " , " << y <<
std::endl;
};
print();
return 0;
}
Đầu ra: main () :: <lambda ()>:
100, 200
Trong ví dụ trên, tôi đã đề cập & trong danh sách chụp. Điều này nắm bắt biến x và y làm tham chiếu.
Tương tự, = biểu thị giá trị được nắm bắt, điều này sẽ tạo thành viên dữ liệu cùng loại trong quá trình
đóng và việc gán sao chép sẽ diễn ra.
Lưu ý rằng danh sách tham số là tùy chọn; bạn có thể bỏ qua các dấu ngoặc trống nếu bạn không chuyển
các đối số vào biểu thức lambda.
Lambda Capture List
Bảng sau đây cho thấy các trường hợp sử dụng khác nhau cho cùng một:
[] () {} không có ảnh chụp
[=] () {} chụp mọi thứ bằng bản sao (không được đề xut)
[&] () {} ghi lại mọi thứ bằng cách tham khảo (không được đề xut)
[x] () {} chụp x bằng bản sao
[& x] () {} chụp x bằng cách tham chiếu
lOMoARcPSD| 27879799
[&, x] () {} chụp x bằng bản sao, mọi thứ khác bằng cách tham chiếu
[=, & x] () {} chụp x theo tham chiếu, mọi thứ khác bằng bản sao
Chuyển Lambda làm tham số
template <typename Functor>
void f(Functor functor)
{
std::cout << __PRETTY_FUNCTION__ << std::endl;
}
/* Or alternatively you can use this
void f(std::function<int(int)> functor)
{
std::cout << __PRETTY_FUNCTION__ << std::endl;
}
*/
int g() { static int i = 0; return i++; }
int main()
{
auto lambda_func = [i = 0]() mutable { return i++; };
f(lambda_func); // Pass lambda
f(g); // Pass function
}
Đầu ra:
Function Type : void f(Functor) [with Functor = main()::<lambda(int)>]
Function Type : void f(Functor) [with Functor = int (*)(int)]
Bạn cũng có thể chuyển các hàm lambda làm đối số cho các hàm khác giống như một hàm bình thường
mà tôi đã viết mã ở trên.
Nếu bạn để ý, ở đây tôi đã khai báo một biến i trong danh sách chụp, biến này sẽ tr thành thành viên
dữ liệu. Kết quả là, mi khi bạn gọi lambda_func, nó sẽ được trả về và tăng dần. Nắm bắt biến thành
viên trong Lambda hoặc Con trỏ này
class
Example
{ public:
Example() : m_var(10) {}
void func()
{
[=]() { std::cout << m_var << std::endl; }(); // IIFE
}
private:
int m_var;
};
int main()
{
Example e;
e.func();
lOMoARcPSD| 27879799
}
con trỏ này cũng có thể được ghi lại bằng cách sử dụng [this], [=] hoặc [&]. Trong bất ktrường hợp nào
trong sốy, các thành viên dữ liệu lớp (bao gồm cả private) có thể được truy cập như bạn làm trong
một phương thức thông thường.
Nếu bạn nhìn thấy dòng biểu thức lambda, tôi đã sử dụng extra () ở cuối khai báo hàm lambda được sử
dụng để gọi nó ngay sau khi khai báo. Nó được gọi là IIFE (Biểu thức hàm được gọi ngay lập tc).
Các kiểu hàm Lambda trong C ++
Lambda chung
const auto l = [](auto a, auto b, auto c) {};
// is equivalent to
struct anonymous
{
template <class T0, class T1, class T2>
auto operator()(T0 a, T1 b, T2 c) const
{
}
};
Lambda chung được giới thiệu trong C ++ 14 nắm bắt các tham số với trình xác định tự động.
Variadic Generic Lambda
lOMoARcPSD| 27879799
void print() {}
template <typename First, typename... Rest>
void print(const First &first, Rest &&...
args)
{
std::cout << first << std::endl;
print(args...);
}
int main()
{
auto variadic_generic_lambda = [](auto... param)
{ print(param...);
};
variadic_generic_lambda(1, "lol", 1.1);
}
Lambda với một gói tham số thay đổi sẽ hữu ích trong nhiều trường hợp như gỡ lỗi, hoạt động lặp lại với
đầu vào dliệu khác nhau, v.v.
Hàm Lambda có thể thay đổi
Thông thường, toán tử gọi hàm của lambda là const-by-value, có nghĩa là lambda yêu cầu từ khóa có thể
thay đổi nếu bạn đang nắm bắt bt kthứ gì theo giá trị.
[]() mutable {}
// is equivalent to
struct anonymous
{
auto operator()() // call operator
{
}
};
Chúng tôi đã thấy mt ví dụ về điều này ở trên. Tôi hy vọng bạn nhận thấy nó.
Lambda dưới dạng một con trỏ hàm
#include <iostream>
#include <type_traits>
int main()
{
auto funcPtr = +[] {};
static_assert(std::is_same<decltype(funcPtr), void (*)()>::value);
}
Bạn có thể buộc trình biên dịch tạo lambda dưới dạng một con trỏ hàm thay vì đóng bằng cách thêm +
vào trước nó, như được hiển thị ở trên.
Các chức năng Lambda trả về có thứ tự cao hơn
lOMoARcPSD| 27879799
const auto less_than = [](auto x)
{ return [x](auto y)
{ return y < x;
};
};
int main(void)
{
auto less_than_five = less_than(5);
std::cout << less_than_five(3) << std::endl;
std::cout << less_than_five(10) << std::endl;
return 0;
}
Đi xa hơn một chút, các hàm lambda cũng có thể trvề một hàm lambda khác. Điều này sẽ mở ra cánh
cửa của khả năng tùy biến vô tận, khả năng diễn đạt mã và khả năng tương thích (nhân 琀椀 ện, không
có từ nào như thế này) của mã.
Constexpr Lambda Expression
Kể từ C ++ 17, một biểu thức lambda có thể được khai báo là constexpr.
constexpr auto sum = [](const auto &a, const auto &b) { return a + b; };
/*
is equivalent to constexpr struct anonymous
{
template <class T1, class T2>
constexpr auto operator()(T1 a, T2 b) const
{
return a + b;
}
};
*/ constexpr int answer = sum(10, 10);
Ngay cả khi bạn không chỉ định constexpr, toán tử gi hàm vẫn sẽ là constexpr, nếu điều đó xảy ra để đáp
ứng tất cả các yêu cầu của hàm constexpr.
Lời kết
Tôi hy vọng bạn thích bài viết này. Tôi đã cố gắng trìnhy hầu hết những điều phức tạp xung quanh
lambda bằng một vài ví dụ đơn giản và nhỏ. Bạn nên sử dụng lambda ở bất c nơi nào nó xuất hiện
trong tâm trí bạn xem xét nh biểu cảm của mã và khả năng bảo trì dễ dàng giống như bạn có thể sử
dụng nó trong trình xóa tùy chỉnh cho con trỏ thông minh và với hầu hết các thuật toán STL.
lOMoARcPSD| 27879799
Giới thiệu về lambdas (hàm ẩn danh)
#include <algorithm>
#include <array>
#include <iostream>
#include <string_view>
stac bool containsNut(std::string_view str) // stac means internal linkage in this context
{
// std::string_view::昀椀 nd returns std::string_view::npos, which is a very large number, // if it doesn't
昀椀 nd the substring.
// Otherwise it returns the index where the substring occurs in str.
return (str.昀椀 nd("nut") != std::string_view::npos);
}
int main()
{
constexpr std::array<std::string_view, 4> arr{ "apple", "banana", "walnut", "lemon" };
// std::昀椀 nd_if takes a pointer to a funcon
const auto found{ std::昀椀 nd_if(arr.begin(), arr.end(), containsNut) };
if (found == arr.end())
{
std::cout << "No nuts\n";
}
else
{
std::cout << "Found " << *found << '\n';
}
return 0;
}
Đoạn mã này m kiếm qua một mảng các chuỗi để m phần tử đầu 琀椀 ên có chứa chuỗi con
“nut. Do đó, nó tạo ra kết quả:
Found walnut
Và trong khi nó hoạt động, nó có thể được cải thiện.
Gốc của vấn đề ở đây là std :: 昀椀 nd_if yêu cầu chúng ta chuyển nó một con trỏ hàm. Do đó, chúng tôi
buộc phải xác định một hàm chỉ được sử dụng một ln, phải được đặt tên và phải được đặt trong phạm
vi toàn cục (vì các hàm không thể lồng vào nhau!). Hàm cũng rất ngắn, hầu như dễ dàng phân biệt nó làm
gì từ một dòng mã hơn là từ tên và nhận xét.
lOMoARcPSD| 27879799
Lambdas đến giải cứu
Một biểu thức lambda (còn được gi là lambda hoặc bao đóng) cho phép chúng ta xác định một hàm ẩn
danh bên trong một hàm khác. Việc lồng vào nhau rất quan trọng, vì nó cho phép chúng ta vừa tránh
được ô nhiễm đặt tên không gian tên, vừa xác định hàm càng gần nơi nó được sử dụng càng tốt (cung
cấp thêm ngữ cảnh).
Cú pháp cho lambdas là mt trong những thứ kỳ lạ trong C ++ và cần một chút làm quen. Lambdas có
dạng:
[ captureClause ] ( parameters ) -> returnType
{
statements; }
Cả mệnh đề nắm bắt và tham số đều có thể để trống nếu chúng không cần thiết.
Kiểu trả về là tùy chọn và nếu bị bỏ qua, tự động sẽ được giả định (do đó sử dụng suy luận kiểu được sử
dụng để xác định kiểu trả về). Mặc dù trước đây chúng tôi đã lưu ý rằng nên tránh suy luận kiểu cho các
kiểu trả về hàm, nhưng trong ngữ cảnh này, bạn có thể sử dụng (vì các hàmy thường rất nhỏ).
Cũng xin lưu ý rằng lambdas không có tên, vì vậy chúng tôi không cần cung cấp tên.
Điều này có nghĩa là một định nghĩa lambda tầm thường trông như thế này:
#include <iostream>
int main()
{
[]() {}; // de 昀椀 nes a lambda with no captures, no parameters, and no return type
return 0;
}
y viết lại ví dụ trên bằng lambda:
lOMoARcPSD| 27879799
#include <algorithm>
#include <array>
#include <iostream>
#include <string_view>
int main()
{
constexpr std::array<std::string_view, 4> arr{ "apple", "banana", "walnut", "lemon" };
// De 昀椀 ne the funcon right where we use it.
const auto found{ std::昀椀 nd_if(arr.begin(), arr.end(),
[](std::string_view str) // here's our lambda, no capture clause
{
return (str.昀椀 nd("nut") !=
std::string_view::npos); }) };
if (found == arr.end())
{
std::cout << "No nuts\n";
}
else
{
std::cout << "Found " << *found << '\n';
}
return 0;
}
Điều này hoạt động giống như trường hợp con tr hàm và tạo ra một kết quả giống hệt nhau:
Found walnut
Lưu ý rằng lambda của chúng ta tương tự như thế nào với hàm containsNut của chúng ta. Cả hai đều có
các tham số và cơ quan chức năng giống hệt nhau. Lambda không có mệnh đề nắm bắt (chúng tôi sẽ gii
thích mệnh đề nắm bắt là gì trong bài học 琀椀 ếp theo) vì nó không cần mệnh đề này. Và chúng tôi đã
bỏ qua kiểu trả về theo sau trong lambda (cho ngắn gọn), nhưng vì toán tử! = Trả về bool, lambda của
chúng tôi cũng sẽ trả về bool.
Loại lambda
Trong ví dụ trên, chúng tôi đã xác định một lambda ngay tại nơi nó cần thiết. Việc sử dụng lambda này
đôi khi được gi là một hàm theo nghĩa đen.
lOMoARcPSD| 27879799
Tuy nhiên, việc viết lambda cùng dòng với dòng được sử dụng đôi khi có thể khiến mã khó đọc hơn.
Giống như chúng ta có thể khởi tạo một biến với giá trị ch (hoặc một con trỏ hàm) để sử dụng sau này,
chúng ta cũng có thể khởi tạo một biến lambda với định nghĩa lambda và sau đó sử dụng nó sau. Một
lambda được đặt tên cùng với một tên hàm tốt có thể làm cho mã dễ đọc hơn.
Ví dụ: trong đoạn mã sau, chúng tôi đang sử dụng std :: all_of để kiểm tra xem tất cả các phần tử của
một mảng có chẵn hay không:
// Bad: We have to read the lambda to understand what's happening.
return std::all_of(array.begin(), array.end(), [](int i){ return ((i % 2) == 0); }); // Xấu: Chúng ta
phải đọc lambda để hiểu chuyện gì đang xảy ra. return std :: all_of (array.begin
(), array.end (), [] (int i) {return ((i% 2) == 0);});
Chúng tôi có thể cải thiện khả năng đọc của điều này như sau:
// Good: Instead, we can store the lambda in a named variable and pass it to the funcon. auto isEven{
[](int i)
{
return ((i % 2) == 0);
}
};
return std::all_of(array.begin(), array.end(), isEven);
Lưu ý dòng cuối cùng đọc tốt như thế nào: "trả về liệu tất cả các phần tử trong mảng có chẵn hay không"
Nhưng loại lambda isEven là gì?
Hóa ra, lambdas không có loại mà chúng ta có thể sử dụng một cách rõ ràng. Khi chúng ta viết lambda,
trình biên dịch sẽ tạo ra một kiểu duy nhất chỉ dành cho lambda không hiển thị với chúng ta.
Dành cho người đọc nâng cao
Trên thực tế, lambdas không phải là hàm (đó là một phần của cách chúng tránh giới hạn của việc C ++
không hỗ tr các hàm lồng nhau). Chúng là một loại vt thể đặc biệt được gọi là cái thú. Functors là các
đối tượng có chứa toán tử được nạp chồng () khiến chúng có thể gọi được giống như một hàm.
Mặc dù chúng tôi không biết loại lambda, nhưng có một số cách lưu trữ lambda để sử dụng sau định
nghĩa. Nếu lambda có mệnh đề bắt trống, chúng ta có thể sử dụng một con trỏ hàm thông thường. Trong
bài học 琀椀 ếp theo, chúng tôi giới thiệu về chp lambda, con trỏ hàm sẽ không hot động nữa vào thời
lOMoARcPSD| 27879799
điểm đó. Tuy nhiên, hàm std :: có thể được sử dụng cho lambdas ngay cả khi chúng đang chụp một thứ
gì đó.
#include <funconal>
int main() {
// A regular funcon pointer. Only works with an empty capture clause.
double (*addNumbers1)(double, double){
[](double a, double b) {
return (a + b);
}
};
addNumbers1(1, 2);
// Using std::funcon. The lambda could have a non-empty capture clause (Next lesson).
std::funcon addNumbers2{ // note: pre-C++17, use std::funcon<double(double, double)>
instead
[](double a, double b) {
return (a + b);
}
};
addNumbers2(3, 4);
// Using auto. Stores the lambda with its real type.
auto addNumbers3{
[](double a, double b) {
return (a + b);
}
};
addNumbers3(5, 6);
return 0;
}
Cách duy nhất để sử dụng kiểu thực tế của lambda là tự động. auto cũng có lợi ích là không có chi phí so
với hàm std ::.
lOMoARcPSD| 27879799
Rất 琀椀 ếc, không phải lúc nào chúng ta cũng có thể sử dụng auto. Trong trường hợp lambda thực tế
không xác định (ví dụ: vì chúng tôi đang truyền lambda cho một hàm dưới dạng tham số và người gọi xác
định lambda sẽ được truyền vào), chúng tôi không thể sử dụng auto. Trong những trường hợp như vy,
nên sử dụng hàm std ::.
#include <funconal>
#include <iostream>
// We don't know what fn will be. std::funcon works with regular funcons and lambdas.
void repeat(int repeons, const std::funcon<void(int)>& fn)
{
for (int i{ 0 }; i < repeons; ++i)
{
fn(i);
}
}
int main()
{
repeat(3, [](int i) {
std::cout << i << '\n';
});
return 0;
}
Output
0
1
2
Qui định
Sử dụng tự động khi khởi tạo biến bằng lambdas và std :: func 琀椀 on nếu bạn kng thể khởi tạo biến
bằng lambda.
Lambdas chung
Đối với hầu hết các phần, các tham số lambda hoạt động theo các quy tắc giống như các tham số hàm
thông thường.
lOMoARcPSD| 27879799
Một ngoại lệ đáng chú ý là vì C ++ 14, chúng tôi được phép sử dụng tự động cho các tham số (lưu ý:
trong C ++ 20, các hàm thông thường cũng có thể sử dụng tự động cho các tham số). Khi một lambda có
một hoặc nhiều tham số tự động, trình biên dịch sẽ suy ra loại tham s nào là cần thiết từ các lệnh gọi
đến lambda.
Vì lambdas có một hoặc nhiều tham số tự động có thể hoạt động với nhiều loại khác nhau, chúng được
gọi là lambdas chung.
Dành cho người đọc nâng cao
Khi được sử dụng trong ngữ cảnh của lambda, auto chỉ là cách viết tắt của một tham số mẫu.
Chúng ta hãy xem xét một lambda chung:
lOMoARcPSD| 27879799
#include <algorithm>
#include <array>
#include <iostream>
#include <string_view>
int main() {
constexpr std::array months{ // pre-C++17 use std::array<const char*, 12>
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December"
};
// Search for two consecuve months that start with the same leer.
const auto sameLeer{ std::adjacent_昀椀 nd(months.begin(), months.end(),
[](const auto& a, const auto& b) {
return (a[0] == b[0]);
}) };
// Make sure that two months were found.
if (sameLeer != months.end())
{
// std::next returns the next iterator aer sameLeer
std::cout << *sameLeer << " and " << *std::next(sameLeer)
<< " start with the same leer\n";
}
return 0;
}
Output:
June and July start with the same letter
Trong vd trên,rất nhiều, chúng tôi sử dụng các tham số tự động để nắm bắt các chuỗi của chúng tôi bằng
tham chiếu const. Bởi vì tất cả các loại chuỗi đều cho phép truy cập vào các ký tự riêng lẻ của chúng
thông qua toán tử [], chúng tôi không cần quan tâm liệu người dùng có đang chuyển vào chuỗi std ::
lOMoARcPSD| 27879799
string, C-style hay thứ gì khác hay không. Điều này cho phép chúng tôi viết lambda có thể chấp nhận bất
kỳ điều nào trong số này, có nghĩa là nếu chúng tôi thay đổi loại vài tháng sau, chúng tôi sẽ không phải
viết lại lambda.
Tuy nhiên, tự động không phải lúc nào cũng là lựa chọn tốt nhất. Xem xét:
#include <algorithm>
#include <array>
#include <iostream>
#include <string_view>
int main()
{
constexpr std::array months{ // pre-C++17 use std::array<const char*, 12>
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December"
};
// Count how many months consist of 5 leers
const auto 昀椀 veLeerMonths{ std::count_if(months.begin(), months.end(),
[](std::string_view str) {
return (str.length() == 5);
}) };
std::cout << "There are " << 昀椀 veLeerMonths << " months with 5 leers\n";
return 0;
}
Output:
There are 2 months with 5 letters
Trong ví dụ này, sử dụng auto sẽ suy ra một loại const char *. Chuỗi kiểu C không dễ làm việc (ngoài việc
sử dụng toán tử []). Trong trường hợp này, chúng tôi muốn xác định rõ ràng tham số ới dạng std ::
string_view, điều này cho phép chúng tôi làm việc với dliệu cơ bản dễ dàng hơn nhiều (ví dụ: chúng tôi
có thể hỏi chế độ xem chuỗi về độ dài của nó, ngay cả khi người dùng đã chuyển vào C - kiểu mảng).
lOMoARcPSD| 27879799
Lambdas chung và biến nh
Một điều cần lưu ý là một lambda duy nhất sđược tạo cho mỗi loại khác nhau mà tự động phân giải. Ví
dụ sau cho thấy cách một lambda chung biến thành hai lambda riêng biệt:
#include <algorithm>
#include <array>
#include <iostream>
#include <string_view>
int main() {
// Print a value and count how many mes @print has been called.
auto print{
[](auto value) {
stac int callCount{ 0 };
std::cout << callCount++ << ": " << value << '\n';
}
};
print("hello"); // 0: hello
print("world"); // 1: world
print(1); // 0: 1
print(2); // 1: 2
print("ding dong"); // 2: ding dong
return 0;
}
Output
0: hello
1: world
0: 1
1: 2
2: ding dong
Trong ví dụ trên, chúng ta định nghĩa lambda và sau đó gọi nó với hai tham số khác nhau (tham số dạng
chuỗi ký tự và tham số số nguyên). Điều này tạo ra hai phiên bản khác nhau của lambda (một phiên bản
có tham số chuỗi ký tự và một phiên bản có tham số số nguyên).
lOMoARcPSD| 27879799
Hầu hết thi gian, điều này là không quan trọng. Tuy nhiên, lưu ý rằng nếu lambda chung sử dụng các
biến thời lượng nh, các biến đó sẽ không được chia sẻ giữa các lambda đã tạo.
Chúng ta có thể thy điều này trong ví dụ trên, trong đó mỗi kiểu (chuỗi ký tự và số nguyên) có số ng
duy nhất ca riêng nó! Mặc dù chúng tôi chỉ viết lambda một lần, hai lambda đã được tạo - và mỗi
lambda đều có phiên bản callCount riêng. Để có mt bộ đếm được chia sẻ giữa hai lambda đã tạo, chúng
ta phải xác định một biến toàn cục hoặc một biến cục bộ nh bên ngoài lambda. Như bạn đã biết t
các bài học trước, cả biến cục bộ toàn cục và nh đều có thể y ra sự cố và làm cho việc hiểu mã trở
nên khó khăn hơn. Chúng ta sẽ có thể tránh những biến đó sau khi nói về lambda capture trong bài học
琀椀 ếp theo.
Loại trả lại khấu trừ và các loại lợi nhuận theo sau
Nếu sử dụng kiểu khấu trừ trả về, kiểu trả về của lambda sẽ được suy ra từ các câu lệnh trả về bên trong
lambda. Nếu suy luận kiểu trả về được sử dụng, tất cả các câu lệnh trả về trong lambda phải trả về cùng
một kiểu (nếu không trình biên dịch sẽ không biết nên chọn kiểu nào).
Ví dụ:
#include <iostream>
int main()
{
auto divide{ [](int x, int y, bool bInteger) { // note: no speci 昀椀 ed return type
if (bInteger)
return x / y;
else
return stac_cast<double>(x) / y; // ERROR: return type doesn't match previous return type } };
std::cout << divide(3, 2, true) << '\n';
std::cout << divide(3, 2, false) << '\n';
return 0;
}
Điều này tạo ra lỗi biên dịch vì kiểu trả về của câu lệnh trả về đầu 琀椀 ên (int) không khớp với kiểu tr
về của câu lệnh trả v thứ hai (double).
lOMoARcPSD| 27879799
Trong trường hợp chúng tôi trả lại các loại khác nhau, chúng tôi có hai tùy chọn:
1) Thực hiện chuyển đổi rõ ràng để làm cho tất cc loại trả lại khớp nhau, hoặc
2) chỉ định rõ ràng kiểu trả về cho lambda và để trình biên dịch thực hiện các chuyển đổi ngầm định.
Trường hợp thứ hai thường là lựa chọn tốt hơn:
#include <iostream>
int main()
{
// note: explicitly specifying this returns a double
auto divide{ [](int x, int y, bool bInteger) -> double { if (bInteger)
return x / y; // will do an implicit conversion to double
else
return stac_cast<double>(x) / y;
} };
std::cout << divide(3, 2, true) << '\n';
std::cout << divide(3, 2, false) << '\n';
return 0;
}
Theo cách đó, nếu bạn quyết định thay đổi kiểu trả về, bạn (thường) chỉ cần thay đổi kiểu trả về của
lambda và không chạm vào nội dung lambda.
Các đối tượng chức năng thư viện 琀椀 êu chuẩn
Đối với các hoạt đng phổ biến (ví dụ: bổ sung, phủ định hoặc so sánh), bạn không cần phải viết lambdas
của riêng mình, vì thư viện 琀椀 êu chuẩn đi kèm với nhiều đối tượng cơ bản có thể gọi có thể được sử
dụng thay thế. Chúng được định nghĩa trong 琀椀 êu đề <func 琀椀 onal>.
Standard library function objects
For common operations (e.g. addition, negation, or comparison) you don’t need to write
your own lambdas, because the standard library comes with many basic callable objects
that can be used instead. These are defined in the <functional> header.
lOMoARcPSD| 27879799
In the following example:
1 #include <algorithm>
2 #include <array>
3 #include <iostream>
4
5 bool greater(int a, int b)
6 {
7 // Order @a before @b if @a is greater than @b.
8 return (a > b);
9 }
1
0 int main()
1 {
1 std::array arr{ 13, 90, 99, 5, 40, 80 };
1
2 // Pass greater to std::sort
1 std::sort(arr.begin(), arr.end(), greater);
3
1 for (int i : arr)
4 {
1 std::cout << i << ' ';
5 }
1
6 std::cout << '\n';
1
7 return 0;
1 }
8
1
9
2
0
2
1
2
2
2
3
2
| 1/75

Preview text:

lOMoAR cPSD| 27879799
Các hàm lambda là một khái niệm khá trực quan của Modern C ++ được giới thiệu trong C ++ 11, vì vậy
đã có rất nhiều bài viết về hướng dẫn hàm lambda trên internet. Nhưng vẫn còn một số điều chưa kể
(như IIFE, các loại lambda, v.v.) còn sót lại mà không ai nói đến. Do đó, ở đây tôi không chỉ cho bạn thấy
hàm lambda trong C ++, mà chúng tôi sẽ đề cập đến cách hoạt động bên trong và các khía cạnh khác của Lambda.
Tiêu đề của bài viết này là một chút sai lầm. Bởi vì lambda không phải lúc nào cũng tổng hợp thành con
trỏ hàm. Đó là một biểu thức (đóng chính xác là duy nhất). Nhưng tôi đã giữ nó theo cách đó cho đơn
giản. Vì vậy, từ bây giờ, tôi sẽ sử dụng hàm lambda và biểu thức thay thế cho nhau. Hàm Lambda là gì?
Hàm lambda là một đoạn mã ngắn:
Không đáng đặt tên (không tên, ẩn danh, dùng một lần, v.v. Bạn có thể gọi nó là gì), và
cũng sẽ không được sử dụng lại.
Nói cách khác, nó chỉ là đường cú pháp. cú pháp hàm lambda được định nghĩa là:
[ capture list ] (parameters) -> return-type { method definition }
Thông thường, trình biên dịch đánh giá kiểu trả về của chính hàm lambda. Vì vậy, chúng ta không cần chỉ
định kiểu trả về theo sau một cách rõ ràng, tức là -> kiểu trả về.
Tuy nhiên, trong một số trường hợp phức tạp, trình biên dịch không thể suy ra kiểu trả về và chúng ta
cần xác định điều đó.
Tại sao sử dụng một hàm Lambda?
C ++ bao gồm nhiều hàm chung hữu ích, như std :: for_each, có thể hữu ích. Thật không may, chúng cũng
có thể khá cồng kềnh để sử dụng, đặc biệt nếu functor bạn muốn áp dụng là duy nhất cho một chức
năng cụ thể. Hãy xem xét đoạn mã sau để làm ví dụ: lOMoAR cPSD| 27879799 struct print { void operator()(int element) {
cout << element << endl; } }; int main(void) {
std::vector v = {1, 2, 3, 4, 5}; std::for_each(v.begin(),
v.end(), print()); return 0; }
Nếu bạn sử dụng bản in một lần, ở một nơi cụ thể, có vẻ như quá mức cần thiết để viết cả lớp chỉ để làm
một việc nhỏ nhặt và một lần.
Tuy nhiên, loại mã nội tuyến của 琀 nh huống này sẽ phù hợp hơn và thích hợp hơn có thể đạt được bằng hàm lambda như sau:
std :: for_each (v.begin (), v.end (), [] (int element) {cout << element << endl;}); Các
chức năng của Lambda hoạt động nội bộ như thế nào?
[&i] ( ) { std::cout << i; } // is equivalent to struct anonymous { int &m_i;
anonymous(int &i) : m_i(i) {}
inline auto operator()() const { std::cout << i; } };
Trình biên dịch tạo ra bao đóng duy nhất như trên cho mỗi hàm lambda. Cuối cùng, bí mật cũng được 琀
椀 ết lộ. Danh sách Capture sẽ trở thành một đối số phương thức khởi tạo trong bao đóng. Nếu bạn nắm
bắt đối số dưới dạng giá trị, thì thành viên dữ liệu kiểu tương ứng sẽ được tạo trong bao đóng.
Hơn nữa, bạn có thể khai báo một biến / đối tượng trong đối số hàm lambda, đối số này sẽ trở thành đối
số để gọi toán tử, tức là operator ().
Lợi ích của việc sử dụng một hàm Lambda
Chi phí trừu tượng bằng không. Đúng! Bạn đọc nó đúng. Lambda không tốn hiệu suất của bạn và nó
nhanh như một chức năng bình thường. lOMoAR cPSD| 27879799
Ngoài ra, mã trở nên nhỏ gọn, có cấu trúc và biểu cảm. Học biểu thức Lambda
Nắm bắt theo tham chiếu / giá trị int main() { int x = 100, y = 200;
auto print = [&] { // Capturing object by reference
std::cout << __PRETTY_FUNCTION__ << " : " << x << " , " << y << std::endl; }; print(); return 0; } Đầu ra: main () :: : 100, 200
Trong ví dụ trên, tôi đã đề cập & trong danh sách chụp. Điều này nắm bắt biến x và y làm tham chiếu.
Tương tự, = biểu thị giá trị được nắm bắt, điều này sẽ tạo thành viên dữ liệu cùng loại trong quá trình
đóng và việc gán sao chép sẽ diễn ra.
Lưu ý rằng danh sách tham số là tùy chọn; bạn có thể bỏ qua các dấu ngoặc trống nếu bạn không chuyển
các đối số vào biểu thức lambda. Lambda Capture List
Bảng sau đây cho thấy các trường hợp sử dụng khác nhau cho cùng một:
[] () {} không có ảnh chụp
[=] () {} chụp mọi thứ bằng bản sao (không được đề xuất)
[&] () {} ghi lại mọi thứ bằng cách tham khảo (không được đề xuất)
[x] () {} chụp x bằng bản sao
[& x] () {} chụp x bằng cách tham chiếu lOMoAR cPSD| 27879799
[&, x] () {} chụp x bằng bản sao, mọi thứ khác bằng cách tham chiếu
[=, & x] () {} chụp x theo tham chiếu, mọi thứ khác bằng bản sao
Chuyển Lambda làm tham số template void f(Functor functor) {
std::cout << __PRETTY_FUNCTION__ << std::endl; }
/* Or alternatively you can use this void f(std::function functor) {
std::cout << __PRETTY_FUNCTION__ << std::endl; } */
int g() { static int i = 0; return i++; } int main() {
auto lambda_func = [i = 0]() mutable { return i++; };
f(lambda_func); // Pass lambda f(g); // Pass function } Đầu ra:
Function Type : void f(Functor) [with Functor = main()::]
Function Type : void f(Functor) [with Functor = int (*)(int)]
Bạn cũng có thể chuyển các hàm lambda làm đối số cho các hàm khác giống như một hàm bình thường
mà tôi đã viết mã ở trên.
Nếu bạn để ý, ở đây tôi đã khai báo một biến i trong danh sách chụp, biến này sẽ trở thành thành viên
dữ liệu. Kết quả là, mỗi khi bạn gọi lambda_func, nó sẽ được trả về và tăng dần. Nắm bắt biến thành
viên trong Lambda hoặc Con trỏ này class Example { public: Example() : m_var(10) {} void func() {
[=]() { std::cout << m_var << std::endl; }(); // IIFE } private: int m_var; }; int main() { Example e; e.func(); lOMoAR cPSD| 27879799 }
con trỏ này cũng có thể được ghi lại bằng cách sử dụng [this], [=] hoặc [&]. Trong bất kỳ trường hợp nào
trong số này, các thành viên dữ liệu lớp (bao gồm cả private) có thể được truy cập như bạn làm trong
một phương thức thông thường.
Nếu bạn nhìn thấy dòng biểu thức lambda, tôi đã sử dụng extra () ở cuối khai báo hàm lambda được sử
dụng để gọi nó ngay sau khi khai báo. Nó được gọi là IIFE (Biểu thức hàm được gọi ngay lập tức).
Các kiểu hàm Lambda trong C ++ Lambda chung
const auto l = [](auto a, auto b, auto c) {}; // is equivalent to struct anonymous { template
auto operator()(T0 a, T1 b, T2 c) const { } };
Lambda chung được giới thiệu trong C ++ 14 nắm bắt các tham số với trình xác định tự động. Variadic Generic Lambda lOMoAR cPSD| 27879799 void print() {} template
void print(const First &first, Rest &&... args) {
std::cout << first << std::endl; print(args...); } int main() {
auto variadic_generic_lambda = [](auto... param) { print(param...); };
variadic_generic_lambda(1, "lol", 1.1); }
Lambda với một gói tham số thay đổi sẽ hữu ích trong nhiều trường hợp như gỡ lỗi, hoạt động lặp lại với
đầu vào dữ liệu khác nhau, v.v.
Hàm Lambda có thể thay đổi
Thông thường, toán tử gọi hàm của lambda là const-by-value, có nghĩa là lambda yêu cầu từ khóa có thể
thay đổi nếu bạn đang nắm bắt bất kỳ thứ gì theo giá trị. []() mutable {} // is equivalent to struct anonymous {
auto operator()() // call operator { } };
Chúng tôi đã thấy một ví dụ về điều này ở trên. Tôi hy vọng bạn nhận thấy nó.
Lambda dưới dạng một con trỏ hàm #include #include int main() { auto funcPtr = +[] {};
static_assert(std::is_same::value); }
Bạn có thể buộc trình biên dịch tạo lambda dưới dạng một con trỏ hàm thay vì đóng bằng cách thêm +
vào trước nó, như được hiển thị ở trên.
Các chức năng Lambda trả về có thứ tự cao hơn lOMoAR cPSD| 27879799
const auto less_than = [](auto x) { return [x](auto y) { return y < x; }; }; int main(void) {
auto less_than_five = less_than(5);
std::cout << less_than_five(3) << std::endl;
std::cout << less_than_five(10) << std::endl; return 0; }
Đi xa hơn một chút, các hàm lambda cũng có thể trả về một hàm lambda khác. Điều này sẽ mở ra cánh
cửa của khả năng tùy biến vô tận, khả năng diễn đạt mã và khả năng tương thích (nhân 琀椀 ện, không
có từ nào như thế này) của mã. Constexpr Lambda Expression
Kể từ C ++ 17, một biểu thức lambda có thể được khai báo là constexpr.
constexpr auto sum = [](const auto &a, const auto &b) { return a + b; }; /*
is equivalent to constexpr struct anonymous { template
constexpr auto operator()(T1 a, T2 b) const { return a + b; } };
*/ constexpr int answer = sum(10, 10);
Ngay cả khi bạn không chỉ định constexpr, toán tử gọi hàm vẫn sẽ là constexpr, nếu điều đó xảy ra để đáp
ứng tất cả các yêu cầu của hàm constexpr. Lời kết
Tôi hy vọng bạn thích bài viết này. Tôi đã cố gắng trình bày hầu hết những điều phức tạp xung quanh
lambda bằng một vài ví dụ đơn giản và nhỏ. Bạn nên sử dụng lambda ở bất cứ nơi nào nó xuất hiện
trong tâm trí bạn xem xét 琀 nh biểu cảm của mã và khả năng bảo trì dễ dàng giống như bạn có thể sử
dụng nó trong trình xóa tùy chỉnh cho con trỏ thông minh và với hầu hết các thuật toán STL. lOMoAR cPSD| 27879799
Giới thiệu về lambdas (hàm ẩn danh) #include #include #include #include
static bool containsNut(std::string_view str) // static means internal linkage in this context {
// std::string_view::昀椀 nd returns std::string_view::npos, which is a very large number, // if it doesn't 昀椀 nd the substring.
// Otherwise it returns the index where the substring occurs in str.
return (str.昀椀 nd("nut") != std::string_view::npos); } int main() {
constexpr std::array arr{ "apple", "banana", "walnut", "lemon" };
// std::昀椀 nd_if takes a pointer to a function
const auto found{ std::昀椀 nd_if(arr.begin(), arr.end(), containsNut) }; if (found == arr.end()) {
std::cout << "No nuts\n"; } else {
std::cout << "Found " << *found << '\n'; } return 0; }
Đoạn mã này 琀 m kiếm qua một mảng các chuỗi để 琀 m phần tử đầu 琀椀 ên có chứa chuỗi con
“nut”. Do đó, nó tạo ra kết quả: Found walnut
Và trong khi nó hoạt động, nó có thể được cải thiện.
Gốc của vấn đề ở đây là std :: 昀椀 nd_if yêu cầu chúng ta chuyển nó một con trỏ hàm. Do đó, chúng tôi
buộc phải xác định một hàm chỉ được sử dụng một lần, phải được đặt tên và phải được đặt trong phạm
vi toàn cục (vì các hàm không thể lồng vào nhau!). Hàm cũng rất ngắn, hầu như dễ dàng phân biệt nó làm
gì từ một dòng mã hơn là từ tên và nhận xét. lOMoAR cPSD| 27879799 Lambdas đến giải cứu
Một biểu thức lambda (còn được gọi là lambda hoặc bao đóng) cho phép chúng ta xác định một hàm ẩn
danh bên trong một hàm khác. Việc lồng vào nhau rất quan trọng, vì nó cho phép chúng ta vừa tránh
được ô nhiễm đặt tên không gian tên, vừa xác định hàm càng gần nơi nó được sử dụng càng tốt (cung cấp thêm ngữ cảnh).
Cú pháp cho lambdas là một trong những thứ kỳ lạ trong C ++ và cần một chút làm quen. Lambdas có dạng:
[ captureClause ] ( parameters ) -> returnType { statements; }
Cả mệnh đề nắm bắt và tham số đều có thể để trống nếu chúng không cần thiết.
Kiểu trả về là tùy chọn và nếu bị bỏ qua, tự động sẽ được giả định (do đó sử dụng suy luận kiểu được sử
dụng để xác định kiểu trả về). Mặc dù trước đây chúng tôi đã lưu ý rằng nên tránh suy luận kiểu cho các
kiểu trả về hàm, nhưng trong ngữ cảnh này, bạn có thể sử dụng (vì các hàm này thường rất nhỏ).
Cũng xin lưu ý rằng lambdas không có tên, vì vậy chúng tôi không cần cung cấp tên.
Điều này có nghĩa là một định nghĩa lambda tầm thường trông như thế này: #include int main() {
[]() {}; // de 昀椀 nes a lambda with no captures, no parameters, and no return type return 0; }
Hãy viết lại ví dụ trên bằng lambda: lOMoAR cPSD| 27879799 #include #include #include #include int main() {
constexpr std::array arr{ "apple", "banana", "walnut", "lemon" };
// De 昀椀 ne the function right where we use it.
const auto found{ std::昀椀 nd_if(arr.begin(), arr.end(),
[](std::string_view str) // here's our lambda, no capture clause {
return (str.昀椀 nd("nut") !=
std::string_view::npos); }) }; if (found == arr.end()) {
std::cout << "No nuts\n"; } else {
std::cout << "Found " << *found << '\n'; } return 0; }
Điều này hoạt động giống như trường hợp con trỏ hàm và tạo ra một kết quả giống hệt nhau: Found walnut
Lưu ý rằng lambda của chúng ta tương tự như thế nào với hàm containsNut của chúng ta. Cả hai đều có
các tham số và cơ quan chức năng giống hệt nhau. Lambda không có mệnh đề nắm bắt (chúng tôi sẽ giải
thích mệnh đề nắm bắt là gì trong bài học 琀椀 ếp theo) vì nó không cần mệnh đề này. Và chúng tôi đã
bỏ qua kiểu trả về theo sau trong lambda (cho ngắn gọn), nhưng vì toán tử! = Trả về bool, lambda của
chúng tôi cũng sẽ trả về bool. Loại lambda
Trong ví dụ trên, chúng tôi đã xác định một lambda ngay tại nơi nó cần thiết. Việc sử dụng lambda này
đôi khi được gọi là một hàm theo nghĩa đen. lOMoAR cPSD| 27879799
Tuy nhiên, việc viết lambda cùng dòng với dòng được sử dụng đôi khi có thể khiến mã khó đọc hơn.
Giống như chúng ta có thể khởi tạo một biến với giá trị chữ (hoặc một con trỏ hàm) để sử dụng sau này,
chúng ta cũng có thể khởi tạo một biến lambda với định nghĩa lambda và sau đó sử dụng nó sau. Một
lambda được đặt tên cùng với một tên hàm tốt có thể làm cho mã dễ đọc hơn.
Ví dụ: trong đoạn mã sau, chúng tôi đang sử dụng std :: all_of để kiểm tra xem tất cả các phần tử của
một mảng có chẵn hay không:
// Bad: We have to read the lambda to understand what's happening.
return std::all_of(array.begin(), array.end(), [](int i){ return ((i % 2) == 0); }); // Xấu: Chúng ta
phải đọc lambda để hiểu chuyện gì đang xảy ra. return std :: all_of (array.begin
(), array.end (), [] (int i) {return ((i% 2) == 0);});
Chúng tôi có thể cải thiện khả năng đọc của điều này như sau:
// Good: Instead, we can store the lambda in a named variable and pass it to the function. auto isEven{ [](int i) { return ((i % 2) == 0); } };
return std::all_of(array.begin(), array.end(), isEven);
Lưu ý dòng cuối cùng đọc tốt như thế nào: "trả về liệu tất cả các phần tử trong mảng có chẵn hay không"
Nhưng loại lambda isEven là gì?
Hóa ra, lambdas không có loại mà chúng ta có thể sử dụng một cách rõ ràng. Khi chúng ta viết lambda,
trình biên dịch sẽ tạo ra một kiểu duy nhất chỉ dành cho lambda không hiển thị với chúng ta.
Dành cho người đọc nâng cao
Trên thực tế, lambdas không phải là hàm (đó là một phần của cách chúng tránh giới hạn của việc C ++
không hỗ trợ các hàm lồng nhau). Chúng là một loại vật thể đặc biệt được gọi là cái thú. Functors là các
đối tượng có chứa toán tử được nạp chồng () khiến chúng có thể gọi được giống như một hàm.
Mặc dù chúng tôi không biết loại lambda, nhưng có một số cách lưu trữ lambda để sử dụng sau định
nghĩa. Nếu lambda có mệnh đề bắt trống, chúng ta có thể sử dụng một con trỏ hàm thông thường. Trong
bài học 琀椀 ếp theo, chúng tôi giới thiệu về chụp lambda, con trỏ hàm sẽ không hoạt động nữa vào thời lOMoAR cPSD| 27879799
điểm đó. Tuy nhiên, hàm std :: có thể được sử dụng cho lambdas ngay cả khi chúng đang chụp một thứ gì đó. #include int main() {
// A regular function pointer. Only works with an empty capture clause.
double (*addNumbers1)(double, double){ [](double a, double b) { return (a + b); } }; addNumbers1(1, 2);
// Using std::function. The lambda could have a non-empty capture clause (Next lesson).
std::function addNumbers2{ // note: pre-C++17, use std::function instead [](double a, double b) { return (a + b); } }; addNumbers2(3, 4);
// Using auto. Stores the lambda with its real type. auto addNumbers3{ [](double a, double b) { return (a + b); } }; addNumbers3(5, 6); return 0; }
Cách duy nhất để sử dụng kiểu thực tế của lambda là tự động. auto cũng có lợi ích là không có chi phí so với hàm std ::. lOMoAR cPSD| 27879799
Rất 琀椀 ếc, không phải lúc nào chúng ta cũng có thể sử dụng auto. Trong trường hợp lambda thực tế
không xác định (ví dụ: vì chúng tôi đang truyền lambda cho một hàm dưới dạng tham số và người gọi xác
định lambda sẽ được truyền vào), chúng tôi không thể sử dụng auto. Trong những trường hợp như vậy, nên sử dụng hàm std ::. #include #include
// We don't know what fn will be. std::function works with regular functions and lambdas.
void repeat(int repetitions, const std::function& fn) {
for (int i{ 0 }; i < repetitions; ++i) { fn(i); } } int main() { repeat(3, [](int i) {
std::cout << i << '\n'; }); return 0; } Output 0 1 2 Qui định
Sử dụng tự động khi khởi tạo biến bằng lambdas và std :: func 琀椀 on nếu bạn không thể khởi tạo biến bằng lambda. Lambdas chung
Đối với hầu hết các phần, các tham số lambda hoạt động theo các quy tắc giống như các tham số hàm thông thường. lOMoAR cPSD| 27879799
Một ngoại lệ đáng chú ý là vì C ++ 14, chúng tôi được phép sử dụng tự động cho các tham số (lưu ý:
trong C ++ 20, các hàm thông thường cũng có thể sử dụng tự động cho các tham số). Khi một lambda có
một hoặc nhiều tham số tự động, trình biên dịch sẽ suy ra loại tham số nào là cần thiết từ các lệnh gọi đến lambda.
Vì lambdas có một hoặc nhiều tham số tự động có thể hoạt động với nhiều loại khác nhau, chúng được gọi là lambdas chung.
Dành cho người đọc nâng cao
Khi được sử dụng trong ngữ cảnh của lambda, auto chỉ là cách viết tắt của một tham số mẫu.
Chúng ta hãy xem xét một lambda chung: lOMoAR cPSD| 27879799 #include #include #include #include int main() {
constexpr std::array months{ // pre-C++17 use std::array "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" };
// Search for two consecutive months that start with the same letter.
const auto sameLetter{ std::adjacent_昀椀 nd(months.begin(), months.end(),
[](const auto& a, const auto& b) { return (a[0] == b[0]); }) };
// Make sure that two months were found.
if (sameLetter != months.end()) {
// std::next returns the next iterator after sameLetter
std::cout << *sameLetter << " and " << *std::next(sameLetter)
<< " start with the same letter\n"; } return 0; } Output:
June and July start with the same letter
Trong vd trên,rất nhiều, chúng tôi sử dụng các tham số tự động để nắm bắt các chuỗi của chúng tôi bằng
tham chiếu const. Bởi vì tất cả các loại chuỗi đều cho phép truy cập vào các ký tự riêng lẻ của chúng
thông qua toán tử [], chúng tôi không cần quan tâm liệu người dùng có đang chuyển vào chuỗi std :: lOMoAR cPSD| 27879799
string, C-style hay thứ gì khác hay không. Điều này cho phép chúng tôi viết lambda có thể chấp nhận bất
kỳ điều nào trong số này, có nghĩa là nếu chúng tôi thay đổi loại vài tháng sau, chúng tôi sẽ không phải viết lại lambda.
Tuy nhiên, tự động không phải lúc nào cũng là lựa chọn tốt nhất. Xem xét: #include #include #include #include int main() {
constexpr std::array months{ // pre-C++17 use std::array "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" };
// Count how many months consist of 5 letters
const auto 昀椀 veLetterMonths{ std::count_if(months.begin(), months.end(), [](std::string_view str) { return (str.length() == 5); }) };
std::cout << "There are " << 昀椀 veLetterMonths << " months with 5 letters\n"; return 0; } Output:
There are 2 months with 5 letters
Trong ví dụ này, sử dụng auto sẽ suy ra một loại const char *. Chuỗi kiểu C không dễ làm việc (ngoài việc
sử dụng toán tử []). Trong trường hợp này, chúng tôi muốn xác định rõ ràng tham số dưới dạng std ::
string_view, điều này cho phép chúng tôi làm việc với dữ liệu cơ bản dễ dàng hơn nhiều (ví dụ: chúng tôi
có thể hỏi chế độ xem chuỗi về độ dài của nó, ngay cả khi người dùng đã chuyển vào C - kiểu mảng). lOMoAR cPSD| 27879799
Lambdas chung và biến 琁⤀nh
Một điều cần lưu ý là một lambda duy nhất sẽ được tạo cho mỗi loại khác nhau mà tự động phân giải. Ví
dụ sau cho thấy cách một lambda chung biến thành hai lambda riêng biệt: #include #include #include #include int main() {
// Print a value and count how many times @print has been called. auto print{ [](auto value) { static int callCount{ 0 };
std::cout << callCount++ << ": " << value << '\n'; } }; print("hello"); // 0: hello print("world"); // 1: world print(1); // 0: 1 print(2); // 1: 2
print("ding dong"); // 2: ding dong return 0; } Output 0: hello 1: world 0: 1 1: 2 2: ding dong
Trong ví dụ trên, chúng ta định nghĩa lambda và sau đó gọi nó với hai tham số khác nhau (tham số dạng
chuỗi ký tự và tham số số nguyên). Điều này tạo ra hai phiên bản khác nhau của lambda (một phiên bản
có tham số chuỗi ký tự và một phiên bản có tham số số nguyên). lOMoAR cPSD| 27879799
Hầu hết thời gian, điều này là không quan trọng. Tuy nhiên, lưu ý rằng nếu lambda chung sử dụng các
biến thời lượng 琁⤀nh, các biến đó sẽ không được chia sẻ giữa các lambda đã tạo.
Chúng ta có thể thấy điều này trong ví dụ trên, trong đó mỗi kiểu (chuỗi ký tự và số nguyên) có số lượng
duy nhất của riêng nó! Mặc dù chúng tôi chỉ viết lambda một lần, hai lambda đã được tạo - và mỗi
lambda đều có phiên bản callCount riêng. Để có một bộ đếm được chia sẻ giữa hai lambda đã tạo, chúng
ta phải xác định một biến toàn cục hoặc một biến cục bộ 琁⤀nh bên ngoài lambda. Như bạn đã biết từ
các bài học trước, cả biến cục bộ toàn cục và 琁⤀nh đều có thể gây ra sự cố và làm cho việc hiểu mã trở
nên khó khăn hơn. Chúng ta sẽ có thể tránh những biến đó sau khi nói về lambda capture trong bài học 琀椀 ếp theo.
Loại trả lại khấu trừ và các loại lợi nhuận theo sau
Nếu sử dụng kiểu khấu trừ trả về, kiểu trả về của lambda sẽ được suy ra từ các câu lệnh trả về bên trong
lambda. Nếu suy luận kiểu trả về được sử dụng, tất cả các câu lệnh trả về trong lambda phải trả về cùng
một kiểu (nếu không trình biên dịch sẽ không biết nên chọn kiểu nào). Ví dụ: #include int main() {
auto divide{ [](int x, int y, bool bInteger) { // note: no speci 昀椀 ed return type if (bInteger) return x / y; else
return static_cast(x) / y; // ERROR: return type doesn't match previous return type } };
std::cout << divide(3, 2, true) << '\n';
std::cout << divide(3, 2, false) << '\n'; return 0; }
Điều này tạo ra lỗi biên dịch vì kiểu trả về của câu lệnh trả về đầu 琀椀 ên (int) không khớp với kiểu trả
về của câu lệnh trả về thứ hai (double). lOMoAR cPSD| 27879799
Trong trường hợp chúng tôi trả lại các loại khác nhau, chúng tôi có hai tùy chọn:
1) Thực hiện chuyển đổi rõ ràng để làm cho tất cả các loại trả lại khớp nhau, hoặc
2) chỉ định rõ ràng kiểu trả về cho lambda và để trình biên dịch thực hiện các chuyển đổi ngầm định.
Trường hợp thứ hai thường là lựa chọn tốt hơn: #include int main() {
// note: explicitly specifying this returns a double
auto divide{ [](int x, int y, bool bInteger) -> double { if (bInteger)
return x / y; // will do an implicit conversion to double else return static_cast(x) / y; } };
std::cout << divide(3, 2, true) << '\n';
std::cout << divide(3, 2, false) << '\n'; return 0; }
Theo cách đó, nếu bạn quyết định thay đổi kiểu trả về, bạn (thường) chỉ cần thay đổi kiểu trả về của
lambda và không chạm vào nội dung lambda.
Các đối tượng chức năng thư viện 琀椀 êu chuẩn
Đối với các hoạt động phổ biến (ví dụ: bổ sung, phủ định hoặc so sánh), bạn không cần phải viết lambdas
của riêng mình, vì thư viện 琀椀 êu chuẩn đi kèm với nhiều đối tượng cơ bản có thể gọi có thể được sử
dụng thay thế. Chúng được định nghĩa trong 琀椀 êu đề .
Standard library function objects
For common operations (e.g. addition, negation, or comparison) you don’t need to write
your own lambdas, because the standard library comes with many basic callable objects
that can be used instead. These are defined in the header. lOMoAR cPSD| 27879799 In the following example: 1 #include 2 #include 3 #include 4 5 bool greater(int a, int b) 6 {
7 // Order @a before @b if @a is greater than @b. 8 return (a > b); 9 } 1 0 int main() 1 {
1 std::array arr{ 13, 90, 99, 5, 40, 80 }; 1
2 // Pass greater to std::sort
1 std::sort(arr.begin(), arr.end(), greater); 3 1 for (int i : arr) 4 {
1 std::cout << i << ' '; 5 } 1 6 std::cout << '\n'; 1 7 return 0; 1 } 8 1 9 2 0 2 1 2 2 2 3 2