Sự khác biệt giữa std :: reference_wrapper và con trỏ đơn giản?


99

Tại sao cần phải có std::reference_wrapper? Nó nên được sử dụng ở đâu? Nó khác với một con trỏ đơn giản như thế nào? Hiệu suất của nó như thế nào so với một con trỏ đơn giản?


4
Về cơ bản, nó là một con trỏ mà bạn sử dụng .thay vì->
MM

5
@MM Không, việc sử dụng .không hoạt động theo cách bạn đề xuất (trừ khi tại một số thời điểm, đề xuất chấm toán tử được thông qua và tích hợp :))
Columbo

3
Chính những câu hỏi như thế này khiến tôi không hài lòng khi phải làm việc với C ++ mới.
Nils

Để theo dõi Columbo, std :: reference_wrapper được sử dụng với get()hàm thành viên của nó hoặc với chuyển đổi ngầm của nó trở lại kiểu cơ bản.
Max Barraclough

Câu trả lời:


88

std::reference_wrapperhữu ích khi kết hợp với các mẫu. Nó bao bọc một đối tượng bằng cách lưu trữ một con trỏ tới nó, cho phép gán lại và sao chép trong khi bắt chước ngữ nghĩa thông thường của nó. Nó cũng hướng dẫn các mẫu thư viện nhất định để lưu trữ các tham chiếu thay vì các đối tượng.

Hãy xem xét các thuật toán trong STL sao chép bộ chức năng: Bạn có thể tránh sao chép đó bằng cách chỉ cần chuyển một trình bao bọc tham chiếu tham chiếu đến bộ chức năng thay vì chính bộ chức năng:

unsigned arr[10];
std::mt19937 myEngine;
std::generate_n( arr, 10, std::ref(myEngine) ); // Modifies myEngine's state

Điều này hoạt động vì…

  • ... reference_wrappers quá tảioperator() vì vậy họ có thể được gọi giống như chức năng các đối tượng họ đề cập đến:

    std::ref(myEngine)() // Valid expression, modifies myEngines state
  • … (Un) giống như các tham chiếu thông thường, sao chép (và gán) reference_wrapperschỉ chỉ định người được chỉ định.

    int i, j;
    auto r = std::ref(i); // r refers to i
    r = std::ref(j); // Okay; r refers to j
    r = std::cref(j); // Error: Cannot bind reference_wrapper<int> to <const int>
    

Việc sao chép một trình bao bọc tham chiếu thực tế tương đương với việc sao chép một con trỏ, nó rẻ như cho. Tất cả các lệnh gọi hàm vốn có trong việc sử dụng nó (ví dụ: các lệnh gọi đến operator()) chỉ nên được nội dòng vì chúng là một chữ lót.

reference_wrappers được tạo qua std::refstd::cref :

int i;
auto r = std::ref(i); // r is of type std::reference_wrapper<int>
auto r2 = std::cref(i); // r is of type std::reference_wrapper<const int>

Đối số mẫu chỉ định loại và trình độ cv của đối tượng được tham chiếu đến; r2đề cập đến một const intvà sẽ chỉ mang lại một tham chiếu đến const int. Các lệnh gọi đến các trình bao bọc tham chiếu với các chức năng consttrong đó sẽ chỉ gọi consthàm thành viên operator()s.

Các trình khởi tạo Rvalue không được phép, vì cho phép chúng sẽ gây hại nhiều hơn lợi. Vì các giá trị sẽ được di chuyển bằng mọi cách (và với việc tách bản sao được đảm bảo ngay cả khi điều đó được tránh một phần), chúng tôi không cải thiện ngữ nghĩa; chúng ta có thể giới thiệu các con trỏ treo lơ lửng, vì một trình bao bọc tham chiếu không kéo dài thời gian tồn tại của con trỏ.

Thư viện tương tác

Như đã đề cập trước đây, người ta có thể hướng dẫn make_tuplelưu trữ một tham chiếu trong kết quả tuplebằng cách chuyển đối số tương ứng thông qua reference_wrapper:

