Biểu thức lambda trong C ++ 11 là gì?


1487

Biểu thức lambda trong C ++ 11 là gì? Khi nào tôi sẽ sử dụng một? Loại vấn đề nào họ giải quyết mà không thể có trước khi giới thiệu?

Một vài ví dụ và trường hợp sử dụng sẽ hữu ích.


14
Tôi đã thấy một trường hợp mà lambda rất hữu ích: Một đồng nghiệp của tôi đang thực hiện mã có hàng triệu lần lặp để giải quyết vấn đề tối ưu hóa không gian. Thuật toán nhanh hơn nhiều khi sử dụng lambda so với một hàm thích hợp! Trình biên dịch là Visual C ++ 2013.
sergiol

Câu trả lời:


1490

Vấn đề

C ++ bao gồm các hàm chung hữu ích như std::for_eachstd::transform, có thể rất tiện dụng. Thật không may, chúng cũng có thể khá cồng kềnh khi sử dụng, đặc biệt nếu functor mà bạn muốn áp dụng là duy nhất cho chức năng cụ thể.

#include <algorithm>
#include <vector>

namespace {
  struct f {
    void operator()(int) {
      // do something
    }
  };
}

void func(std::vector<int>& v) {
  f f;
  std::for_each(v.begin(), v.end(), f);
}

Nếu bạn chỉ sử dụng fmột lần và ở nơi cụ thể đó, có vẻ như quá mức cần thiết để viết cả một lớp chỉ để làm một cái gì đó tầm thường và một cái.

Trong C ++ 03, bạn có thể muốn viết một cái gì đó như sau, để giữ functor cục bộ:

void func2(std::vector<int>& v) {
  struct {
    void operator()(int) {
       // do something
    }
  } f;
  std::for_each(v.begin(), v.end(), f);
}

tuy nhiên điều này không được phép, fkhông thể truyền cho hàm mẫu trong C ++ 03.

Giải pháp mới

C ++ 11 giới thiệu lambdas cho phép bạn viết một functor nội tuyến, ẩn danh để thay thế struct f. Đối với các ví dụ đơn giản nhỏ, điều này có thể dễ đọc hơn (nó giữ mọi thứ ở một nơi) và có khả năng duy trì đơn giản hơn, ví dụ ở dạng đơn giản nhất:

void func3(std::vector<int>& v) {
  std::for_each(v.begin(), v.end(), [](int) { /* do something here*/ });
}

Các hàm Lambda chỉ là đường cú pháp cho hàm functor ẩn danh.

Trả về các loại

Trong các trường hợp đơn giản, kiểu trả về của lambda được suy ra cho bạn, ví dụ:

void func4(std::vector<double>& v) {
  std::transform(v.begin(), v.end(), v.begin(),
                 [](double d) { return d < 0.00001 ? 0 : d; }
                 );
}

tuy nhiên khi bạn bắt đầu viết lambdas phức tạp hơn, bạn sẽ nhanh chóng gặp phải trường hợp loại trả về không thể được suy ra bởi trình biên dịch, ví dụ:

void func4(std::vector<double>& v) {
    std::transform(v.begin(), v.end(), v.begin(),
        [](double d) {
            if (d < 0.0001) {
                return 0;
            } else {
                return d;
            }
        });
}

Để giải quyết vấn đề này, bạn được phép chỉ định rõ ràng loại trả về cho hàm lambda, sử dụng -> T:

void func4(std::vector<double>& v) {
    std::transform(v.begin(), v.end(), v.begin(),
        [](double d) -> double {
            if (d < 0.0001) {
                return 0;
            } else {
                return d;
            }
        });
}

Các biến "Chụp"

Cho đến nay chúng ta không sử dụng bất cứ thứ gì ngoài những gì được truyền cho lambda trong đó, nhưng chúng ta cũng có thể sử dụng các biến khác, trong lambda. Nếu bạn muốn truy cập các biến khác, bạn có thể sử dụng mệnh đề chụp ( []biểu thức), cho đến nay vẫn chưa được sử dụng trong các ví dụ này, ví dụ:

void func5(std::vector<double>& v, const double& epsilon) {
    std::transform(v.begin(), v.end(), v.begin(),
        [epsilon](double d) -> double {
            if (d < epsilon) {
                return 0;
            } else {
                return d;
            }
        });
}

