2013-06-04 32 views
30

Đi qua một lambda là thực sự dễ dàng trong C++ 11:Làm thế nào để C++ 11 lambdas được biểu diễn và truyền?

func([](int arg) { 
    // code 
}) ; 

Nhưng tôi tự hỏi, chi phí đi qua một lambda đến một chức năng như thế này là gì? Nếu func chuyển lambda sang các hàm khác thì sao?

void func(function< void (int arg) > f) { 
    doSomethingElse(f) ; 
} 

Việc truyền lambda có đắt không? Kể từ khi một đối tượng functionthể được gán 0,

function< void (int arg) > f = 0 ; // 0 means "not init" 

nó dẫn tôi nghĩ rằng chức năng các đối tượng loại hành động như con trỏ. Nhưng mà không sử dụng new, thì điều đó có nghĩa là chúng có thể giống như giá trị được đánh số struct hoặc các lớp, mặc định là phân bổ ngăn xếp và sao chép thành viên khôn ngoan.

Cơ chế mã C++ 11 "và nhóm biến bị bắt qua khi bạn chuyển đối tượng hàm" theo giá trị "? Có rất nhiều bản sao dư thừa của nội dung mã không? Tôi có nên phải đánh dấu mỗi function đối tượng thông qua với const& để một bản sao không được thực hiện:

void func(const function< void (int arg) >& f) { 
} 

Hoặc làm chức năng các đối tượng bằng cách nào đó vượt qua khác biệt so với cấu trúc của C++ thường xuyên?

+0

Tốt câu hỏi, cũng muốn biết :) – Xaqq

Trả lời

30

Tuyên bố từ chối trách nhiệm: Câu trả lời của tôi hơi đơn giản hóa so với thực tế (tôi đã bỏ một số chi tiết sang một bên) nhưng bức tranh lớn ở đây. Ngoài ra, tiêu chuẩn không xác định đầy đủ cách lambdas hoặc std::function phải được thực hiện trong nội bộ (việc triển khai có một số quyền tự do), giống như bất kỳ cuộc thảo luận nào về chi tiết triển khai, trình biên dịch của bạn có thể hoặc không thể thực hiện theo cách này. Tuy nhiên, một lần nữa, đây là một chủ đề khá giống với VTables: Tiêu chuẩn không ủy thác nhiều nhưng bất kỳ trình biên dịch hợp lý nào vẫn có khả năng làm theo cách này, vì vậy tôi tin rằng nó rất đáng để đào sâu vào nó một chút. :)


Lambdas

Cách đơn giản nhất để thực hiện một lambda là loại một ẩn danh struct:

auto lambda = [](Args...) -> Return { /*...*/ }; 

// roughly equivalent to: 
struct { 
    Return operator()(Args...) { /*...*/ } 
} 
lambda; // instance of the anonymous struct 

Cũng giống như bất kỳ lớp khác, khi bạn vượt qua trường hợp của nó xung quanh bạn không bao giờ phải sao chép mã, chỉ là dữ liệu thực tế (ở đây, không có gì cả).


Đối tượng bị bắt theo giá trị được sao chép vào struct:

Value v; 
auto lambda = [=](Args...) -> Return { /*... use v, captured by value...*/ }; 

// roughly equivalent to: 
struct Temporary { // note: we can't make it an anonymous struct any more since we need 
        // a constructor, but that's just a syntax quirk 

    const Value v; // note: capture by value is const by default unless the lambda is mutable 
    Temporary(Value v_) : v(v_) {} 
    Return operator()(Args...) { /*... use v, captured by value...*/ } 
} 
lambda(v); // instance of the struct 

Một lần nữa, đi qua nó xung quanh chỉ có nghĩa là bạn vượt qua các dữ liệu (v) không phải là mã riêng của mình.


Tương tự như vậy, các đối tượng bị bắt bằng cách tham khảo được tham chiếu vào struct:

Value v; 
auto lambda = [&](Args...) -> Return { /*... use v, captured by reference...*/ }; 

// roughly equivalent to: 
struct Temporary { 
    Value& v; // note: capture by reference is non-const 
    Temporary(Value& v_) : v(v_) {} 
    Return operator()(Args...) { /*... use v, captured by reference...*/ } 
} 
lambda(v); // instance of the struct 

Đó là khá nhiều tất cả khi nói đến lambdas mình (trừ vài chi tiết thực hiện Tôi ommitted, nhưng mà không liên quan để hiểu cách hoạt động).


std::function

std::function là một wrapper chung quanh bất kỳ loại functor (lambdas, chức năng độc lập/static/thành viên, các lớp functor giống như những người tôi đã giới thiệu, ...).

Nội bộ của std::function khá phức tạp vì chúng phải hỗ trợ tất cả các trường hợp đó. Tùy thuộc vào loại functor chính xác, điều này yêu cầu ít nhất dữ liệu sau (cung cấp hoặc thực hiện chi tiết triển khai):

  • Con trỏ đến một hàm độc lập/tĩnh.

Hoặc,

  • Một con trỏ tới một bản sao [xem ghi chú bên dưới] của functor (cấp phát động để cho phép bất kỳ loại functor, như bạn đúng lưu ý nó).
  • Một con trỏ tới hàm thành viên sẽ được gọi.
  • Một con trỏ đến một cấp phát có thể sao chép functor và chính nó (vì bất kỳ loại functor nào có thể được sử dụng, con trỏ tới functor phải là void* và do đó phải có cơ chế như vậy - có thể sử dụng polymorphism aka lớp cơ sở + phương pháp ảo, lớp dẫn xuất được tạo ra cục bộ trong các nhà xây dựng template<class Functor> function(Functor)).

