Tại sao lambda của C ++ 11 lại yêu cầu từ khóa có thể thay đổi được cho tính năng bắt lỗi theo mặc định?


256

Ví dụ ngắn:

#include <iostream>

int main()
{
    int n;
    [&](){n = 10;}();             // OK
    [=]() mutable {n = 20;}();    // OK
    // [=](){n = 10;}();          // Error: a by-value capture cannot be modified in a non-mutable lambda
    std::cout << n << "\n";       // "10"
}

Câu hỏi: Tại sao chúng ta cần mutabletừ khóa? Nó khá khác với tham số truyền thống truyền đến các hàm được đặt tên. Lý do đằng sau là gì?

Tôi có ấn tượng rằng toàn bộ quan điểm của giá trị nắm bắt là cho phép người dùng thay đổi tạm thời - nếu không thì tôi hầu như luôn luôn tốt hơn khi sử dụng tham chiếu chụp, phải không?

Có giác ngộ nào không?

(Nhân tiện, tôi đang sử dụng MSVC2010. AFAIK đây phải là tiêu chuẩn)


101
Câu hỏi hay; mặc dù tôi rất vui vì một cái gì đó cuối cùng constđược mặc định!
xtofl

3
Không phải là một câu trả lời, nhưng tôi nghĩ đây là một điều hợp lý: nếu bạn lấy thứ gì đó theo giá trị, bạn không nên thay đổi nó chỉ để tiết kiệm cho bạn 1 bản sao cho một biến cục bộ. Ít nhất bạn sẽ không phạm sai lầm khi thay đổi n bằng cách thay thế = bằng &.
stefaanv

8
@xtofl: Không chắc là nó tốt, khi mọi thứ khác không constđược mặc định.
kizzx2

8
@ Tamás Szelei: Không bắt đầu tranh luận, nhưng IMHO khái niệm "dễ học" không có chỗ trong ngôn ngữ C ++, đặc biệt là trong thời hiện đại. Dù sao: P
kizzx2

3
"Toàn bộ quan điểm của giá trị nắm bắt là cho phép người dùng thay đổi tạm thời" - Không, toàn bộ vấn đề là lambda có thể vẫn còn hiệu lực trong suốt thời gian tồn tại của bất kỳ biến số nào. Nếu C ++ lambdas chỉ có tính năng bắt giữ, chúng sẽ không thể sử dụng được theo cách quá nhiều tình huống.
Sebastian Redl

Câu trả lời:


230

Nó yêu cầu mutablebởi vì theo mặc định, một đối tượng hàm sẽ tạo ra kết quả tương tự mỗi lần nó được gọi. Đây là sự khác biệt giữa một hàm định hướng đối tượng và một hàm sử dụng biến toàn cục, một cách hiệu quả.


7
đây là một quan điểm tốt. Tôi hoàn toàn đồng ý. Mặc dù trong C ++ 0x, tôi không thấy mặc định giúp thực thi như thế nào. Hãy xem xét tôi đang ở cuối nhận lambda, ví dụ như tôi void f(const std::function<int(int)> g). Làm thế nào tôi được đảm bảo rằng gthực sự là minh bạch tham chiếu ? gNhà cung cấp có thể đã sử dụng mutableanyway. Vì vậy, tôi sẽ không biết. Mặt khác, nếu mặc định là không- constvà mọi người phải thêm constthay vì vào các mutableđối tượng chức năng, trình biên dịch thực sự có thể thực thi const std::function<int(int)>phần đó và bây giờ fcó thể cho rằng đó gconstkhông?
kizzx2

8
@ kizzx2: Trong C ++, không có gì được thi hành , chỉ được đề xuất. Như thường lệ, nếu bạn làm điều gì đó ngu ngốc (yêu cầu tài liệu về tính minh bạch của tham chiếu và sau đó vượt qua chức năng không tham chiếu), bạn sẽ nhận được bất cứ điều gì đến với mình.
Cún con