Bạn có thể chụp theo cả tham chiếu và giá trị, mà bạn có thể chỉ định bằng cách sử dụng &=tương ứng:

  • [&epsilon] chụp bằng cách tham khảo
  • [&] nắm bắt tất cả các biến được sử dụng trong lambda bằng cách tham chiếu
  • [=] nắm bắt tất cả các biến được sử dụng trong lambda theo giá trị
  • [&, epsilon] nắm bắt các biến như với [&], nhưng epsilon theo giá trị
  • [=, &epsilon] nắm bắt các biến như với [=], nhưng epsilon bằng cách tham chiếu

Được tạo ra operator()consttheo mặc định, với ngụ ý rằng ảnh chụp sẽ được constkhi bạn truy cập chúng bằng cách mặc định. Điều này có tác dụng là mỗi cuộc gọi có cùng một đầu vào sẽ tạo ra cùng một kết quả, tuy nhiên bạn có thể đánh dấu lambda làmutable để yêu cầu cuộc gọi operator()được tạo ra không const.


9
@Yakk bạn đã bị mắc bẫy. lambdas mà không chụp có một chuyển đổi ngầm định thành con trỏ kiểu chức năng. chức năng chuyển đổi constluôn luôn là ...
Johannes Schaub - litb

2
@ JohannesSchaub-litb oh lén lút - và nó xảy ra khi bạn gọi ()- nó được thông qua dưới dạng lambda đối số 0, nhưng vì () constkhông khớp với lambda, nên nó tìm kiếm một chuyển đổi loại cho phép nó, bao gồm cả diễn viên ngầm -to-function-con trỏ, và sau đó gọi đó! Lén lút!
Yakk - Adam Nevraumont

2
Thú vị - Ban đầu tôi nghĩ rằng lambdas là các chức năng ẩn danh chứ không phải functor, và bị nhầm lẫn về cách thức hoạt động của việc bắt giữ.
dùng253751

49
Nếu bạn muốn sử dụng lambdas làm biến trong chương trình của mình, bạn có thể sử dụng: std::function<double(int, bool)> f = [](int a, bool b) -> double { ... }; Nhưng thông thường, chúng tôi để trình biên dịch suy ra loại: auto f = [](int a, bool b) -> double { ... }; (và đừng quên #include <functional>)
Evert Heylen

11
Tôi cho rằng không phải ai cũng hiểu tại sao return d < 0.00001 ? 0 : d;được đảm bảo trả về gấp đôi, khi một trong các toán hạng là hằng số nguyên (đó là do quy tắc quảng cáo ngầm của toán tử ?: Trong đó toán hạng 2 và 3 được cân bằng với nhau thông qua số học thông thường chuyển đổi bất kể cái nào được chọn). Thay đổi thành 0.0 : dcó lẽ sẽ làm cho ví dụ dễ hiểu hơn.
Lundin

830

Hàm lambda là gì?

Khái niệm C ++ của hàm lambda bắt nguồn từ phép tính lambda và lập trình hàm. Lambda là một hàm không tên, rất hữu ích (trong lập trình thực tế, không phải lý thuyết) cho các đoạn mã ngắn không thể sử dụng lại và không đáng để đặt tên.

Trong C ++, hàm lambda được định nghĩa như thế này

[]() { } // barebone lambda

hoặc trong tất cả vinh quang của nó

[]() mutable -> T { } // T is the return type, still lacking throw()

[]là danh sách chụp, ()danh sách đối số và {}thân hàm.

Danh sách chụp

Danh sách chụp xác định những gì từ bên ngoài lambda nên có sẵn bên trong thân hàm và làm thế nào. Nó có thể là:

  1. một giá trị: [x]
  2. một tài liệu tham khảo [& x]
  3. bất kỳ biến nào trong phạm vi theo tham chiếu [&]
  4. giống như 3, nhưng theo giá trị [=]

Bạn có thể trộn bất kỳ mục nào ở trên trong danh sách được phân tách bằng dấu phẩy [x, &y].

Danh sách đối số

Danh sách đối số giống như trong bất kỳ hàm C ++ nào khác.

Cơ thể chức năng

Mã sẽ được thực thi khi lambda thực sự được gọi.

Loại trừ

Nếu lambda chỉ có một câu lệnh return, kiểu trả về có thể được bỏ qua và có kiểu ẩn decltype(return_statement).

Có thể thay đổi

Nếu lambda được đánh dấu là có thể thay đổi (ví dụ []() mutable { }), nó được phép thay đổi các giá trị đã bị bắt bởi giá trị.

Trường hợp sử dụng

Thư viện được xác định theo tiêu chuẩn ISO được hưởng lợi rất nhiều từ lambdas và tăng khả năng sử dụng một số thanh vì giờ đây người dùng không phải làm lộn xộn mã của họ với các hàm xử lý nhỏ trong một phạm vi có thể truy cập được.

C ++ 14

Trong C ++ 14 lambdas đã được mở rộng bởi các đề xuất khác nhau.

Khởi tạo Lambda

Một phần tử của danh sách chụp bây giờ có thể được khởi tạo với =. Điều này cho phép đổi tên các biến và để nắm bắt bằng cách di chuyển. Một ví dụ được lấy từ tiêu chuẩn:

int x = 4;
auto y = [&r = x, x = x+1]()->int {
            r += 2;
            return x+2;
         }();  // Updates ::x to 6, and initializes y to 7.

và một bức được lấy từ Wikipedia chỉ ra cách chụp với std::move:

auto ptr = std::make_unique<int>(10); // See below for std::make_unique
auto lambda = [ptr = std::move(ptr)] {return *ptr;};

Lambdas chung

Bây giờ Lambdas có thể là chung ( autosẽ tương đương với Tở đây nếu Tlà một đối số mẫu kiểu ở đâu đó trong phạm vi xung quanh):

auto lambda = [](auto x, auto y) {return x + y;};

Khấu trừ loại hoàn trả

C ++ 14 cho phép các kiểu trả về được suy ra cho mọi hàm và không giới hạn nó ở các hàm của biểu mẫu return expression;. Điều này cũng được mở rộng đến lambdas.


2
Trong ví dụ của bạn cho các lambda khởi tạo ở trên, tại sao bạn kết thúc hàm lamba bằng ();? Điều này xuất hiện như [] () {} (); thay vì [](){};. Cũng không nên giá trị của x là 5?
Ramakrishnan Kannan

6
@RamakrishnanKannan: 1) () có mặt để gọi lambda ngay sau khi xác định nó và đưa ra giá trị trả về của nó. Biến y là một số nguyên, không phải là lambda. 2) Không, x = 5 là cục bộ của lambda (một giá trị bắt giữ có cùng tên với biến phạm vi bên ngoài x), và sau đó x + 2 = 5 + 2 được trả về. Việc gán lại biến ngoài x xảy ra thông qua tham chiếu r : r = &x; r += 2;, nhưng điều này xảy ra với giá trị ban đầu là 4.
Vee

