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ó).
Tốt câu hỏi, cũng muốn biết :) – Xaqq