Tôi có nên vượt qua hàm std :: bằng const-Reference không?


141

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

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

Tôi có nên vượt qua xtham chiếu const thay thế?:

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

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


1
Chắc là không. Tôi không biết chắc chắn, nhưng tôi sẽ mong đợi sizeof(std::function)không nhiều hơn 2 * sizeof(size_t), đó là kích thước nhỏ nhất mà bạn từng xem xét để tham khảo const.
Mats Petersson

12
@Mats: Tôi không nghĩ kích thước của std::functiontrình bao bọc 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 sizeofgợi ý.
Ben Voigt

Bạn có nên movechức năng trong?
Yakk - Adam Nevraumont

operator()()constmột tài liệu tham khảo const nên làm việc. Nhưng tôi chưa bao giờ sử dụng chức năng std ::.
Neel Basu

@Yakk Tôi chỉ cần truyền lambda trực tiếp vào hàm.
Sven Adbring

Câu trả lời:


79

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

Giả sử bạn có một hàm gọi là "chạy cái này trong luồng UI".

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

chạy một số mã trong luồng "ui", sau đó báo hiệu futurekhi hoàn thành. (Hữu ích trong các khung UI, trong đó luồng UI là nơi bạn có nghĩa vụ phải làm rối với các thành phần UI)

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ó thể sử dụng những điều này như sau:

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

cái này sẽ tạo ra một bao đóng ẩn danh (lambda), xây dựng std::functionnó, chuyển nó đến run_in_ui_threadhàm, sau đó đợi nó chạy xong trong luồng 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 chuyển nào được thực hiện một cách hiệu quả vào nó.

Trong trường hợp thứ hai, một tạm thời std::functionđược tạo ra, lambda được moveđưa vào đó, sau đó 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ừ việc run_in_ui_threadsẽ tạo một bản sao của đối số chức năng của nó để gửi đến luồng 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à movecác std::functionthành lưu trữ lâu dài 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 đó làm cho việc đ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 giá trị. Mặt khác, một trong hai cách là tương đương nhau: nhược điểm duy nhất của giá trị phụ là nếu bạn đang sử dụng cùng std::functionmột phương pháp cồng kềnh và có một phương thức phụ sau khi sử dụng phương pháp khác. Chặn rằng, a movesẽ hiệu quả như a const&.

Bây giờ, có một số khác biệt khác giữa hai chủ yếu là đá nếu chúng ta có trạng thái dai dẳng trong std::function.

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

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

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

Bạn muốn đối xử std::functionnhư bạn sẽ làm với bất kỳ loại di chuyển nặng, có thể nặng khác. Di chuyển là rẻ, sao chép có thể tốn kém.


Lợi thế về ngữ nghĩa của "vượt qua 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 thông qua. Nhưng có thực sự đúng là "Chặn rằng, 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 việc đi ngang qua const&tôi chỉ thấy chi phí của hoạt động sao chép.
ceztko

2
@ceztko Trong cả hai trường hợp (A) và (B), tạm thời std::functionđược tạo từ lambda. Trong (A), tạm thời được đưa vào đối số để run_in_ui_thread. Trong (B) một tham chiếu để nói tạm thời được chuyển đến run_in_ui_thread. Chừng nào std::functions của bạn được tạo ra từ lambdas như tạm thời, mệnh đề đó còn tồn tại. Đoạn trước đề cập đến trường hợp std::functionvẫn tồn tại. Nếu chúng tôi không lưu trữ, chỉ cần tạo từ lambda function const&functioncó 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(). Có phải chỉ có một chữ ký để nói "Chuyển 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

1
@ Yakk-AdamNevraumont nếu có thể hoàn thiện hơn để đưa ra một lựa chọn khác để vượt qua giới thiệu rvalue:std::future<void> run_in_ui_thread( std::function<void()>&& )
Pavel P

33

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

Việc tạo functor thành một tham số mẫu cho phép tối ưu hóa tốt 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 vượt xa các mối quan tâm về sao chép và so sánh về cách vượt qua std::function.

Nhanh hơn:

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

1
Tôi thực sự không lo lắng về hiệu suất. Tôi chỉ nghĩ rằng việc sử dụng các tham chiếu const trong đó chúng nên được sử dụng là thông lệ (chuỗi và vectơ xuất hiện trong tâm trí).
Sven Adbring

13
@Ben: Tôi nghĩ rằng cách thức thân thiện với người hippie hiện đại nhất này là sử dụng std::forward<Functor>(x)();, để duy trì danh mục giá trị của functor, vì đó là một tài liệu tham khảo "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

1
@Ben Voigt vậy đối với trường hợp của bạn, tôi sẽ gọi hàm bằng cách di chuyển chứ? callFunction(std::move(myFunctor));
arias_JC

2
@arias_JC: Nếu tham số là lambda, thì đó là giá trị. Nếu bạn có một giá trị, bạn có thể sử dụng std::movenế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 rời khỏi đối tượng hiện có. Quy tắc thu gọn tham chiếu đảm bảo rằng callFunction<T&>()có một tham số loại T&, không T&&.
Ben Voigt

1
@BoltzmannBrain: Tôi đã chọn không thực hiện thay đổi đó vì nó chỉ hợp lệ cho trường hợp đơn giản nhất, khi chức năng chỉ được gọi một lần. Câu trả lời của tôi là câu hỏi "làm thế nào tôi nên vượt qua một đối tượng chức năng?" và không giới hạn ở một chức năng không làm gì ngoài việc gọi vô điều kiện functor đó chính xác một lần.
Ben Voigt

25

Như thường lệ trong C ++ 11, chuyển 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 mình. std::functionkhông khác

Truyền theo giá trị cho phép bạn di chuyển đối số thành 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 mình 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 quan điểm. 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.

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.