2012-05-20 22 views
20

C++ 11 lambdas là tuyệt vời!Trong cú pháp lambda C++ 11, đóng cửa phân bổ đống?

Nhưng có một điều bị thiếu, đó là cách xử lý dữ liệu có thể thay đổi một cách an toàn.

Sau đây sẽ cho đếm xấu sau khi đếm đầu tiên:

#include <cstdio> 
#include <functional> 
#include <memory> 

std::function<int(void)> f1() 
{ 
    int k = 121; 
    return std::function<int(void)>([&]{return k++;}); 
} 

int main() 
{ 
    int j = 50; 
    auto g = f1(); 
    printf("%d\n", g()); 
    printf("%d\n", g()); 
    printf("%d\n", g()); 
    printf("%d\n", g()); 
} 

cho,

$ g++-4.5 -std=c++0x -o test test.cpp && ./test 
121 
8365280 
8365280 
8365280 

Lý do là sau khi f1() lợi nhuận, k là ra khỏi phạm vi nhưng vẫn trên stack. Vì vậy, lần đầu tiên g() được thực hiện k là tốt, nhưng sau đó ngăn xếp bị hỏng và k mất giá trị của nó.

Vì vậy, cách duy nhất tôi đã quản lý để thực hiện việc đóng cửa một cách an toàn được trả lại trong C++ 11 là để phân bổ biến khép kín một cách rõ ràng trên heap:

std::function<int(void)> f2() 
{ 
    int k = 121; 
    std::shared_ptr<int> o = std::shared_ptr<int>(new int(k)); 
    return std::function<int(void)>([=]{return (*o)++;}); 
} 

int main() 
{ 
    int j = 50; 
auto g = f2(); 
    printf("%d\n", g()); 
    printf("%d\n", g()); 
    printf("%d\n", g()); 
    printf("%d\n", g()); 
} 

Ở đây, [=] được sử dụng để đảm bảo con trỏ chia sẻ được sao chép, không được tham chiếu, do đó việc xử lý bộ nhớ được thực hiện chính xác: bản sao được phân bổ heap của k phải được giải phóng khi hàm được tạo g nằm ngoài phạm vi. Kết quả là như mong muốn,

$ g++-4.5 -std=c++0x -o test test.cpp && ./test 
121 
122 
123 
124 

Nó khá xấu xí để đề cập đến các biến bởi dereferencing họ, nhưng chúng ta có thể sử dụng tài liệu tham khảo thay vì:

std::function<int(void)> f3() 
{ 
    int k = 121; 
    std::shared_ptr<int> o = std::shared_ptr<int>(new int(k)); 
    int &p = *o; 
    return std::function<int(void)>([&]{return p++;}); 
} 

Trên thực tế, điều này kỳ quặc mang lại cho tôi,

$ g++-4.5 -std=c++0x -o test test.cpp && ./test 
0 
1 
2 
3 

Bất kỳ ý tưởng nào tại sao? Có lẽ nó không lịch sự để có một tham chiếu của một con trỏ chia sẻ, bây giờ mà tôi nghĩ về nó, vì nó không phải là một tài liệu tham khảo theo dõi. Tôi thấy rằng việc di chuyển tài liệu tham khảo để bên trong lambda gây ra một vụ tai nạn,

std::function<int(void)> f4() 
{ 
    int k = 121; 
std::shared_ptr<int> o = std::shared_ptr<int>(new int(k)); 
    return std::function<int(void)>([&]{int &p = *o; return p++;}); 
} 

hiến,

g++-4.5 -std=c++0x -o test test.cpp && ./test 
156565552 
/bin/bash: line 1: 25219 Segmentation fault  ./test 

Trong mọi trường hợp, nó sẽ được tốt đẹp nếu có một cách để tự động thực hiện việc đóng cửa một cách an toàn được trả lại qua phân bổ đống. Ví dụ: nếu có một giải pháp thay thế cho [=][&] cho biết rằng các biến phải được phân bổ theo heap và được tham chiếu qua tham chiếu đến các con trỏ được chia sẻ. Suy nghĩ ban đầu của tôi khi tôi biết về std::function là nó tạo ra một đối tượng đóng gói đóng cửa, do đó nó có thể cung cấp dung lượng cho môi trường đóng cửa, nhưng các thử nghiệm của tôi cho thấy điều này dường như không giúp ích gì.

Tôi nghĩ rằng đóng cửa có thể trả lại an toàn trong C++ 11 sẽ là tối quan trọng khi sử dụng chúng, có ai biết cách này có thể được thực hiện thanh lịch hơn không?

+2

Bạn nên chạy thử nghiệm như thế này bên trong Valgrind, bởi vì bạn đang nhìn thấy hành vi đúng cơ hội khi truy cập bộ nhớ deallocated. – Potatoswatter

+0

static int k = 121; – adnako

Trả lời

22

Trong f1 bạn đang nhận được hành vi không xác định vì lý do bạn nói; lambda chứa tham chiếu đến biến cục bộ và sau khi hàm trả về tham chiếu không còn hợp lệ nữa.Để làm được việc này bạn không phải phân bổ trên heap, bạn chỉ cần phải tuyên bố rằng các giá trị bị bắt là có thể thay đổi:

int k = 121; 
return std::function<int(void)>([=]() mutable {return k++;}); 