Vì nó không biết trước loại functor nào sẽ phải lưu trữ (và điều này được thể hiện rõ ràng bởi thực tế là std::function có thể được giao lại), nó phải đối phó với tất cả các trường hợp có thể và đưa ra quyết định trong thời gian chạy.

Lưu ý: Tôi không biết nơi các nhiệm vụ chuẩn nó nhưng điều này chắc chắn là một bản sao mới, các functor cơ bản không được chia sẻ:

int v = 0; 
std::function<void()> f = [=]() mutable { std::cout << v++ << std::endl; }; 
std::function<void()> g = f; 

f(); // 0 
f(); // 1 
g(); // 0 
g(); // 1 

Vì vậy, khi bạn vượt qua một std::function xung quanh nó liên quan đến ít nhất bốn con trỏ (và thực sự trên GCC 4.7 64 bit sizeof(std::function<void()> là 32 là bốn con trỏ 64 bit) và tùy chọn bản sao được gán động của hàm functor (như tôi đã nói, chỉ chứa các đối tượng đã chụp, bạn không sao chép mã số).


trả lời cho câu hỏi

chi phí đi qua một lambda đến một chức năng như thế này là gì?[bối cảnh của câu hỏi: bởi giá trị]

Vâng, như bạn có thể nhìn thấy nó phụ thuộc chủ yếu vào functor của bạn (hoặc một làm bằng tay struct functor hoặc lambda) và các biến nó chứa. Giá trị trên không so với việc chuyển trực tiếp một hàm struct bằng giá trị khá không đáng kể, nhưng tất nhiên là cao hơn nhiều so với việc chuyển số struct functor bằng cách tham chiếu.

Tôi có nên đánh dấu từng đối tượng chức năng được chuyển qua const& sao cho bản sao không được tạo?

Tôi e rằng điều này rất khó trả lời theo cách chung chung. Đôi khi bạn sẽ muốn vượt qua tham chiếu const, đôi khi theo giá trị, đôi khi theo tham chiếu rvalue để bạn có thể di chuyển. Nó thực sự phụ thuộc vào ngữ nghĩa của mã của bạn.

Các quy tắc liên quan đến cái bạn nên chọn là IMO chủ đề hoàn toàn khác, chỉ cần nhớ rằng chúng giống như đối với bất kỳ đối tượng nào khác.

Dù sao, bây giờ bạn có tất cả các phím để đưa ra quyết định sáng suốt (một lần nữa, tùy thuộc vào mã của bạn và ngữ nghĩa của nó).

1

Nếu lambda có thể được thực hiện như một chức năng đơn giản (nghĩa là nó không nắm bắt bất cứ điều gì), sau đó nó được thực hiện chính xác theo cùng một cách. Đặc biệt là tiêu chuẩn yêu cầu nó phải tương thích với kiểu con trỏ-to-chức năng kiểu cũ với cùng chữ ký. [CHỈNH SỬA: không chính xác, hãy xem thảo luận trong nhận xét]

Đối với phần còn lại, tùy thuộc vào việc triển khai, nhưng tôi không lo lắng trước. Việc thực hiện đơn giản nhất không làm gì ngoài việc mang thông tin xung quanh. Chính xác như bạn đã yêu cầu trong quá trình chụp. Vì vậy, hiệu ứng sẽ giống như khi bạn đã tự tạo một lớp. Hoặc sử dụng một số biến thể std :: bind.

+0

"* Tiêu chuẩn yêu cầu nó để tương thích với các kiểu cũ trỏ-to-chức năng với cùng chữ ký * "=> quan tâm đến báo giá? Tôi không biết về điều đó (mà bản thân nó không đáng ngạc nhiên). – syam

+0

Nevermind, tìm thấy nó: 5.1.2-6 * Kiểu đóng cho biểu thức lambda không có lambda-capture có hàm không chuyển đổi const không rõ ràng công khai cho con trỏ tới hàm có cùng tham số và kiểu trả về như toán tử gọi hàm của loại đóng.Giá trị được trả về bởi hàm chuyển đổi này sẽ là địa chỉ của một hàm, khi được gọi, có cùng tác dụng như gọi toán tử gọi hàm của kiểu đóng. * ==> Một lambda không bắt giữ không bắt buộc phải được thực hiện như một độc lập chức năng, nó chỉ phải cung cấp một chức năng chuyển đổi. – syam

+0

hy vọng rằng không mâu thuẫn với những gì tôi đã nêu ;-) –

3

Xem thêm C++11 lambda implementation and memory model

Biểu thức lambda chỉ là: biểu thức. Sau khi biên dịch, nó kết quả trong một đối tượng đóng cửa khi chạy.

5.1.2 biểu thức Lambda [expr.prim.lambda]

Việc thẩm định kết quả lambda thể hiện trong một prvalue tạm (12,2). Tạm thời này được gọi là đối tượng đóng cửa.

Bản thân đối tượng được xác định thực hiện và có thể thay đổi từ trình biên dịch sang trình biên dịch.

Đây là việc thực hiện ban đầu của lambdas trong vang https://github.com/faisalv/clang-glambda

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