Câu hỏi Tôi có nên vượt qua một std :: chức năng bằng cách tham chiếu const?


Giả sử tôi có một hàm std::function:

void callFunction(std::function<void()> x)
{
    x();
}

Tôi có nên vượt qua x bằng tham chiếu const thay thế ?:

void callFunction(const std::function<void()>& x)
{
    x();
}

Câu trả lời cho câu hỏi này có thay đổi tùy theo chức năng của nó không? Ví dụ nếu nó là một hàm thành viên lớp hoặc hàm khởi tạo lưu trữ hoặc khởi tạo std::function vào biến thành viên.


110
2017-08-21 18:58


gốc


Chắc là không. Tôi không biết chắc chắn, nhưng tôi mong đợi sizeof(std::function) không được nhiều hơn 2 * sizeof(size_t), đó là kích thước nhỏ nhất mà bạn từng cân nhắc cho một tham chiếu const. - Mats Petersson
@Mats: Tôi không nghĩ rằng kích thước của std::function wrapper cũng quan trọng như sự phức tạp của việc sao chép nó. Nếu các bản sao sâu có liên quan, nó có thể đắt hơn nhiều so với sizeof gợi ý. - Ben Voigt
Bạn có nên move chức năng trong? - Yakk - Adam Nevraumont
operator()() Là const do đó, tham chiếu const sẽ hoạt động. Nhưng tôi chưa bao giờ sử dụng std :: function. - Neel Basu
@ Yakk Tôi chỉ cần vượt qua một lambda trực tiếp đến chức năng. - Sven Adbring


Các câu trả lời:


Nếu bạn muốn hiệu suất, vượt qua theo giá trị nếu bạn đang lưu trữ nó.

Giả sử bạn có một chức năng được gọi là "chạy điều này trong chuỗi giao diện người dùng".

std::future<void> run_in_ui_thread( std::function<void()> )

chạy một số mã trong chuỗi "ui", sau đó báo hiệu future khi hoàn thành. (Hữu ích trong các khung giao diện người dùng trong đó chuỗi giao diện người dùng là nơi bạn được cho là gây rối với các phần tử giao diện người dùng)

Chúng tôi có hai chữ ký chúng tôi đang xem xét:

std::future<void> run_in_ui_thread( std::function<void()> ) // (A)
std::future<void> run_in_ui_thread( std::function<void()> const& ) // (B)

Bây giờ, chúng tôi có khả năng sử dụng chúng như sau:

run_in_ui_thread( [=]{
  // code goes here
} ).wait();

mà sẽ tạo ra một đóng cửa vô danh (một lambda), xây dựng một std::function ra khỏi nó, chuyển nó đến run_in_ui_thread , sau đó đợi cho đến khi nó chạy xong trong chuỗi chính.

Trong trường hợp (A), std::function được xây dựng trực tiếp từ lambda của chúng tôi, sau đó được sử dụng trong run_in_ui_thread. Lambda là moved vào std::function, vì vậy bất kỳ trạng thái di động nào được thực hiện hiệu quả vào nó.

Trong trường hợp thứ hai, tạm thời std::function được tạo ra, lambda là moved vào nó, rồi tạm thời std::function được sử dụng bởi tham chiếu trong run_in_ui_thread.

Cho đến nay, rất tốt - hai người họ thực hiện giống hệt nhau. Ngoại trừ run_in_ui_thread sẽ tạo một bản sao đối số hàm của nó để gửi đến chuỗi ui để thực thi! (nó sẽ trở lại trước khi nó được thực hiện với nó, vì vậy nó không thể chỉ sử dụng một tham chiếu đến nó). Đối với trường hợp (A), chúng tôi chỉ đơn giản là move các std::function vào kho lưu trữ dài hạn của nó. Trong trường hợp (B), chúng tôi buộc phải sao chép std::function.

Cửa hàng đó đi qua giá trị tối ưu hơn. Nếu có bất kỳ khả năng nào bạn đang lưu trữ một bản sao của std::function, vượt qua theo giá trị. Nếu không, một trong hai cách tương đương với nhau: nhược điểm duy nhất của giá trị là nếu bạn đang dùng cùng một cồng kềnh std::function và có một phương thức phụ sau khi sử dụng phương thức đó. Chặn đó, một move sẽ hiệu quả như một const&.

Bây giờ, có một số khác biệt khác giữa hai yếu tố chủ yếu bắt đầu nếu chúng ta có trạng thái liên tục trong std::function.

Giả sử rằng std::function lưu trữ một số đối tượng với một operator() const, nhưng nó cũng có một số mutable dữ liệu thành viên mà nó sửa đổi (làm thế nào thô lỗ!).

bên trong std::function<> const& trường hợp, mutable các thành viên dữ liệu được sửa đổi sẽ lan truyền ra khỏi cuộc gọi hàm. bên trong std::function<> trường hợp, họ sẽ không.

Đây là một trường hợp góc tương đối kỳ lạ.

