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


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 tên t1 và t2 để tham chiếu rvalue chưa đặt tên. Mục đích của việc đó là gì? Điều đó ảnh hưởng đến hàm được gọi như thế nào inner nếu chúng ta rời đi t1 & t2 như lvalues?

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

341
2017-08-27 06:59


gốc




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


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

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


Cách đơ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 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 lvalue.

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 flips flops. Nó bây giờ không cho phép E để có đối số không 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_castlà của const xa:

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ể chuyển 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 của f, với tất cả các sự kết hợp của const và không 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);

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

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


Trong C ++ 11, chúng ta 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 mới được thêm vào tham chiếu rvalue; chúng tôi có thể giới thiệu các quy tắc mới khi loại bỏ các loại tham chiếu rvalue và tạo ra bất kỳ kết quả mong muốn nào. Sau khi tất cả, chúng tôi không thể có thể phá vỡ mã ngay bây giờ.

Nếu được tham chiếu đến tham chiếu (tham chiếu ghi chú là một thuật ngữ bao hàm có nghĩa là cả hai T& và T&&), chúng tôi sử dụng quy tắc sau đây để tìm ra loại kết quả:

"[đưa ra] một kiểu TR là tham chiếu đến kiểu T, một nỗ lực để tạo kiểu" tham chiếu lvalue tới cv TR "tạo kiểu" tham chiếu lvalue tới T ", trong khi cố gắng tạo kiểu" tham chiếu rvalue cv TR ”tạo kiểu 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 trích dẫn đối số mẫu: nếu một đối số là một giá trị A, chúng tôi cung cấp đối số mẫu với tham chiếu giá trị bằng không A. Nếu không, chúng tôi sẽ suy ra bình thường. Điều này cho phé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 giờ là người 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à một giá trị, chúng tôi có tham số tham chiếu giá trị bằng lvalue, nếu không chúng tôi có thông số tham chiếu giá trị.

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 chuyển như một 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 một loại danh mục giá trị mà chúng tôi có! Giải pháp này là:

static_cast<T&&>(x);

Điều này làm gì? Hãy xem xét chúng ta đang ở trong deduce và chúng tôi đã được thông qua một giá trị. Điều này có nghĩa là T là một A&và do đó loại mục tiêu cho dàn 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à được để lại với một tham chiếu lvalue.

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

Đặt chúng lại với nhau 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 nào f nhận được một giá trị, E nhận được một giá trị. Khi nào f nhận được một giá trị, E được một giá trị. Hoàn hảo.


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

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

643
2017-08-27 07:59



Sẽ không f là một hàm, và không phải là một biểu thức? - Michael Foukarakis
Nỗ lực cuối cùng của bạn không chính xác đối với câu lệnh vấn đề: Nó sẽ chuyển tiếp các giá trị const như non-const, do đó không chuyển tiếp. Cũng lưu ý rằng trong lần thử đầu tiên, const int i sẽ được chấp nhận: A được suy ra const int. Những thất bại là cho các giá trị rvalues ​​literal. Cũng lưu ý rằng để gọi đến deduced(1), x là int&&, không phải 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 x sẽ là tham số theo giá trị). Merely T Là int. Lý do mà x việc đánh giá một giá trị trong giao nhận là bởi vì các tham chiếu rvalue được đặt tên trở thành các biểu thức giá trị lvalue. - Johannes Schaub - litb
Có sự khác biệt nào trong việc sử dụng không forward hoặc là move đây? Hay nó chỉ là sự khác biệt ngữ nghĩa? - 0x499602D2
@David: std::move nê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::forward có thể kết thúc như 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ó ở nơi khác, hãy sử dụng std::forward để làm điều đó theo các giá trị được truyền cho mẫu chức năng của bạn. - GManNickG
Cảm ơn bạn đã bắt đầu với các ví dụ cụ thể trước và thúc đẩy vấn đề; rất hữu ích! - ShreevatsaR


Tôi nghĩ rằng để có một mã khái niệm thực hiện std :: forward có thể thêm vào các cuộc thảo luận. Đây là một slide từ Scott Meyers nói chuyện Bộ lấy mẫu C ++ 11/14 hiệu quả

conceptual code implementing std::forward

Chức năng move trong mã là std::move. Có một (làm việc) thực hiện cho nó trước đó trong cuộc nói chuyện đó. tôi đã tìm thấy thực hiện thực tế của std :: forward trong libstdc ++, trong tập tin move.h, nhưng nó không phải là ở tất cả các instructive.