167

Các biểu thức Lambda thường được sử dụng để đóng gói các thuật toán để chúng có thể được chuyển sang một hàm khác. Tuy nhiên, có thể thực hiện lambda ngay lập tức theo định nghĩa :

[&](){ ...your code... }(); // immediately executed lambda expression

có chức năng tương đương với

{ ...your code... } // simple code block

Điều này làm cho biểu thức lambda trở thành một công cụ mạnh mẽ để tái cấu trúc các hàm phức tạp . Bạn bắt đầu bằng cách gói một phần mã trong hàm lambda như được hiển thị ở trên. Quá trình tham số hóa rõ ràng sau đó có thể được thực hiện dần dần với thử nghiệm trung gian sau mỗi bước. Khi bạn có khối mã được tham số hóa đầy đủ (như được thể hiện bằng cách loại bỏ &), bạn có thể di chuyển mã đến một vị trí bên ngoài và biến nó thành một chức năng bình thường.

Tương tự, bạn có thể sử dụng biểu thức lambda để khởi tạo các biến dựa trên kết quả của thuật toán ...

int a = []( int b ){ int r=1; while (b>0) r*=b--; return r; }(5); // 5!

một cách phân vùng logic chương trình của bạn , bạn thậm chí có thể thấy hữu ích khi truyền biểu thức lambda làm đối số cho biểu thức lambda khác ...

[&]( std::function<void()> algorithm ) // wrapper section
   {
   ...your wrapper code...
   algorithm();
   ...your wrapper code...
   }
([&]() // algorithm section
   {
   ...your algorithm code...
   });

Biểu thức Lambda cũng cho phép bạn tạo các hàm lồng nhau có tên , đây có thể là một cách thuận tiện để tránh logic trùng lặp. Sử dụng lambdas có tên cũng có xu hướng dễ dàng hơn một chút (so với lambdas nội tuyến ẩn danh) khi chuyển một hàm không tầm thường như một tham số cho một chức năng khác. Lưu ý: đừng quên dấu chấm phẩy sau khi đóng ngoặc nhọn.

auto algorithm = [&]( double x, double m, double b ) -> double
   {
   return m*x+b;
   };

int a=algorithm(1,2,3), b=algorithm(4,5,6);