Bạn muốn điều trị std::function giống như bạn sẽ có thể có trọng lượng nặng, loại di chuyển rẻ tiền khác. Di chuyển là rẻ, sao chép có thể tốn kém.


60
2017-08-21 19:42



Lợi thế ngữ nghĩa của "truyền theo giá trị nếu bạn đang lưu trữ nó", như bạn nói, là bằng hợp đồng, hàm không thể giữ địa chỉ của đối số được truyền. Nhưng nó thực sự đúng là "Chặn đó, một động thái sẽ hiệu quả như một const &"? Tôi luôn thấy chi phí của một hoạt động sao chép cộng với chi phí của hoạt động di chuyển. Với đi ngang qua const& Tôi chỉ thấy chi phí của hoạt động sao chép. - ceztko
@ceztko Trong cả hai trường hợp (A) và (B), tạm thời std::function được tạo ra từ lambda. Trong (A), tạm thời được chia thành các đối số run_in_ui_thread. Trong (B) một tham chiếu đến tạm thời nói được chuyển đến run_in_ui_thread. Miễn là bạn std::functions được tạo ra từ lambdas như thời gian, mệnh đề đó nắm giữ. Đoạn trước đề cập đến trường hợp std::function vẫn tồn tại. Nếu chúng ta không lưu trữ, chỉ cần tạo từ một lambda, function const& và function có cùng chi phí chính xác. - Yakk - Adam Nevraumont
Ah tôi thấy! Điều này tất nhiên phụ thuộc vào những gì xảy ra bên ngoài run_in_ui_thread(). Chỉ có chữ ký để nói "Vượt qua tham chiếu, nhưng tôi sẽ không lưu địa chỉ"? - ceztko
@ceztko Không, không có. - Yakk - Adam Nevraumont


Nếu bạn lo lắng về hiệu suất và bạn không xác định được chức năng thành viên ảo thì có nhiều khả năng bạn không nên sử dụng std::function ở tất cả.

Làm cho loại functor một tham số mẫu cho phép tối ưu hóa lớn hơn std::function, bao gồm cả nội tuyến logic functor. Hiệu quả của những tối ưu hóa này có khả năng lớn hơn rất nhiều so với những lo ngại về bản sao-vs-indirection về cách vượt qua std::function.

Nhanh hơn:

template<typename Functor>
void callFunction(Functor&& x)
{
    x();
}

26
2017-08-21 19:11



Tôi không lo lắng về hiệu suất thực sự. Tôi chỉ nghĩ rằng sử dụng const-tài liệu tham khảo, nơi họ nên được sử dụng là thực tế phổ biến (dây và vectơ đến tâm trí). - Sven Adbring
@Ben: Tôi nghĩ rằng cách hippie thân thiện nhất hiện nay để thực hiện điều này là sử dụng std::forward<Functor>(x)();, để bảo toàn danh mục giá trị của hàm functor, vì nó là tham chiếu "phổ quát". Tuy nhiên, sẽ không tạo ra sự khác biệt trong 99% các trường hợp. - GManNickG
Tốt nhất, tôi không biết rằng điều này sẽ thừa nhận các hàm Lambda. - Emily L.
@Ben Voigt như vậy cho trường hợp của bạn, tôi sẽ gọi chức năng với một động thái? callFunction(std::move(myFunctor)); - arias_JC
@arias_JC: Nếu tham số là một lambda, nó đã là một rvalue. Nếu bạn có một lvalue, bạn có thể sử dụng std::move nếu bạn không còn cần nó theo bất kỳ cách nào khác, hoặc chuyển trực tiếp nếu bạn không muốn di chuyển ra khỏi đối tượng hiện có. Quy tắc thu hẹp tham chiếu đảm bảo rằng callFunction<T&>()có một tham số kiểu T&, không phải T&&. - Ben Voigt


Như thường lệ trong C ++ 11, đi qua giá trị / tham chiếu / tham chiếu const phụ thuộc vào những gì bạn làm với đối số của bạn. std::function không khác nhau.

Đi qua giá trị cho phép bạn di chuyển đối số vào một biến (thường là biến thành viên của một lớp):

struct Foo {
    Foo(Object o) : m_o(std::move(o)) {}

    Object m_o;
};

Khi bạn biết chức năng của bạn sẽ di chuyển đối số của nó, đây là giải pháp tốt nhất, theo cách này người dùng của bạn có thể kiểm soát cách họ gọi chức năng của bạn:

Foo f1{Object()};               // move the temporary, followed by a move in the constructor
Foo f2{some_object};            // copy the object, followed by a move in the constructor
Foo f3{std::move(some_object)}; // move the object, followed by a move in the constructor

Tôi tin rằng bạn đã biết ngữ nghĩa của (không) tham chiếu const nên tôi sẽ không tin vào vấn đề. Nếu bạn cần tôi để thêm giải thích thêm về điều này, chỉ cần hỏi và tôi sẽ cập nhật.


20
2017-08-21 19:16