Ưu điểm của việc sử dụng chuyển tiếp


437

Trong chuyển tiếp hoàn hảo, std::forwardđược sử dụng để chuyển đổi các tham chiếu rvalue có tên t1t2thành các tham chiếu rvalue không tên. Mục đích của việc đó là gì? Điều đó sẽ ảnh hưởng đến hàm được gọi như thế nào innernếu chúng ta rời khỏi t1& t2dưới dạng giá trị?

template <typename T1, typename T2>
void outer(T1&& t1, T2&& t2) 
{
    inner(std::forward<T1>(t1), std::forward<T2>(t2));
}

Câu trả lời:


788

Bạn phải hiểu vấn đề chuyển tiếp. Bạn có thể đọc toàn bộ vấn đề một cách chi tiết , nhưng tôi sẽ tóm tắt.

Về cơ bản, với biểu thức E(a, b, ... , c), chúng tôi muốn biểu thức f(a, b, ... , c)tương đương. Trong C ++ 03, điều này là không thể. Có nhiều cố gắng, nhưng tất cả đều thất bại trong một số vấn đề.


Đơn giản nhất là sử dụng tham chiếu lvalue:

template <typename A, typename B, typename C>
void f(A& a, B& b, C& c)
{
    E(a, b, c);
}

Nhưng điều này không thể xử lý các giá trị tạm thời: f(1, 2, 3);vì chúng không thể bị ràng buộc với tham chiếu giá trị.

Nỗ lực tiếp theo có thể là:

template <typename A, typename B, typename C>
void f(const A& a, const B& b, const C& c)
{
    E(a, b, c);
}

Mà sửa chữa vấn đề trên, nhưng flops flops. Bây giờ không cho phép Ecó các đối số không phải là const:

int i = 1, j = 2, k = 3;
void E(int&, int&, int&); f(i, j, k); // oops! E cannot modify these

Nỗ lực thứ ba chấp nhận tham chiếu const, nhưng sau đó const_castconst:

template <typename A, typename B, typename C>
void f(const A& a, const B& b, const C& c)
{
    E(const_cast<A&>(a), const_cast<B&>(b), const_cast<C&>(c));
}

Điều này chấp nhận tất cả các giá trị, có thể truyền vào tất cả các giá trị, nhưng có khả năng dẫn đến hành vi không xác định:

const int i = 1, j = 2, k = 3;
E(int&, int&, int&); f(i, j, k); // ouch! E can modify a const object!

Một giải pháp cuối cùng xử lý mọi thứ một cách chính xác ... với chi phí không thể duy trì. Bạn cung cấp quá tải f, với tất cả sự kết hợp của const và non-const:

template <typename A, typename B, typename C>
void f(A& a, B& b, C& c);

template <typename A, typename B, typename C>
void f(const A& a, B& b, C& c);

template <typename A, typename B, typename C>
void f(A& a, const B& b, C& c);

template <typename A, typename B, typename C>
void f(A& a, B& b, const C& c);

template <typename A, typename B, typename C>
void f(const A& a, const B& b, C& c);

template <typename A, typename B, typename C>
void f(const A& a, B& b, const C& c);

template <typename A, typename B, typename C>
void f(A& a, const B& b, const C& c);

template <typename A, typename B, typename C>
void f(const A& a, const B& b, const C& c);

Đối số N yêu cầu 2 kết hợp N , một cơn ác mộng. Chúng tôi muốn làm điều này tự động.

(Đây thực sự là những gì chúng tôi có được trình biên dịch để làm cho chúng tôi trong C ++ 11.)


Trong C ++ 11, chúng tôi có cơ hội sửa lỗi này. Một giải pháp sửa đổi các quy tắc khấu trừ mẫu trên các loại hiện có, nhưng điều này có khả năng phá vỡ rất nhiều mã. Vì vậy, chúng ta phải tìm một cách khác.

Giải pháp là thay vào đó sử dụng các tham chiếu rvalue mới được thêm vào ; chúng tôi có thể giới thiệu các quy tắc mới khi suy ra các loại tham chiếu giá trị và tạo ra bất kỳ kết quả mong muốn nào. Rốt cuộc, chúng ta không thể phá mã ngay bây giờ.

Nếu được cung cấp một tham chiếu đến một tham chiếu (tham chiếu ghi chú là một thuật ngữ bao gồm cả hai T&T&&), chúng tôi sử dụng quy tắc sau để tìm ra loại kết quả:

"[đã cho] một loại TR là tham chiếu đến loại T, một nỗ lực để tạo tham chiếu giá trị kiểu Lvue cho cv TR, tạo ra tham chiếu kiểu lvalue cho Típ, trong khi cố gắng tạo tham chiếu rvalue loại cv TRv tạo ra loại TR. "