Nếu hồ sơ tiếp theo cho thấy chi phí khởi tạo đáng kể cho đối tượng hàm, bạn có thể chọn viết lại phần này như một hàm bình thường.


11
Bạn có nhận ra rằng câu hỏi này đã được hỏi 1,5 năm trước và hoạt động cuối cùng đã gần 1 năm trước? Dù sao, bạn đang đóng góp một số ý tưởng thú vị mà tôi chưa từng thấy trước đây!
Piotr99

7
Cảm ơn về mẹo xác định và thực hiện đồng thời! Tôi nghĩ rằng đáng chú ý rằng nó hoạt động như một sự tranh luận cho các iftuyên bố : if ([i]{ for (char j : i) if (!isspace(j)) return false ; return true ; }()) // i is all whitespace, giả sử ilà mộtstd::string
Blacklight Shining

74
Vì vậy, sau đây là một biểu thức pháp lý : [](){}();.
tộc

8
Ừ! (lambda: None)()Cú pháp của Python dễ đọc hơn nhiều.
dan04

9
@nobar - Bạn nói đúng, tôi đã nhầm. Đây là hợp pháp (tôi đã thử nghiệm lần này)main() {{{{((([](){{}}())));}}}}
Mark Lakata

38

Đáp án

Q: Biểu thức lambda trong C ++ 11 là gì?

A: Dưới mui xe, nó là đối tượng của một lớp được tạo tự động với toán tử nạp chồng () const . Đối tượng như vậy được gọi là đóng và được tạo bởi trình biên dịch. Khái niệm 'đóng cửa' này gần với khái niệm liên kết từ C ++ 11. Nhưng lambdas thường tạo mã tốt hơn. Và các cuộc gọi thông qua việc đóng cửa cho phép nội tuyến đầy đủ.

Q: Khi nào tôi sẽ sử dụng nó?

Trả lời: Để xác định "logic đơn giản và nhỏ" và yêu cầu trình biên dịch thực hiện tạo từ câu hỏi trước. Bạn cung cấp cho trình biên dịch một số biểu thức mà bạn muốn ở bên trong toán tử (). Tất cả các trình biên dịch công cụ khác sẽ tạo ra cho bạn.

Q: Loại vấn đề nào họ giải quyết mà không thể có trước khi giới thiệu?

Trả lời: Đó là một số loại cú pháp như toán tử nạp chồng thay vì các hàm để thêm tùy chỉnh , thao tác subrtact ... Nhưng nó lưu nhiều dòng mã không cần thiết để bọc 1-3 dòng logic thực cho một số lớp, v.v.! Một số kỹ sư nghĩ rằng nếu số lượng dòng nhỏ hơn thì sẽ ít xảy ra lỗi hơn (tôi cũng nghĩ vậy)

Ví dụ về cách sử dụng

auto x = [=](int arg1){printf("%i", arg1); };
void(*f)(int) = x;
f(1);
x(1);

Extras về lambdas, không được bao phủ bởi câu hỏi. Bỏ qua phần này nếu bạn không quan tâm

1. Giá trị thu được. Những gì bạn có thể chụp

1.1. Bạn có thể tham chiếu đến một biến có thời lượng lưu trữ tĩnh trong lambdas. Tất cả đều bị bắt.

1.2. Bạn có thể sử dụng lambda để chụp các giá trị "theo giá trị". Trong trường hợp như vậy, các vars bị bắt sẽ được sao chép vào đối tượng hàm (bao đóng).

[captureVar1,captureVar2](int arg1){}

1.3. Bạn có thể chụp được tham khảo. & - trong ngữ cảnh này có nghĩa là tham chiếu, không phải con trỏ.

   [&captureVar1,&captureVar2](int arg1){}

1.4. Nó tồn tại ký hiệu để nắm bắt tất cả các bình không tĩnh theo giá trị hoặc theo tham chiếu

  [=](int arg1){} // capture all not-static vars by value

  [&](int arg1){} // capture all not-static vars by reference

1.5. Nó tồn tại ký hiệu để nắm bắt tất cả các bình không tĩnh theo giá trị, hoặc bằng cách tham chiếu và chỉ định smth. hơn. Ví dụ: Chụp tất cả các vars không tĩnh theo giá trị, nhưng bằng cách bắt tham chiếu Param2

[=,&Param2](int arg1){} 

Chụp tất cả các lọ không tĩnh bằng tham chiếu, nhưng bằng cách chụp giá trị Param2

[&,Param2](int arg1){} 

2. Khấu trừ kiểu trả về