int i;
auto t1 = std::make_tuple(i); // Copies i. Type of t1 is tuple<int>
auto t2 = std::make_tuple(std::ref(i)); // Saves a reference to i.
                                        // Type of t2 is tuple<int&>

Lưu ý rằng điều này hơi khác với forward_as_tuple: Ở đây, không cho phép các giá trị làm đối số.

std::bindhiển thị cùng một hành vi: Nó sẽ không sao chép đối số nhưng lưu trữ một tham chiếu nếu nó là một reference_wrapper. Hữu ích nếu đối số đó (hoặc functor!) Không cần được sao chép nhưng vẫn ở trong phạm vi trong khi bind-functor được sử dụng.

Sự khác biệt so với con trỏ thông thường

  • Không có cấp độ bổ sung của hướng cú pháp. Con trỏ phải được tham chiếu để có được giá trị cho đối tượng mà chúng tham chiếu đến; reference_wrappers có một toán tử chuyển đổi ngầm định và có thể được gọi giống như đối tượng mà chúng bao bọc.

    int i;
    int& ref = std::ref(i); // Okay
    
  • reference_wrappers, không giống như con trỏ, không có trạng thái null. Chúng phải được khởi tạo bằng tham chiếu hoặc tham chiếu khácreference_wrapper .

    std::reference_wrapper<int> r; // Invalid
  • Một điểm tương đồng là ngữ nghĩa sao chép nông: Các con trỏ và reference_wrappers có thể được gán lại.


std::make_tuple(std::ref(i));vượt trội hơn std::make_tuple(&i);theo một cách nào đó?
Laurynas Lazauskas

6
@LaurynasLazauskas Nó khác. Cái sau mà bạn đã hiển thị lưu một con trỏ tới i, không phải là một tham chiếu đến nó.
Columbo

Hm ... Tôi đoán tôi vẫn không thể phân biệt được hai điều này cũng như tôi muốn ... Vâng, cảm ơn bạn.
Laurynas Lazauskas

@Columbo Làm cách nào để có thể sử dụng một mảng trình bao bọc tham chiếu nếu chúng không có trạng thái rỗng? Không phải mảng thường bắt đầu với tất cả các phần tử được đặt ở trạng thái null?
anatolyg

2
@anatolyg Điều gì cản trở bạn khởi tạo mảng đó?
Columbo

27

Có ít nhất hai mục đích thúc đẩy std::reference_wrapper<T>:

  1. Nó là để cung cấp ngữ nghĩa tham chiếu cho các đối tượng được truyền dưới dạng tham số giá trị cho các mẫu hàm. Ví dụ: bạn có thể có một đối tượng hàm lớn mà bạn muốn chuyển đến std::for_each(), đối tượng này nhận tham số đối tượng hàm theo giá trị. Để tránh sao chép đối tượng, bạn có thể sử dụng

    std::for_each(begin, end, std::ref(fun));

    Việc truyền các đối số std::reference_wrapper<T>cho một std::bind()biểu thức là khá phổ biến để ràng buộc các đối số bằng tham chiếu thay vì theo giá trị.

  2. Khi sử dụng một std::reference_wrapper<T>với std::make_tuple()phần tử tuple tương ứng sẽ trở thành một T&thay vì một T:

    T object;
    f(std::make_tuple(1, std::ref(object)));
    

Bạn có thể vui lòng đưa ra một ví dụ mã cho trường hợp đầu tiên?
user1708860

1
@ user1708860: ý bạn là khác với cái đã cho ...?
Dietmar Kühl

Ý tôi là mã thực tế mà đi với std :: ref (vui vẻ), vì tôi không hiểu làm thế nào được sử dụng (trừ khi niềm vui là một đối tượng và không phải là một chức năng ...)
user1708860