Hoặc ở dạng bảng:

TR   R

T&   &  -> T&  // lvalue reference to cv TR -> lvalue reference to T
T&   && -> T&  // rvalue reference to cv TR -> TR (lvalue reference to T)
T&&  &  -> T&  // lvalue reference to cv TR -> lvalue reference to T
T&&  && -> T&& // rvalue reference to cv TR -> TR (rvalue reference to T)

Tiếp theo, với suy luận đối số mẫu: nếu đối số là giá trị A, chúng tôi cung cấp đối số mẫu với tham chiếu giá trị cho A. Mặt khác, chúng tôi suy luận bình thường. Điều này cung cấp cái gọi là tài liệu tham khảo phổ quát (thuật ngữ tham chiếu chuyển tiếp hiện là tài liệu tham khảo chính thức).

Tại sao điều này hữu ích? Bởi vì kết hợp chúng tôi duy trì khả năng theo dõi danh mục giá trị của một loại: nếu đó là giá trị, chúng tôi có tham số tham chiếu giá trị, nếu không, chúng tôi có tham số tham chiếu rvalue.

Trong mã:

template <typename T>
void deduce(T&& x); 

int i;
deduce(i); // deduce<int&>(int& &&) -> deduce<int&>(int&)
deduce(1); // deduce<int>(int&&)

Điều cuối cùng là "chuyển tiếp" danh mục giá trị của biến. Hãy ghi nhớ, một khi bên trong hàm, tham số có thể được truyền dưới dạng giá trị cho bất kỳ thứ gì:

void foo(int&);

template <typename T>
void deduce(T&& x)
{
    foo(x); // fine, foo can refer to x
}

deduce(1); // okay, foo operates on x which has a value of 1

Điêu đo không tôt. E cần phải có cùng loại giá trị mà chúng tôi có! Giải pháp là đây:

static_cast<T&&>(x);

Cái này làm gì Hãy xem xét chúng tôi bên trong deducechức năng và chúng tôi đã được thông qua một giá trị. Điều này có nghĩa Tlà một A&, và do đó, loại mục tiêu cho các diễn viên tĩnh là A& &&, hoặc chỉ A&. Vì xđã là một A&, chúng tôi không làm gì cả và chỉ còn lại một tham chiếu giá trị.

Khi chúng tôi đã được thông qua một rvalue, TA, vì vậy các loại mục tiêu cho các diễn viên tĩnh là A&&. Các kết quả đúc trong một biểu thức giá trị, không còn có thể được chuyển đến một tham chiếu giá trị . Chúng tôi đã duy trì loại giá trị của tham số.

Đặt những thứ này lại với nhau mang lại cho chúng ta "chuyển tiếp hoàn hảo":

template <typename A>
void f(A&& a)
{
    E(static_cast<A&&>(a)); 
}

Khi fnhận được một giá trị, Enhận được một giá trị. Khi fnhận được một giá trị, Enhận được một giá trị. Hoàn hảo.


Và tất nhiên, chúng tôi muốn thoát khỏi sự xấu xí. static_cast<T&&>là khó hiểu và kỳ lạ để nhớ; thay vào đó, hãy tạo một hàm tiện ích được gọi forward, thực hiện điều tương tự:

std::forward<A>(a);
// is the same as
static_cast<A&&>(a);

1
Sẽ không phải flà một chức năng, và không phải là một biểu thức?
Michael Foukarakis

1
Nỗ lực cuối cùng của bạn là không chính xác đối với tuyên bố vấn đề: Nó sẽ chuyển tiếp các giá trị const dưới dạng không const, do đó không chuyển tiếp gì cả. Cũng lưu ý rằng trong lần thử đầu tiên, ý const int ichí sẽ được chấp nhận: Ađược suy ra const int. Những thất bại là cho các giá trị chữ. Cũng lưu ý rằng đối với lệnh gọi deduced(1), x là int&&, không int(chuyển tiếp hoàn hảo không bao giờ tạo bản sao, như sẽ được thực hiện nếu xlà tham số theo giá trị). Chỉ đơn thuần Tint. Lý do xđánh giá một giá trị trong giao nhận là vì các tham chiếu rvalue có tên trở thành biểu thức giá trị.
Julian Schaub - litb

5
Có sự khác biệt trong việc sử dụng forwardhoặc moveở đây? Hay đó chỉ là một sự khác biệt về ngữ nghĩa?
0x499602D2