2.1. Kiểu trả về Lambda có thể được suy ra nếu lambda là một biểu thức. Hoặc bạn có thể chỉ định rõ ràng nó.

[=](int arg1)->trailing_return_type{return trailing_return_type();}

Nếu lambda có nhiều hơn một biểu thức, thì kiểu trả về phải được chỉ định thông qua kiểu trả về theo sau. Ngoài ra, cú pháp tương tự có thể được áp dụng cho các chức năng tự động và chức năng thành viên

3. Giá trị thu được. Những gì bạn không thể nắm bắt

3.1. Bạn chỉ có thể chụp các vars cục bộ, không phải biến thành viên của đối tượng.

4. Biến đổi

4.1 !! Lambda không phải là một con trỏ hàm và nó không phải là một hàm ẩn danh, nhưng lambdas không có khả năng bắt giữ có thể được chuyển đổi hoàn toàn thành một con trỏ hàm.

ps

  1. Thông tin thêm về thông tin ngữ pháp lambda có thể được tìm thấy trong Bản thảo làm việc cho Ngôn ngữ lập trình C ++ # 337, 2012-01-16, 5.1.2. Biểu thức Lambda, tr.88

  2. Trong C ++ 14, tính năng bổ sung có tên là "init Capture" đã được thêm vào. Nó cho phép thực hiện tuyên bố một cách nghiêm túc các thành viên dữ liệu đóng cửa:

    auto toFloat = [](int value) { return float(value);};
    auto interpolate = [min = toFloat(0), max = toFloat(255)](int value)->float { return (value - min) / (max - min);};

Đây [&,=Param2](int arg1){}dường như không phải là cú pháp hợp lệ. Mẫu chính xác sẽ là[&,Param2](int arg1){}
GetFree

Cảm ơn. Đầu tiên tôi đã cố gắng biên dịch đoạn trích này. Và có vẻ như sự đồng hóa kỳ lạ trong các bộ điều biến cho phép trong danh sách chụp // g ++ -std = c ++ 11 main.cpp -o test_bin; ./test_bin #include <stdio.h> int main () {#if 1 {int param = 0; auto f = [=, & param] (int arg1) có thể thay đổi {param = arg1;}; f (111); printf ("% i \ n", param); } #endif #if 0 {int param = 0; auto f = [&, = param] (int arg1) có thể thay đổi {param = arg1;}; f (111); printf ("% i \ n", param); } #endif trả về 0; }
bruziuz

Có vẻ như dòng mới không được hỗ trợ trong bình luận. Sau đó, tôi đã mở 5.1.2 biểu thức Lambda, tr.88, "Bản nháp làm việc, Tiêu chuẩn cho ngôn ngữ lập trình C ++", Số Dcoument: # 337, 2012-01-16. Và nhìn vào cú pháp ngữ pháp. Và bạn đã đúng. Không tồn tại những thứ như chụp qua "= arg"
bruziuz

Cảm ơn rất nhiều, đã sửa nó trong phần mô tả và cũng có được kiến ​​thức mới.
bruziuz

16