6
Câu trả lời này đã mở mắt tôi. Trước đây, tôi nghĩ rằng trong trường hợp này lambda chỉ đột biến một bản sao cho "chạy" hiện tại.
Zsolt Szatmari

4
@ZsoltSzatmari Nhận xét của bạn đã mở mắt tôi! : -DI không hiểu ý nghĩa thực sự của câu trả lời này cho đến khi tôi đọc bình luận của bạn.
Jendas

5
Tôi không đồng ý với tiền đề cơ bản của câu trả lời này. C ++ không có khái niệm "các hàm nên luôn trả về cùng một giá trị" ở bất kỳ nơi nào khác trong ngôn ngữ. Như một nguyên tắc thiết kế, tôi sẽ đồng ý đó là một cách tốt để viết một hàm, nhưng tôi không nghĩ rằng nó giữ nước như các lý do cho hành vi tiêu chuẩn.
Ionoclast Brigham

103

Mã của bạn gần như tương đương với điều này:

#include <iostream>

class unnamed1
{
    int& n;
public:
    unnamed1(int& N) : n(N) {}

    /* OK. Your this is const but you don't modify the "n" reference,
    but the value pointed by it. You wouldn't be able to modify a reference
    anyway even if your operator() was mutable. When you assign a reference
    it will always point to the same var.
    */
    void operator()() const {n = 10;}
};

class unnamed2
{
    int n;
public:
    unnamed2(int N) : n(N) {}

    /* OK. Your this pointer is not const (since your operator() is "mutable" instead of const).
    So you can modify the "n" member. */
    void operator()() {n = 20;}
};

class unnamed3
{
    int n;
public:
    unnamed3(int N) : n(N) {}

    /* BAD. Your this is const so you can't modify the "n" member. */
    void operator()() const {n = 10;}
};

int main()
{
    int n;
    unnamed1 u1(n); u1();    // OK
    unnamed2 u2(n); u2();    // OK
    //unnamed3 u3(n); u3();  // Error
    std::cout << n << "\n";  // "10"
}

Vì vậy, bạn có thể nghĩ lambdas như tạo một lớp với toán tử () mặc định là const trừ khi bạn nói rằng nó có thể thay đổi.

Bạn cũng có thể nghĩ về tất cả các biến được nắm bắt bên trong [] (rõ ràng hoặc ngầm) là thành viên của lớp đó: bản sao của các đối tượng cho [=] hoặc tham chiếu đến các đối tượng cho [&]. Chúng được khởi tạo khi bạn khai báo lambda của bạn như thể có một hàm tạo ẩn.


5
Mặc dù một lời giải thích hay về việc một consthoặc mutablelambda sẽ trông như thế nào nếu được triển khai dưới dạng các loại do người dùng xác định tương đương, câu hỏi là (như trong tiêu đề và được OP xây dựng trong các bình luận) tại sao lại const là mặc định, vì vậy điều này không trả lời nó.
gạch dưới

36

Tôi có ấn tượng rằng toàn bộ quan điểm của giá trị nắm bắt là cho phép người dùng thay đổi tạm thời - nếu không thì tôi hầu như luôn luôn tốt hơn khi sử dụng tham chiếu chụp, phải không?

Câu hỏi là, nó "gần như"? Một trường hợp sử dụng thường xuyên dường như là để trả lại hoặc vượt qua lambdas:

void registerCallback(std::function<void()> f) { /* ... */ }

void doSomething() {
  std::string name = receiveName();
  registerCallback([name]{ /* do something with name */ });
}

Tôi nghĩ đó mutablekhông phải là một trường hợp "gần như". Tôi xem xét "bắt theo giá trị" như "cho phép tôi sử dụng giá trị của nó sau khi thực thể bị bắt chết" thay vì "cho phép tôi thay đổi bản sao của nó". Nhưng có lẽ điều này có thể được tranh luận.


2
Ví dụ tốt. Đây là trường hợp sử dụng rất mạnh cho việc sử dụng giá trị nắm bắt. Nhưng tại sao nó mặc định const? Nó đạt được mục đích gì? mutabledường như ra khỏi nơi đây, khi constkhông mặc định trong "gần như" (: P) mọi thứ khác của ngôn ngữ.
kizzx2