28
@David: std::movenên được gọi mà không có đối số mẫu rõ ràng và luôn dẫn đến một giá trị, trong khi std::forwardcó thể kết thúc bằng một trong hai. Sử dụng std::movekhi bạn biết bạn không còn cần giá trị và muốn di chuyển nó đi nơi khác, sử dụng std::forwardđể làm điều đó theo các giá trị được truyền cho mẫu hàm của bạn.
GManNickG

5
Cảm ơn bạn đã bắt đầu với các ví dụ cụ thể đầu tiên và thúc đẩy vấn đề; rất hữu ích
ShreevatsaR

60

Tôi nghĩ rằng để có một mã khái niệm triển khai std :: Forward có thể thêm vào cuộc thảo luận. Đây là một slide trong bài nói chuyện của Scott Meyers Một Sampler C ++ 11/14 hiệu quả

mã khái niệm thực hiện std :: về phía trước

Chức năng movetrong mã là std::move. Có một triển khai (làm việc) cho nó sớm hơn trong cuộc nói chuyện đó. Tôi thấy thực hiện thực tế của std :: Forward trong libstdc ++ , trong tệp move.h, nhưng nó hoàn toàn không phải là hướng dẫn.

Từ quan điểm của người dùng, ý nghĩa của nó std::forwardlà một diễn viên có điều kiện thành một giá trị. Nó có thể hữu ích nếu tôi đang viết một hàm dự kiến ​​giá trị hoặc giá trị trong một tham số và muốn chuyển nó sang hàm khác dưới dạng giá trị chỉ khi nó được truyền vào dưới dạng giá trị. Nếu tôi không gói tham số trong std :: Forward, nó sẽ luôn được chuyển như một tham chiếu bình thường.

#include <iostream>
#include <string>
#include <utility>

void overloaded_function(std::string& param) {
  std::cout << "std::string& version" << std::endl;
}
void overloaded_function(std::string&& param) {
  std::cout << "std::string&& version" << std::endl;
}

template<typename T>
void pass_through(T&& param) {
  overloaded_function(std::forward<T>(param));
}

int main() {
  std::string pes;
  pass_through(pes);
  pass_through(std::move(pes));
}

Chắc chắn, nó in

std::string& version
std::string&& version

Mã này được dựa trên một ví dụ từ cuộc nói chuyện đã đề cập trước đó. Trượt 10, vào khoảng 15:00 từ lúc bắt đầu.


2
Liên kết thứ hai của bạn đã kết thúc chỉ ra một nơi hoàn toàn khác nhau.
Pharap

32

Trong chuyển tiếp hoàn hảo, std :: Forward được sử dụng để chuyển đổi tham chiếu rvalue có tên t1 và t2 thành tham chiếu rvalue không tên. Mục đích của việc đó là gì? Điều đó sẽ ảnh hưởng đến hàm được gọi bên trong như thế nào nếu chúng ta để t1 & t2 làm giá trị?

template <typename T1, typename T2> void outer(T1&& t1, T2&& t2) 
{
    inner(std::forward<T1>(t1), std::forward<T2>(t2));
}

Nếu bạn sử dụng một tham chiếu rvalue có tên trong một biểu thức thì nó thực sự là một giá trị (vì bạn tham chiếu đến đối tượng theo tên). Hãy xem xét ví dụ sau:

void inner(int &,  int &);  // #1
void inner(int &&, int &&); // #2

Bây giờ, nếu chúng ta gọi outernhư thế này

outer(17,29);

chúng tôi muốn 17 và 29 được chuyển tiếp đến # 2 vì 17 và 29 là số nguyên và các giá trị như vậy. Nhưng vì t1t2trong biểu thức inner(t1,t2);là giá trị, bạn sẽ gọi # 1 thay vì # 2. Đó là lý do tại sao chúng ta cần biến các tài liệu tham khảo trở lại thành các tài liệu tham khảo không tên std::forward. Vì vậy, t1trong outerluôn luôn là một biểu thức giá trị trong khi forward<T1>(t1)có thể là một biểu thức giá trị tùy thuộc vào T1. Cái sau chỉ là một biểu thức lvalue nếu T1là một tham chiếu lvalue. Và T1chỉ được suy luận là một tham chiếu giá trị trong trường hợp đối số đầu tiên bên ngoài là một biểu thức giá trị.


Đây là một loại giải thích xuống nước, nhưng một giải thích rất tốt và chức năng. Mọi người nên đọc câu trả lời này trước và sau đó đi sâu hơn nếu muốn
NicoBerrog lỗi

11

Điều đó sẽ ảnh hưởng đến hàm được gọi bên trong như thế nào nếu chúng ta để t1 & t2 làm giá trị?