Bạn sẽ phải cẩn thận về việc sử dụng lambda này mặc dù, bởi vì bản khác nhau của nó sẽ được sửa đổi bản sao của riêng họ của biến bị bắt. Các thuật toán thường kỳ vọng rằng việc sử dụng một bản sao của một hàm functor tương đương với việc sử dụng bản gốc. Tôi nghĩ rằng chỉ có một thuật toán thực sự tạo phụ cấp cho một đối tượng hàm stateful, std :: for_each, nơi nó trả về một bản sao của đối tượng hàm mà nó sử dụng để bạn có thể truy cập bất kỳ sửa đổi nào xảy ra.


Không giữ gìn bản sao của con trỏ dùng chung, do đó bộ nhớ được giải phóng và truy cập nó mang lại hành vi không xác định. Bạn có thể sửa lỗi này bằng cách chụp rõ ràng con trỏ được chia sẻ theo giá trị và vẫn nắm bắt được trỏ tới int bằng tham chiếu.

std::shared_ptr<int> o = std::shared_ptr<int>(new int(k)); 
int &p = *o; 
return std::function<int(void)>([&p,o]{return p++;}); 

f4 là hành vi không xác định một lần nữa bởi vì bạn đang một lần nữa chiếm được một tham chiếu đến một biến địa phương, o. Bạn chỉ cần nắm bắt theo giá trị nhưng sau đó vẫn tạo int &p bên trong lambda để nhận cú pháp bạn muốn.

std::shared_ptr<int> o = std::shared_ptr<int>(new int(k)); 
return std::function<int(void)>([o]() -> int {int &p = *o; return p++;}); 

Lưu ý rằng khi bạn thêm câu lệnh thứ hai C++ 11 không còn cho phép bạn bỏ qua kiểu trả về. (clang và tôi giả định gcc có phần mở rộng cho phép khấu trừ kiểu trả về ngay cả với nhiều câu lệnh, nhưng bạn sẽ nhận được cảnh báo ít nhất.)

+0

Lưu ý rằng khó truy cập trạng thái trong lambda hơn hàm functor kiểu cũ. Điều duy nhất bạn có thể làm là gọi lại. Nhưng lambdas nhà nước vẫn còn hữu ích ngay cả khi họ không trả lại kết quả phụ trợ. – Potatoswatter

+0

"* Tôi nghĩ rằng chỉ có một thuật toán thực sự tạo phụ cấp cho lambda stateful, std :: tích lũy, nơi nó trả về lambda mà nó sử dụng để bạn có thể truy cập bất kỳ sửa đổi nào xảy ra. *" Đó là 'std :: for_each' bạn nghĩ đến việc. – ildjarn

+0

@ildjarn Sửa chữa, cảm ơn. – bames53

1

Đây là mã thử nghiệm của tôi. Nó sử dụng hàm đệ quy để so sánh địa chỉ của tham số lambda với các biến dựa trên stack khác.

#include <stdio.h> 
#include <functional> 

void fun2(std::function<void()> callback) { 
    (callback)(); 
} 

void fun1(int n) { 
    if(n <= 0) return; 
    printf("stack address = %p, ", &n); 

    fun2([n]() { 
     printf("capture address = %p\n", &n); 
     fun1(n - 1); 
    }); 
} 

int main() { 
    fun1(200); 
    return 0; 
} 

Biên dịch mã với mingw64 và chạy trên Win7, nó xuất

stack address = 000000000022F1E0, capture address = 00000000002F6D20 
stack address = 000000000022F0C0, capture address = 00000000002F6D40 
stack address = 000000000022EFA0, capture address = 00000000002F6D60 
stack address = 000000000022EE80, capture address = 00000000002F6D80 
stack address = 000000000022ED60, capture address = 00000000002F6DA0 
stack address = 000000000022EC40, capture address = 00000000002F6DC0 
stack address = 000000000022EB20, capture address = 00000000007A7810 
stack address = 000000000022EA00, capture address = 00000000007A7820 
stack address = 000000000022E8E0, capture address = 00000000007A7830 
stack address = 000000000022E7C0, capture address = 00000000007A7840 

Rõ ràng rằng các thông số chụp được không nằm trên đống khu vực, và địa chỉ của các thông số chụp là không liên tục.

Vì vậy, tôi tin rằng một số trình biên dịch có thể sử dụng phân bổ bộ nhớ động phân bổ bộ nhớ động đến
chụp tham số lambda.

+1

Đối tượng Lambda chỉ đơn giản là cú pháp đường để tạo các đối tượng hàm bình thường.Các biến được chụp trở thành thành viên của đối tượng hàm. Phân bổ bộ nhớ động không được sử dụng trừ khi bạn viết mã cụ thể để làm điều đó. – bames53

+0

@ bames53 theo bài viết này: http://www.drdobbs.com/cpp/efficient-use-of-lambda-expressions-and/232500059?pgno=2, std :: thực hiện chức năng thực sự được phép thực hiện phân bổ động . – marcinj

+0

@brightstar Tôi chỉ nói về việc triển khai trình biên dịch của các đối tượng lambda. Câu trả lời ở trên sẽ phân bổ sai phân bổ động trong việc thực hiện 'std :: function <>' đối với việc thực hiện của trình biên dịch đối tượng lambda. – bames53

Các vấn đề liên quan