8
@ kizzx2: Tôi ước constlà mặc định, ít nhất mọi người sẽ bị buộc phải xem xét tính đúng đắn: /
Matthieu M.

1
@ kizzx2 nhìn vào các giấy tờ lambda, có vẻ như họ làm cho nó mặc định để consthọ có thể gọi nó cho dù đối tượng lambda có phải là const hay không. Ví dụ, họ có thể truyền nó cho một hàm lấy a std::function<void()> const&. Để cho phép lambda thay đổi các bản sao đã chụp, trong các giấy tờ ban đầu, các thành viên dữ liệu của việc đóng được xác định mutabletự động trong nội bộ. Bây giờ bạn phải tự đặt mutablebiểu thức lambda. Tôi đã không tìm thấy một lý do chi tiết mặc dù.
Julian Schaub - litb


5
Tại thời điểm này, với tôi, câu trả lời / lý do "thực sự" dường như là "họ đã thất bại trong việc giải quyết một chi tiết thực hiện": /
kizzx2

32

FWIW, Herb Sutter, một thành viên nổi tiếng của ủy ban tiêu chuẩn hóa C ++, cung cấp một câu trả lời khác nhau cho câu hỏi đó trong các vấn đề về tính chính xác và khả năng sử dụng của Lambda :

Hãy xem xét ví dụ người rơm này, nơi lập trình viên nắm bắt một biến cục bộ theo giá trị và cố gắng sửa đổi giá trị đã bắt (là biến thành viên của đối tượng lambda):

int val = 0;
auto x = [=](item e)            // look ma, [=] means explicit copy
            { use(e,++val); };  // error: count is const, need ‘mutable’
auto y = [val](item e)          // darnit, I really can’t get more explicit
            { use(e,++val); };  // same error: count is const, need ‘mutable’

Tính năng này dường như đã được thêm vào do lo ngại rằng người dùng có thể không nhận ra mình đã có một bản sao và đặc biệt là vì lambdas có thể sao chép nên anh ta có thể thay đổi một bản sao khác của lambda.

Bài viết của ông là về lý do tại sao điều này nên được thay đổi trong C ++ 14. Nó ngắn gọn, được viết tốt, đáng đọc nếu bạn muốn biết "những gì trong tâm trí [thành viên ủy ban]" liên quan đến tính năng đặc biệt này.


16

Bạn cần nghĩ kiểu đóng của hàm Lambda là gì. Mỗi khi bạn khai báo một biểu thức Lambda, trình biên dịch sẽ tạo ra một kiểu đóng, không khác gì một khai báo lớp không tên với các thuộc tính ( môi trường nơi biểu thức Lambda được khai báo) và lệnh gọi hàm được ::operator()thực thi. Khi bạn chụp một biến bằng copy-by-value , trình biên dịch sẽ tạo một constthuộc tính mới trong kiểu đóng, vì vậy bạn không thể thay đổi nó trong biểu thức Lambda vì đó là thuộc tính "chỉ đọc", đó là lý do chúng gọi nó là "bao đóng ", bởi vì theo một cách nào đó, bạn đang đóng biểu thức Lambda của mình bằng cách sao chép các biến từ phạm vi trên vào phạm vi Lambda.mutable, thực thể bị bắt sẽ trở thành một non-constthuộc tính của kiểu đóng của bạn. Đây là nguyên nhân khiến các thay đổi được thực hiện trong biến có thể thay đổi được ghi theo giá trị, không được truyền đến phạm vi trên, nhưng giữ bên trong Lambda có trạng thái. Luôn cố gắng tưởng tượng kiểu đóng kết quả của biểu thức Lambda của bạn, điều đó đã giúp tôi rất nhiều và tôi hy vọng nó cũng có thể giúp bạn.


14

Xem dự thảo này , theo 5.1.2 [expr.prim.lambda], mục 5:

Kiểu đóng cho biểu thức lambda có toán tử gọi hàm nội tuyến công khai (13,5.4) có tham số và kiểu trả về được mô tả theo mệnh đề tham số-khai báo tham số và biểu thức tương ứng của lambda-biểu thức. Toán tử gọi hàm này được khai báo const (9.3.1) khi và chỉ khi mệnh đề khai báo tham số của lambdaexpression không được theo sau bởi mutable.

Chỉnh sửa nhận xét của litb: Có lẽ họ đã nghĩ đến việc nắm bắt theo giá trị để những thay đổi bên ngoài đối với các biến không được phản ánh bên trong lambda? Tài liệu tham khảo hoạt động cả hai cách, vì vậy đó là lời giải thích của tôi. Không biết nó có tốt không.

Chỉnh sửa trên nhận xét của kizzx2: Hầu hết thời gian sử dụng lambda là functor cho các thuật toán. Tính năng mặc định constcho phép nó được sử dụng trong một môi trường không đổi, giống như các consthàm đủ tiêu chuẩn thông thường có thể được sử dụng ở đó, nhưng các hàm không constđủ tiêu chuẩn thì không thể. Có lẽ họ chỉ nghĩ làm cho nó trực quan hơn cho những trường hợp đó, những người biết những gì diễn ra trong tâm trí họ. :)


Đó là tiêu chuẩn, nhưng tại sao họ lại viết nó theo cách này?
kizzx2

@ kizzx2: Giải thích của tôi là trực tiếp dưới trích dẫn đó. :) Nó liên quan một chút đến những gì litb nói về tuổi thọ của các đối tượng bị bắt, nhưng cũng đi xa hơn một chút.
Xèo

@Xeo: Ồ vâng, tôi đã bỏ lỡ rằng: P Đó cũng là một lời giải thích tốt khác cho việc sử dụng tốt giá trị nắm bắt . Nhưng tại sao nó phải được constmặc định? Tôi đã có một bản sao mới, có vẻ lạ khi không để tôi thay đổi nó - đặc biệt nó không phải là một cái gì đó sai về cơ bản - họ chỉ muốn tôi thêm vào mutable.
kizzx2

Tôi tin rằng đã có một nỗ lực để tạo ra một cú pháp khai báo chức năng genral mới, trông giống như một lambda có tên. Nó cũng được cho là để khắc phục các vấn đề khác bằng cách làm cho mọi thứ được mặc định. Không bao giờ hoàn thành, nhưng các ý tưởng đã loại bỏ định nghĩa lambda.
Bo Persson

2
@ kizzx2 - Nếu chúng ta có thể bắt đầu lại từ đầu, có lẽ chúng ta sẽ có varmột từ khóa để cho phép thay đổi và liên tục là mặc định cho mọi thứ khác. Bây giờ chúng tôi không, vì vậy chúng tôi phải sống với điều đó. IMO, C ++ 2011 ra đời khá tốt, xem xét mọi thứ.
Bo Persson

11

Tôi có ấn tượng rằng toàn bộ quan điểm của giá trị nắm bắt là cho phép người dùng thay đổi tạm thời - nếu không thì tôi hầu như luôn luôn tốt hơn khi sử dụng tham chiếu chụp, phải không?

nkhông tạm thời. n là thành viên của lambda-function-object mà bạn tạo bằng biểu thức lambda. Kỳ vọng mặc định là việc gọi lambda của bạn không sửa đổi trạng thái của nó, do đó, nó là để ngăn bạn vô tình sửa đổi n.


1
Toàn bộ đối tượng lambda là tạm thời, các thành viên của nó cũng có thời gian tồn tại tạm thời.
Ben Voigt

2
@Ben: IIRC, tôi đã đề cập đến vấn đề rằng khi ai đó nói "tạm thời", tôi hiểu nó có nghĩa là đối tượng tạm thời không tên , mà chính lambda là, nhưng đó không phải là thành viên. Và cũng từ "bên trong" lambda, việc lambda chỉ là tạm thời không quan trọng. Đọc lại câu hỏi mặc dù có vẻ như OP chỉ muốn nói "n bên trong lambda" khi anh ấy nói "tạm thời".
Martin Ba