Nếu, sau khi khởi tạo, T1là loại charT2là của một lớp, bạn muốn vượt qua t1mỗi bản sao và t2mỗi consttham chiếu. Chà, trừ khi inner()lấy chúng cho mỗi người khôngconst tham khảo, nghĩa là, trong trường hợp bạn cũng muốn làm như vậy.

Cố gắng viết một tập hợp các outer()hàm thực hiện điều này mà không cần tham chiếu giá trị, suy ra đúng cách để truyền các đối số từinner() kiểu của. Tôi nghĩ rằng bạn sẽ cần một cái gì đó 2 ^ 2 trong số chúng, các công cụ meta-template khá lớn để suy luận các đối số và rất nhiều thời gian để làm điều này đúng cho mọi trường hợp.

Và sau đó ai đó đi kèm với một inner()đối số có đối số trên mỗi con trỏ. Tôi nghĩ rằng bây giờ làm cho 3 ^ 2. (Hoặc 4 ^ 2. Chết tiệt, tôi không thể bận tâm suy nghĩ liệu constcon trỏ có tạo ra sự khác biệt hay không.)

Và sau đó tưởng tượng bạn muốn làm điều này cho năm tham số. Hoặc bảy.

Bây giờ bạn biết tại sao một số bộ óc thông minh nghĩ ra "chuyển tiếp hoàn hảo": Nó làm cho trình biên dịch làm tất cả điều này cho bạn.


4

Một điểm chưa được làm rõ ràng là static_cast<T&&>xử lý const T&đúng cách.
Chương trình:

#include <iostream>

using namespace std;

void g(const int&)
{
    cout << "const int&\n";
}

void g(int&)
{
    cout << "int&\n";
}

void g(int&&)
{
    cout << "int&&\n";
}

template <typename T>
void f(T&& a)
{
    g(static_cast<T&&>(a));
}

int main()
{
    cout << "f(1)\n";
    f(1);
    int a = 2;
    cout << "f(a)\n";
    f(a);
    const int b = 3;
    cout << "f(const b)\n";
    f(b);
    cout << "f(a * b)\n";
    f(a * b);
}

Sản xuất:

f(1)
int&&
f(a)
int&
f(const b)
const int&
f(a * b)
int&&

Lưu ý rằng 'f' phải là một hàm mẫu. Nếu nó chỉ được định nghĩa là 'void f (int && a)' thì điều này không hoạt động.


điểm tốt, vì vậy T && trong diễn viên tĩnh cũng tuân theo quy tắc thu gọn tham chiếu, phải không?
barney

2

Có thể đáng để nhấn mạnh rằng chuyển tiếp phải được sử dụng song song với một phương pháp bên ngoài với chuyển tiếp / tham chiếu phổ quát. Sử dụng chuyển tiếp của chính nó như các tuyên bố sau đây được cho phép, nhưng không tốt hơn là gây nhầm lẫn. Ủy ban tiêu chuẩn có thể muốn vô hiệu hóa tính linh hoạt như vậy nếu không tại sao chúng ta không sử dụng static_cast thay thế?

     std::forward<int>(1);
     std::forward<std::string>("Hello");

Theo tôi, di chuyển và chuyển tiếp là các mẫu thiết kế là kết quả tự nhiên sau khi loại tham chiếu giá trị r được giới thiệu. Chúng ta không nên đặt tên cho một phương thức giả sử nó được sử dụng đúng trừ khi việc sử dụng không chính xác bị cấm.


Tôi không nghĩ rằng ủy ban C ++ cảm thấy trách nhiệm của họ là sử dụng thành ngữ ngôn ngữ "chính xác", thậm chí không định nghĩa cách sử dụng "chính xác" là gì (mặc dù họ chắc chắn có thể đưa ra hướng dẫn). Cuối cùng, trong khi giáo viên, sếp và bạn bè của một người có thể có nhiệm vụ chỉ đạo họ bằng cách này hay cách khác, tôi tin rằng ủy ban C ++ (và do đó, tiêu chuẩn) không có nhiệm vụ đó.
SirGuy

Vâng, tôi vừa đọc N2951 và tôi đồng ý rằng ủy ban tiêu chuẩn không có nghĩa vụ phải thêm các hạn chế không cần thiết liên quan đến việc sử dụng chức năng. Nhưng tên của hai mẫu hàm này (di chuyển và chuyển tiếp) thực sự hơi khó hiểu khi chỉ nhìn thấy các định nghĩa của chúng trong tệp thư viện hoặc tài liệu chuẩn (23.2.5 Trợ giúp chuyển tiếp / di chuyển). Các ví dụ trong tiêu chuẩn chắc chắn giúp hiểu khái niệm này, nhưng có thể hữu ích khi thêm nhiều nhận xét để làm cho mọi thứ rõ ràng hơn một chút.
colin
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.