Hàm lambda là một hàm ẩn danh mà bạn tạo nội tuyến. Nó có thể nắm bắt các biến như một số đã giải thích, (ví dụ: http://www.stroustrup.com/C++11FAQ.html#lambda ) nhưng có một số hạn chế. Ví dụ: nếu có giao diện gọi lại như thế này,

void apply(void (*f)(int)) {
    f(10);
    f(20);
    f(30);
}

bạn có thể viết một hàm tại chỗ để sử dụng nó giống như hàm được truyền vào bên dưới:

int col=0;
void output() {
    apply([](int data) {
        cout << data << ((++col % 10) ? ' ' : '\n');
    });
}

Nhưng bạn không thể làm điều này:

void output(int n) {
    int col=0;
    apply([&col,n](int data) {
        cout << data << ((++col % 10) ? ' ' : '\n');
    });
}

vì những hạn chế trong tiêu chuẩn C ++ 11. Nếu bạn muốn sử dụng ảnh chụp, bạn phải dựa vào thư viện và

#include <functional> 

(hoặc một số thư viện STL khác như thuật toán để lấy nó một cách gián tiếp) và sau đó làm việc với hàm std :: thay vì truyền các hàm bình thường như các tham số như thế này:

#include <functional>
void apply(std::function<void(int)> f) {
    f(10);
    f(20);
    f(30);
}
void output(int width) {
    int col;
    apply([width,&col](int data) {
        cout << data << ((++col % width) ? ' ' : '\n');
    });
}

1
Lý do là, lambda chỉ có thể chuyển đổi thành một con trỏ hàm, nếu nó không có sự bắt giữ. nếu applylà một mẫu chấp nhận functor, nó sẽ hoạt động
sp2danny 10/03/2015

1
Nhưng vấn đề là nếu áp dụng là một giao diện hiện có, bạn có thể không có khả năng khai báo nó khác với một chức năng cũ đơn giản. Tiêu chuẩn có thể đã được thiết kế để cho phép một phiên bản mới của hàm cũ đơn giản được tạo ra mỗi khi biểu thức lambda như vậy được thực thi, với các tham chiếu được mã hóa cứng cho các biến đã bắt. Có vẻ như một hàm lambda được tạo ra tại thời gian biên dịch. Có những hậu quả khác là tốt. ví dụ: Nếu bạn khai báo một biến tĩnh, ngay cả khi bạn đánh giá lại biểu thức lambda, bạn không nhận được một biến tĩnh mới.
Ted

1
con trỏ hàm thường có nghĩa là được lưu và một bản chụp lambdas có thể vượt ra ngoài phạm vi. mà chỉ lambdas chụp ít chuyển đổi sang chức năng con trỏ là do thiết kế
sp2danny

1
Bạn vẫn phải chú ý đến các biến stack được xử lý vì cùng một lý do. Xem blog.msdn.com/b/nativeconcurrency/archive/2012/01/29/ Khăn Ví dụ tôi đã viết với đầu ra và áp dụng được viết để nếu thay vào đó, các con trỏ hàm được cho phép và sử dụng, chúng cũng hoạt động tốt. Col vẫn được phân bổ cho đến khi tất cả các lệnh gọi từ ứng dụng kết thúc. Làm thế nào bạn sẽ viết lại mã này để làm việc bằng cách sử dụng giao diện áp dụng hiện có? Bạn sẽ kết thúc bằng cách sử dụng các biến toàn cục hay tĩnh, hoặc một số chuyển đổi tối nghĩa hơn của mã?
Ted

1
hoặc có lẽ bạn chỉ đơn giản có nghĩa là các biểu thức lambda là giá trị và do đó là tạm thời, nhưng mã vẫn không đổi (singleton / static) để có thể được gọi trong tương lai. Trong trường hợp đó, có lẽ chức năng vẫn được phân bổ miễn là các ảnh chụp được phân bổ ngăn xếp của nó vẫn được phân bổ. Tất nhiên nó có thể gây rối khi gỡ rối nếu ví dụ nhiều biến thể của hàm được phân bổ trong một vòng lặp.
Ted

12

Một trong những lời giải thích tốt nhất lambda expressionđược đưa ra từ tác giả của C ++ Bjarne Stroustrup trong cuốn sách ***The C++ Programming Language***chương 11 ( ISBN-13: 978-0321563842 ):

What is a lambda expression?

Một biểu thức lambda , đôi khi cũng được gọi là hàm lambda hoặc (nói đúng không chính xác, nhưng thông tục) là lambda , là một ký hiệu đơn giản để xác định và sử dụng một đối tượng hàm ẩn danh . Thay vì định nghĩa một lớp được đặt tên bằng một toán tử (), sau đó tạo một đối tượng của lớp đó và cuối cùng gọi nó, chúng ta có thể sử dụng một tốc ký.

When would I use one?

Điều này đặc biệt hữu ích khi chúng ta muốn chuyển một hoạt động như một đối số cho một thuật toán. Trong ngữ cảnh của giao diện người dùng đồ họa (và các nơi khác), các hoạt động như vậy thường được gọi là các cuộc gọi lại .

What class of problem do they solve that wasn't possible prior to their introduction?

Ở đây tôi đoán mọi hành động được thực hiện với biểu thức lambda có thể được giải quyết mà không có chúng, nhưng với nhiều mã hơn và độ phức tạp lớn hơn nhiều. Lambda biểu hiện đây là cách tối ưu hóa cho mã của bạn và là cách làm cho nó hấp dẫn hơn. Như buồn bởi Stroustup:

cách tối ưu hóa hiệu quả

Some examples

thông qua biểu thức lambda

void print_modulo(const vector<int>& v, ostream& os, int m) // output v[i] to os if v[i]%m==0
{
    for_each(begin(v),end(v),
        [&os,m](int x) { 
           if (x%m==0) os << x << '\n';
         });
}

hoặc thông qua chức năng

class Modulo_print {
         ostream& os; // members to hold the capture list int m;
     public:
         Modulo_print(ostream& s, int mm) :os(s), m(mm) {} 
         void operator()(int x) const
           { 
             if (x%m==0) os << x << '\n'; 
           }
};

hoặc thậm chí

void print_modulo(const vector<int>& v, ostream& os, int m) 
     // output v[i] to os if v[i]%m==0
{
    class Modulo_print {
        ostream& os; // members to hold the capture list
        int m; 
        public:
           Modulo_print (ostream& s, int mm) :os(s), m(mm) {}
           void operator()(int x) const
           { 
               if (x%m==0) os << x << '\n';
           }
     };
     for_each(begin(v),end(v),Modulo_print{os,m}); 
}

Nếu bạn cần bạn có thể đặt tên lambda expressionnhư dưới đây:

void print_modulo(const vector<int>& v, ostream& os, int m)
    // output v[i] to os if v[i]%m==0
{
      auto Modulo_print = [&os,m] (int x) { if (x%m==0) os << x << '\n'; };
      for_each(begin(v),end(v),Modulo_print);
 }

Hoặc giả sử một mẫu đơn giản khác

void TestFunctions::simpleLambda() {
    bool sensitive = true;
    std::vector<int> v = std::vector<int>({1,33,3,4,5,6,7});

    sort(v.begin(),v.end(),
         [sensitive](int x, int y) {
             printf("\n%i\n",  x < y);
             return sensitive ? x < y : abs(x) < abs(y);
         });


    printf("sorted");
    for_each(v.begin(), v.end(),
             [](int x) {
                 printf("x - %i;", x);
             }
             );
}

sẽ tạo tiếp theo

0

1

0

1

0

1

0

1

0

1

0 sắp xếpx - 1; x - 3; x - 4; x - 5; x - 6; x - 7; x - 33;

[]- đây là danh sách chụp hoặc lambda introducer: nếu lambdaskhông có quyền truy cập vào môi trường địa phương của họ, chúng tôi có thể sử dụng nó.

Trích dẫn từ cuốn sách:

Ký tự đầu tiên của biểu thức lambda luôn là [ . Một người giới thiệu lambda có thể có nhiều hình thức:

[] : danh sách chụp trống. Điều này ngụ ý rằng không có tên địa phương từ bối cảnh xung quanh có thể được sử dụng trong cơ thể lambda. Đối với các biểu thức lambda như vậy, dữ liệu được lấy từ các đối số hoặc từ các biến không nhắm mục tiêu.

[&] : chụp ngầm bằng tham chiếu. Tất cả các tên địa phương có thể được sử dụng. Tất cả các biến cục bộ được truy cập bằng cách tham khảo.

[=] : hoàn toàn nắm bắt theo giá trị. Tất cả các tên địa phương có thể được sử dụng. Tất cả các tên đều đề cập đến các bản sao của các biến cục bộ được lấy tại điểm gọi của biểu thức lambda.

[danh sách chụp]: chụp rõ ràng; danh sách chụp là danh sách tên của các biến cục bộ sẽ được ghi lại (nghĩa là được lưu trữ trong đối tượng) theo tham chiếu hoặc theo giá trị. Các biến có tên đứng trước & được bắt bằng tham chiếu. Các biến khác được nắm bắt bởi giá trị. Một danh sách chụp cũng có thể chứa cái này và tên được theo sau bởi ... như các phần tử.

[&, Capture-list] : hoàn toàn nắm bắt bằng cách tham chiếu tất cả các biến cục bộ với các tên không được nhắc đến trong danh sách. Danh sách chụp có thể chứa điều này. Tên được liệt kê không thể đứng trước &. Các biến có tên trong danh sách chụp được ghi theo giá trị.

[=, Capture-list] : hoàn toàn nắm bắt theo giá trị tất cả các biến cục bộ với các tên không được đề cập trong danh sách. Danh sách chụp không thể chứa cái này. Các tên được liệt kê phải được đi trước bởi &. Các biến thể có tên trong danh sách chụp được tham chiếu.

Lưu ý rằng một tên cục bộ có trước & luôn được bắt bởi tham chiếu và một tên cục bộ không được đặt trước bởi & luôn được ghi theo giá trị. Chỉ chụp bằng tham chiếu cho phép sửa đổi các biến trong môi trường gọi.

Additional

Lambda expression định dạng

nhập mô tả hình ảnh ở đây

Tham khảo thêm:


Giải thích tốt đẹp. Sử dụng phạm vi dựa trên phạm vi cho các vòng lặp, bạn có thể tránh lambdas và rút ngắn mãfor (int x : v) { if (x % m == 0) os << x << '\n';}
Dietrich Baumgarten

2

Vâng, một cách sử dụng thực tế mà tôi đã tìm ra là giảm mã tấm nồi hơi. Ví dụ:

void process_z_vec(vector<int>& vec)
{
  auto print_2d = [](const vector<int>& board, int bsize)
  {
    for(int i = 0; i<bsize; i++)
    {
      for(int j=0; j<bsize; j++)
      {
        cout << board[bsize*i+j] << " ";
      }
      cout << "\n";
    }
  };
  // Do sth with the vec.
  print_2d(vec,x_size);
  // Do sth else with the vec.
  print_2d(vec,y_size);
  //... 
}

Không có lambda, bạn có thể cần phải làm một cái gì đó cho các bsizetrường hợp khác nhau . Tất nhiên bạn có thể tạo một chức năng nhưng nếu bạn muốn giới hạn việc sử dụng trong phạm vi của chức năng người dùng linh hồn thì sao? bản chất của lambda đáp ứng yêu cầu này và tôi sử dụng nó cho trường hợp đó.


2

Các lambda trong c ++ được coi là "chức năng có sẵn". có nghĩa đen của nó trên đường đi, bạn xác định nó; sử dụng nó; và khi phạm vi hàm cha kết thúc, hàm lambda sẽ biến mất.

c ++ đã giới thiệu nó trong c ++ 11 và mọi người bắt đầu sử dụng nó như ở mọi nơi có thể. ví dụ và lambda là gì có thể tìm thấy ở đây https://en.cppreference.com/w/cpp/lingu/lambda

tôi sẽ mô tả cái không có nhưng cần thiết cho mọi lập trình viên c ++

Lambda không có nghĩa là sử dụng ở mọi nơi và mọi chức năng không thể được thay thế bằng lambda. Nó cũng không phải là nhanh nhất so với chức năng bình thường. bởi vì nó có một số chi phí cần được xử lý bởi lambda.

Nó chắc chắn sẽ giúp giảm số lượng dòng trong một số trường hợp. về cơ bản nó có thể được sử dụng cho phần mã, được gọi trong cùng một chức năng một hoặc nhiều lần và đoạn mã đó không cần thiết ở bất kỳ nơi nào khác để bạn có thể tạo chức năng độc lập cho nó.

Dưới đây là ví dụ cơ bản về lambda và những gì xảy ra trong nền.

Mã người dùng:

int main()
{
  // Lambda & auto
  int member=10;
  auto endGame = [=](int a, int b){ return a+b+member;};

  endGame(4,5);

  return 0;

}

Cách biên dịch mở rộng nó:

int main()
{
  int member = 10;

  class __lambda_6_18
  {
    int member;
    public: 
    inline /*constexpr */ int operator()(int a, int b) const
    {
      return a + b + member;
    }

    public: __lambda_6_18(int _member)
    : member{_member}
    {}

  };

  __lambda_6_18 endGame = __lambda_6_18{member};
  endGame.operator()(4, 5);

  return 0;
}

như bạn có thể thấy, loại chi phí nào được thêm vào khi bạn sử dụng nó. Vì vậy, nó không phải là ý tưởng tốt để sử dụng chúng ở khắp mọi nơi. nó có thể được sử dụng ở những nơi áp dụng chúng.


có nghĩa đen của nó trên đường đi, bạn xác định nó; sử dụng nó; và khi phạm vi hàm cha kết thúc, hàm lambda sẽ biến mất .. nếu hàm trả về lambda cho người gọi thì sao?
Nawaz

1
Nó cũng không phải là nhanh nhất so với chức năng bình thường. bởi vì nó có một số chi phí cần được xử lý bởi lambda. Bạn đã bao giờ thực sự chạy bất kỳ điểm chuẩn để hỗ trợ yêu cầu này ? Ngược lại, các mẫu lambda + thường tạo mã nhanh nhất có thể.
Nawaz

1

Một vấn đề nó giải quyết: Mã đơn giản hơn lambda cho một cuộc gọi trong hàm tạo sử dụng hàm tham số đầu ra để khởi tạo một thành viên const

Bạn có thể khởi tạo một thành viên const của lớp, với một lệnh gọi đến hàm đặt giá trị của nó bằng cách trả lại đầu ra của nó dưới dạng tham số đầu ra.


Điều này cũng có thể được thực hiện với một hàm đơn giản, thậm chí đó là câu trả lời được chấp nhận cho câu hỏi bạn liên kết để nói phải làm gì.
SirGuy
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.