6

Bạn phải hiểu ý nghĩa của việc chụp! nó đang nắm bắt không tranh luận đi qua! Hãy xem một số mẫu mã:

int main()
{
    using namespace std;
    int x = 5;
    int y;
    auto lamb = [x]() {return x + 5; };

    y= lamb();
    cout << y<<","<< x << endl; //outputs 10,5
    x = 20;
    y = lamb();
    cout << y << "," << x << endl; //output 10,20

}

Như bạn có thể thấy mặc dù xđã được đổi thành 20lambda vẫn trả về 10 ( xvẫn 5ở bên trong lambda) Thay đổi xbên trong lambda có nghĩa là thay đổi chính lambda ở mỗi cuộc gọi (lambda đang biến đổi trong mỗi cuộc gọi). Để thực thi tính đúng đắn, tiêu chuẩn đã đưa ra mutabletừ khóa. Bằng cách chỉ định lambda là có thể thay đổi, bạn đang nói rằng mỗi cuộc gọi đến lambda có thể gây ra thay đổi trong chính lambda. Hãy xem một ví dụ khác:

int main()
{
    using namespace std;
    int x = 5;
    int y;
    auto lamb = [x]() mutable {return x++ + 5; };

    y= lamb();
    cout << y<<","<< x << endl; //outputs 10,5
    x = 20;
    y = lamb();
    cout << y << "," << x << endl; //outputs 11,20

}

Ví dụ trên cho thấy rằng bằng cách làm cho lambda có thể thay đổi, việc thay đổi xbên trong lambda "biến đổi" lambda ở mỗi cuộc gọi với một giá trị mới xkhông liên quan gì đến giá trị thực của xhàm chính


4

Hiện tại có một đề xuất để giảm bớt sự cần thiết mutabletrong các tuyên bố lambda: n3424


Bất kỳ thông tin về những gì đã đến? Cá nhân tôi nghĩ rằng đó là một ý tưởng tồi, vì "việc nắm bắt các biểu hiện tùy ý" mới giúp loại bỏ hầu hết các điểm đau.
Ben Voigt

1
@BenVoigt Vâng, có vẻ như một sự thay đổi vì lợi ích của sự thay đổi.
Tuyến Miles

3
@BenVoigt Mặc dù công bằng, tôi hy vọng có thể có nhiều nhà phát triển C ++ không biết đó mutablethậm chí là một từ khóa trong C ++.
Miles Rout

1

Để mở rộng câu trả lời của Puppy, các hàm lambda được dự định là các hàm thuần túy . Điều đó có nghĩa là mỗi cuộc gọi được cung cấp một bộ đầu vào duy nhất luôn trả về cùng một đầu ra. Hãy xác định đầu vào là tập hợp của tất cả các đối số cộng với tất cả các biến được bắt khi lambda được gọi.

Trong các hàm thuần túy đầu ra chỉ phụ thuộc vào đầu vào chứ không phụ thuộc vào một số trạng thái bên trong. Do đó, bất kỳ chức năng lambda nào, nếu thuần túy, không cần thay đổi trạng thái của nó và do đó là bất biến.

Khi lambda nắm bắt bằng cách tham chiếu, viết vào các biến bị bắt là một biến dạng đối với khái niệm hàm thuần túy, bởi vì tất cả một hàm thuần nên làm là trả về một đầu ra, mặc dù lambda không chắc chắn bị đột biến vì việc viết xảy ra với các biến ngoài. Ngay cả trong trường hợp này, một cách sử dụng đúng ngụ ý rằng nếu lambda được gọi lại với cùng một đầu vào, thì đầu ra sẽ giống nhau mọi lúc, mặc dù các tác dụng phụ này đối với các biến phụ. Các tác dụng phụ như vậy chỉ là cách để trả về một số đầu vào bổ sung (ví dụ: cập nhật bộ đếm) và có thể được định dạng lại thành một hàm thuần túy, ví dụ trả về một tuple thay vì một giá trị.

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.