2
@ user1708860: vâng, rất có thể funlà một đối tượng hàm (tức là một đối tượng của một lớp có toán tử gọi hàm) chứ không phải một hàm: nếu funlà một hàm thực, std::ref(fun)không có mục đích và làm cho mã có khả năng chậm hơn.
Dietmar Kühl

23

Một sự khác biệt khác, về mã tự lập tài liệu, đó là việc sử dụng reference_wrappervề cơ bản từ chối quyền sở hữu đối tượng. Ngược lại, một unique_ptrxác nhận quyền sở hữu, trong khi một con trỏ trần có thể có hoặc có thể không được sở hữu (không thể biết nếu không xem nhiều mã liên quan):

vector<int*> a;                    // the int values might or might not be owned
vector<unique_ptr<int>> b;         // the int values are definitely owned
vector<reference_wrapper<int>> c;  // the int values are definitely not owned

3
trừ khi đó là mã trước c ++ 11, ví dụ đầu tiên phải ngụ ý các giá trị tùy chọn, chưa biết, ví dụ: tra cứu bộ đệm dựa trên chỉ mục. Nó sẽ được tốt đẹp nếu std cung cấp cho chúng ta một cái gì đó tiêu chuẩn để đại diện cho một tổ chức phi-null, giá trị sở hữu (unique & biến thể chia sẻ)
Bwmat

Nó có thể không quan trọng bằng C ++ 11, nơi mà các con trỏ trống gần như luôn luôn là các giá trị mượn.
Elling

reference_wrappervượt trội hơn so với con trỏ thô không chỉ bởi vì rõ ràng rằng nó là không sở hữu, mà còn bởi vì nó không thể nullptr(không có tai quái) và do đó người dùng biết họ không thể vượt qua nullptr(không có tai quái) và bạn biết bạn không cần phải kiểm tra nó.
underscore_d

19

Bạn có thể coi nó như một trình bao bọc tiện lợi xung quanh các tham chiếu để bạn có thể sử dụng chúng trong các vùng chứa.

std::vector<std::reference_wrapper<T>> vec; // OK - does what you want
std::vector<T&> vec2; // Nope! Will not compile

Về cơ bản nó là một CopyAssignablephiên bản của T&. Bất cứ lúc nào bạn muốn một tham chiếu, nhưng nó phải được gán, sử dụng std::reference_wrapper<T>hoặc chức năng trợ giúp của nó std::ref(). Hoặc sử dụng một con trỏ.


Những điều kỳ quặc khác sizeof::

sizeof(std::reference_wrapper<T>) == sizeof(T*) // so 8 on a 64-bit box
sizeof(T&) == sizeof(T) // so, e.g., sizeof(vector<int>&) == 24

Và so sánh:

int i = 42;
assert(std::ref(i) == std::ref(i)); // ok

std::string s = "hello";
assert(std::ref(s) == std::ref(s)); // compile error

1
@LaurynasLazauskas Một người có thể gọi trực tiếp các đối tượng hàm chứa trong trình bao bọc. Điều đó cũng được giải thích trong câu trả lời của tôi.
Columbo

2
Vì triển khai tham chiếu chỉ là một con trỏ bên trong nên tôi không thể hiểu tại sao trình bao bọc lại thêm bất kỳ hình phạt chuyển hướng hoặc hiệu suất nào
Riga

4
Nó không nên là một hướng dẫn hơn là một tham chiếu đơn giản khi nói đến mã phát hành
Riga

3
Tôi mong đợi trình biên dịch nội tuyến reference_wrappermã tầm thường , làm cho nó giống hệt với mã sử dụng con trỏ hoặc tham chiếu.
David Stone

4
@LaurynasLazauskas: std::reference_wrappercó đảm bảo rằng đối tượng không bao giờ rỗng. Xem xét một thành viên trong lớp std::vector<T *>. Bạn phải kiểm tra tất cả mã lớp để xem liệu đối tượng này có thể lưu trữ a nullptrtrong vector hay không, trong khi với std::reference_wrapper<T>, bạn được đảm bảo có các đối tượng hợp lệ.
David Stone,
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.