Từ quan điểm của người dùng, ý nghĩa của nó là std::forward là một dàn diễn viên có điều kiện đối với một rvalue. Nó có thể hữu ích nếu tôi đang viết một hàm mà dự kiến ​​hoặc là một giá trị hoặc rvalue trong một tham số và muốn chuyển nó tới hàm khác như một giá trị chỉ khi nó được truyền vào như một giá trị. Nếu tôi không bọc tham số trong std :: forward, nó sẽ luôn được truyề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 dựa trên một ví dụ từ bài nói chuyện đã đề cập trước đó. Slide 10, vào khoảng 15:00 ngay từ đầu.


37
2018-05-01 15:44



Liên kết thứ hai của bạn đã kết thúc ở một nơi nào đó hoàn toàn khác. - Pharap


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 chưa đặt tên. Mục đích của việc đó là gì? Làm thế nào điều đó sẽ tác động đến hàm được gọi bên trong nếu chúng ta để lại t1 & t2 là lvalue?

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, nó thực sự là một giá trị (vì bạn tham chiếu đến đối tượng theo tên). 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 outer như 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à các số nguyên nguyên và các giá trị như vậy. Nhưng kể từ khi t1 và t2 trong biểu thức inner(t1,t2); là lvalues, bạn sẽ gọi số 1 thay vì # 2. Đó là lý do tại sao chúng tôi cần phải chuyển các tham chiếu trở lại thành các tham chiếu chưa được đặt tên với std::forward. Vì thế, t1 trong outer luô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 rvalue tùy thuộc vào T1. Cái sau chỉ là một biểu thức giá trị nếu T1 là một tham chiếu lvalue. Và T1chỉ được suy luận là một tham chiếu lvalue trong trường hợp đối số đầu tiên bên ngoài là một biểu thức lvalue.


15
2017-08-29 20:34



Cuối cùng, giải thích rõ ràng :) - TT_
Đây là một loại giải thích được tưới nước, nhưng một lời giải thích rất tốt và được thực hiện. 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 - NicoBerrogorry


Làm thế nào mà sẽ ảnh hưởng đến chức năng được gọi là bên trong nếu chúng ta để lại t1 & t2 như lvalue?

Nếu, sau khi instantiating, T1 thuộc loại charT2 là một lớp học, bạn muốn vượt qua t1 mỗi bản sao và t2 mỗi const tài liệu tham khảo. Vâng, trừ khi inner() lấy chúng cho mỗi phiconst tham chiếu, nghĩa là, trong trường hợp này bạn cũng muốn làm như vậy.

Cố gắng viết một bộ outer() các hàm thực hiện điều này mà không có tham chiếu rvalue, suy luận đú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ố họ, khá nặng mẫu-meta công cụ để suy luận các đối số, và rất nhiều thời gian để có được quyền này cho tất cả các trường hợp.

Và sau đó ai đó đến cùng với một inner() có đối số cho mỗi con trỏ. Tôi nghĩ rằng bây giờ làm cho 3 ^ 2. (Hoặc 4 ^ 2. Địa ngục, tôi không thể làm phiền để cố gắng nghĩ xem liệu const con trỏ sẽ tạo ra sự khác biệt.)

Và sau đó hãy 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 lý do tại sao một số tâm trí sáng lên với "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.


11
2017-08-27 07:44





Một điểm chưa được làm rõ ràng là static_cast<T&&> tay cầm 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ì nó không hoạt động.


2
2018-02-23 15:37



điểm tốt, vì vậy T && trong tĩnh đúc cũng theo quy tắc sụp đổ tham chiếu, phải không? - barney


Nó có thể là đáng giá để nhấn mạnh rằng về phía trước đã đượ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 báo cáo sau đây được cho phép, nhưng không tốt khác 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 một phương thức giả sử nó được sử dụng đúng cách trừ khi sử dụng không chính xác bị cấm.


1
2017-12-07 20:20



Tôi không nghĩ rằng ủy ban C ++ cảm thấy onus là trên họ để sử dụng các thành ngữ ngôn ngữ "chính xác", thậm chí không xác định những gì "đúng" sử dụng là (mặc dù họ chắc chắn có thể đưa ra hướng dẫn). Để kết thúc đó, trong khi giáo viên, ông chủ 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 chỉ đọc N2951 và tôi đồng ý rằng ủy ban tiêu chuẩn không có nghĩa vụ bổ sung 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 chức năng này (di chuyển và chuyển tiếp) thực sự hơi khó hiểu khi chỉ thấy định nghĩa của chúng trong tệp thư viện hoặc tài liệu chuẩn (23.2.5 Người 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, nhưng nó có thể hữu ích để thêm nhận xét nhiều hơn để làm cho mọi thứ rõ ràng hơn một chút. - colin