Câu trả lời đầu tiên của tôi là một giới thiệu cực kỳ đơn giản để di chuyển ngữ nghĩa, và nhiều chi tiết bị bỏ qua nhằm mục đích đơn giản. Tuy nhiên, có nhiều hơn nữa để di chuyển ngữ nghĩa, và tôi nghĩ rằng đã đến lúc câu trả lời thứ hai để lấp đầy khoảng trống. Câu trả lời đầu tiên đã khá cũ và cảm thấy không đúng khi chỉ cần thay thế nó bằng một văn bản hoàn toàn khác. Tôi nghĩ rằng nó vẫn phục vụ tốt như là một giới thiệu đầu tiên. Nhưng nếu bạn muốn đào sâu hơn, hãy đọc tiếp :)
Stephan T. Lavavej đã dành thời gian để cung cấp thông tin phản hồi có giá trị. Cảm ơn bạn rất nhiều, Stephan!
Giới thiệu
Di chuyển ngữ nghĩa cho phép một đối tượng, trong những điều kiện nhất định, có quyền sở hữu một số tài nguyên bên ngoài của đối tượng khác. Điều này rất quan trọng theo hai cách:
Biến các bản sao đắt tiền thành động thái rẻ tiền. Xem câu trả lời đầu tiên của tôi cho một ví dụ. Lưu ý rằng nếu một đối tượng không quản lý ít nhất một tài nguyên bên ngoài (trực tiếp hoặc gián tiếp thông qua các đối tượng thành viên của nó), thì ngữ nghĩa di chuyển sẽ không cung cấp bất kỳ lợi thế nào so với ngữ nghĩa sao chép. Trong trường hợp đó, sao chép một đối tượng và di chuyển một đối tượng có nghĩa là điều tương tự chính xác:
class cannot_benefit_from_move_semantics
{
int a; // moving an int means copying an int
float b; // moving a float means copying a float
double c; // moving a double means copying a double
char d[64]; // moving a char array means copying a char array
// ...
};
Thực hiện các loại "chỉ di chuyển" an toàn; đó là, các kiểu sao chép không có ý nghĩa, nhưng di chuyển thì có. Ví dụ bao gồm khóa, xử lý tệp và con trỏ thông minh với ngữ nghĩa sở hữu duy nhất. Lưu ý: Câu trả lời này thảo luận std::auto_ptr
, một mẫu thư viện chuẩn C ++ 98 không dùng nữa, đã được thay thế bằng std::unique_ptr
trong C ++ 11. Các lập trình viên C ++ trung cấp có lẽ ít nhất là hơi quen thuộc std::auto_ptr
và vì "ngữ nghĩa di chuyển" mà nó hiển thị, có vẻ như là một điểm khởi đầu tốt để thảo luận về ngữ nghĩa di chuyển trong C ++ 11. YMMV.
Di chuyển là gì?
Thư viện chuẩn C ++ 98 cung cấp một con trỏ thông minh với ngữ nghĩa sở hữu duy nhất được gọi std::auto_ptr<T>
. Trong trường hợp bạn không quen thuộc auto_ptr
, mục đích của nó là đảm bảo rằng một đối tượng được phân bổ động luôn được giải phóng, ngay cả khi đối mặt với các ngoại lệ:
{
std::auto_ptr<Shape> a(new Triangle);
// ...
// arbitrary code, could throw exceptions
// ...
} // <--- when a goes out of scope, the triangle is deleted automatically
Điều khác thường auto_ptr
là hành vi "sao chép" của nó:
auto_ptr<Shape> a(new Triangle);
+---------------+
| triangle data |
+---------------+
^
|
|
|
+-----|---+
| +-|-+ |
a | p | | | |
| +---+ |
+---------+
auto_ptr<Shape> b(a);
+---------------+
| triangle data |
+---------------+
^
|
+----------------------+
|
+---------+ +-----|---+
| +---+ | | +-|-+ |
a | p | | | b | p | | | |
| +---+ | | +---+ |
+---------+ +---------+
Lưu ý cách thức khởi tạo của b
với a
không không sao chép các hình tam giác, nhưng thay vì chuyển giao quyền sở hữu của tam giác từ a
đến b
. Chúng tôi cũng nói " a
được chuyển thành b
" hoặc "tam giác được chuyển từ a
tới b
". Điều này nghe có vẻ khó hiểu vì bản thân tam giác luôn ở cùng một vị trí trong bộ nhớ.
Để di chuyển một đối tượng có nghĩa là chuyển quyền sở hữu một số tài nguyên mà nó quản lý sang một đối tượng khác.
Hàm tạo sao chép auto_ptr
có thể trông giống như thế này (hơi đơn giản):
auto_ptr(auto_ptr& source) // note the missing const
{
p = source.p;
source.p = 0; // now the source no longer owns the object
}
Di chuyển nguy hiểm và vô hại
Điều nguy hiểm auto_ptr
là những gì về mặt cú pháp trông giống như một bản sao thực sự là một động thái. Cố gắng gọi một hàm thành viên trên một chuyển từ auto_ptr
sẽ gọi hành vi không xác định, vì vậy bạn phải rất cẩn thận không sử dụng một auto_ptr
sau khi nó đã được di chuyển từ:
auto_ptr<Shape> a(new Triangle); // create triangle
auto_ptr<Shape> b(a); // move a into b
double area = a->area(); // undefined behavior
Nhưng auto_ptr
không phải lúc nào cũng nguy hiểm. Các chức năng của nhà máy là một trường hợp sử dụng hoàn toàn tốt cho auto_ptr
:
auto_ptr<Shape> make_triangle()
{
return auto_ptr<Shape>(new Triangle);
}
auto_ptr<Shape> c(make_triangle()); // move temporary into c
double area = make_triangle()->area(); // perfectly safe
Lưu ý cách cả hai ví dụ theo cùng một mẫu cú pháp:
auto_ptr<Shape> variable(expression);
double area = expression->area();
Tuy nhiên, một trong số họ viện dẫn hành vi không xác định, trong khi người còn lại thì không. Vậy sự khác biệt giữa các biểu thức a
và là make_triangle()
gì? Cả hai cùng loại phải không? Thật vậy, họ có, nhưng họ có các loại giá trị khác nhau .
Các loại giá trị
Rõ ràng, phải có một số khác biệt sâu sắc giữa biểu thức biểu a
thị một auto_ptr
biến và biểu thức biểu make_triangle()
thị lệnh gọi của hàm trả về một auto_ptr
giá trị, do đó tạo ra một auto_ptr
đối tượng tạm thời mới mỗi khi nó được gọi. a
là một ví dụ về một giá trị trái , trong khi đó make_triangle()
là một ví dụ về một rvalue .
Di chuyển từ các giá trị như a
là nguy hiểm, vì sau này chúng ta có thể cố gắng gọi một hàm thành viên thông qua a
, gọi hành vi không xác định. Mặt khác, di chuyển từ các giá trị như make_triangle()
là hoàn toàn an toàn, bởi vì sau khi trình xây dựng sao chép đã thực hiện công việc của mình, chúng ta không thể sử dụng lại tạm thời. Không có biểu hiện nào cho thấy tạm thời nói; nếu chúng ta chỉ đơn giản viết make_triangle()
lại, chúng ta sẽ có một tạm thời khác . Trong thực tế, tạm thời chuyển từ đã đi trên dòng tiếp theo:
auto_ptr<Shape> c(make_triangle());
^ the moved-from temporary dies right here
Lưu ý rằng các chữ cái l
và r
có nguồn gốc lịch sử ở phía bên trái và bên phải của một bài tập. Điều này không còn đúng trong C ++, bởi vì có các giá trị không thể xuất hiện ở phía bên trái của một phép gán (như mảng hoặc các kiểu do người dùng định nghĩa mà không có toán tử gán) và có các giá trị có thể (tất cả các giá trị của các loại lớp với một toán tử gán).
Một giá trị của loại lớp là một biểu thức có đánh giá tạo ra một đối tượng tạm thời. Trong trường hợp bình thường, không có biểu thức nào khác trong cùng phạm vi biểu thị cùng một đối tượng tạm thời.
Tài liệu tham khảo Rvalue
Bây giờ chúng tôi hiểu rằng di chuyển từ giá trị có thể nguy hiểm, nhưng di chuyển từ giá trị là vô hại. Nếu C ++ có hỗ trợ ngôn ngữ để phân biệt đối số giá trị với đối số giá trị, chúng ta hoàn toàn có thể cấm di chuyển khỏi giá trị hoặc ít nhất là thực hiện chuyển từ giá trị rõ ràng tại trang web cuộc gọi, để chúng ta không còn di chuyển do tai nạn.
Câu trả lời của C ++ 11 cho vấn đề này là tài liệu tham khảo giá trị . Tham chiếu rvalue là một loại tham chiếu mới chỉ liên kết với các giá trị và cú pháp là X&&
. Các tài liệu tham khảo cũ tốt X&
hiện được gọi là một tài liệu tham khảo lvalue . (Lưu ý rằng X&&
là không một tham chiếu đến một tài liệu tham khảo, không có điều như vậy trong C ++.)
Nếu chúng ta const
kết hợp, chúng ta đã có bốn loại tài liệu tham khảo khác nhau. Những loại biểu thức X
mà họ có thể liên kết với?
lvalue const lvalue rvalue const rvalue
---------------------------------------------------------
X& yes
const X& yes yes yes yes
X&& yes
const X&& yes yes
Trong thực tế, bạn có thể quên đi const X&&
. Bị hạn chế đọc từ giá trị không phải là rất hữu ích.
Một tham chiếu rvalue X&&
là một loại tham chiếu mới chỉ liên kết với các giá trị.
Chuyển đổi ngầm định
Tài liệu tham khảo Rvalue đã trải qua một số phiên bản. Kể từ phiên bản 2.1, một tham chiếu X&&
giá trị cũng liên kết với tất cả các loại giá trị thuộc loại khác Y
, miễn là có một chuyển đổi ngầm định từ Y
sang X
. Trong trường hợp đó, một loại tạm thời X
được tạo và tham chiếu giá trị bị ràng buộc với tạm thời đó:
void some_function(std::string&& r);
some_function("hello world");
Trong ví dụ trên, "hello world"
là một giá trị của loại const char[12]
. Vì có một chuyển đổi ngầm định từ const char[12]
thông qua const char*
sang std::string
, một loại tạm thời std::string
được tạo và r
bị ràng buộc với tạm thời đó. Đây là một trong những trường hợp mà sự phân biệt giữa giá trị (biểu thức) và tạm thời (đối tượng) hơi mờ.
Di chuyển các nhà xây dựng
Một ví dụ hữu ích của hàm với X&&
tham số là hàm tạo di chuyển X::X(X&& source)
. Mục đích của nó là chuyển quyền sở hữu tài nguyên được quản lý từ nguồn vào đối tượng hiện tại.
Trong C ++ 11, std::auto_ptr<T>
đã được thay thế bằng cách std::unique_ptr<T>
tận dụng các tham chiếu rvalue. Tôi sẽ phát triển và thảo luận về một phiên bản đơn giản hóa unique_ptr
. Đầu tiên, chúng tôi gói gọn một con trỏ thô và làm quá tải các toán tử ->
và *
vì vậy lớp của chúng tôi có cảm giác như một con trỏ:
template<typename T>
class unique_ptr
{
T* ptr;
public:
T* operator->() const
{
return ptr;
}
T& operator*() const
{
return *ptr;
}
Hàm tạo có quyền sở hữu đối tượng và hàm hủy sẽ xóa nó:
explicit unique_ptr(T* p = nullptr)
{
ptr = p;
}
~unique_ptr()
{
delete ptr;
}
Bây giờ đến phần thú vị, hàm tạo di chuyển:
unique_ptr(unique_ptr&& source) // note the rvalue reference
{
ptr = source.ptr;
source.ptr = nullptr;
}
Hàm tạo di chuyển này thực hiện chính xác những gì hàm tạo auto_ptr
sao chép đã làm, nhưng nó chỉ có thể được cung cấp với các giá trị:
unique_ptr<Shape> a(new Triangle);
unique_ptr<Shape> b(a); // error
unique_ptr<Shape> c(make_triangle()); // okay
Dòng thứ hai không biên dịch được, vì a
là một giá trị, nhưng tham số unique_ptr&& source
chỉ có thể được liên kết với các giá trị. Đây chính xác là những gì chúng tôi muốn; di chuyển nguy hiểm không bao giờ nên được ngầm. Dòng thứ ba biên dịch tốt, bởi vì make_triangle()
là một giá trị. Các constructor di chuyển sẽ chuyển quyền sở hữu từ tạm thời sang c
. Một lần nữa, đây chính xác là những gì chúng tôi muốn.
Hàm tạo di chuyển chuyển quyền sở hữu tài nguyên được quản lý vào đối tượng hiện tại.
Di chuyển toán tử gán
Phần còn thiếu cuối cùng là toán tử gán di chuyển. Nhiệm vụ của nó là giải phóng tài nguyên cũ và thu nhận tài nguyên mới từ đối số của nó:
unique_ptr& operator=(unique_ptr&& source) // note the rvalue reference
{
if (this != &source) // beware of self-assignment
{
delete ptr; // release the old resource
ptr = source.ptr; // acquire the new resource
source.ptr = nullptr;
}
return *this;
}
};
Lưu ý cách triển khai toán tử gán chuyển động này sao chép logic của cả hàm hủy và hàm tạo di chuyển. Bạn có quen thuộc với thành ngữ copy-and-exchange? Nó cũng có thể được áp dụng để di chuyển ngữ nghĩa như là thành ngữ di chuyển và trao đổi:
unique_ptr& operator=(unique_ptr source) // note the missing reference
{
std::swap(ptr, source.ptr);
return *this;
}
};
Bây giờ source
là một biến kiểu unique_ptr
, nó sẽ được khởi tạo bởi hàm tạo di chuyển; đó là, đối số sẽ được chuyển vào tham số. Đối số vẫn được yêu cầu là một giá trị, bởi vì chính hàm tạo di chuyển có một tham số tham chiếu giá trị. Khi luồng điều khiển đạt đến mức đóng của operator=
, source
đi ra khỏi phạm vi, tự động giải phóng tài nguyên cũ.
Toán tử gán chuyển di chuyển quyền sở hữu tài nguyên được quản lý vào đối tượng hiện tại, giải phóng tài nguyên cũ. Thành ngữ di chuyển và trao đổi đơn giản hóa việc thực hiện.
Di chuyển từ giá trị
Đôi khi, chúng tôi muốn chuyển từ giá trị. Đó là, đôi khi chúng tôi muốn trình biên dịch xử lý một giá trị như thể nó là một giá trị, vì vậy nó có thể gọi hàm tạo di chuyển, mặc dù nó có thể không an toàn. Với mục đích này, C ++ 11 cung cấp một mẫu hàm thư viện tiêu chuẩn được gọi std::move
bên trong tiêu đề <utility>
. Tên này là một chút không may, bởi vì std::move
chỉ đơn giản là đưa ra một giá trị cho một giá trị; nó không tự di chuyển bất cứ thứ gì Nó chỉ cho phép di chuyển. Có lẽ nó nên được đặt tên std::cast_to_rvalue
hoặc std::enable_move
, nhưng bây giờ chúng ta bị mắc kẹt với cái tên này.
Đây là cách bạn rõ ràng di chuyển từ một giá trị:
unique_ptr<Shape> a(new Triangle);
unique_ptr<Shape> b(a); // still an error
unique_ptr<Shape> c(std::move(a)); // okay
Lưu ý rằng sau dòng thứ ba, a
không còn sở hữu một hình tam giác. Điều đó không sao, bởi vì bằng cách viết rõ ràngstd::move(a)
, chúng tôi đã nói rõ ý định của mình: "Kính gửi nhà xây dựng, làm bất cứ điều gì bạn muốn a
để khởi tạo c
; Tôi không quan tâm đến a
nữa. Hãy thoải mái theo cách của bạn a
."
std::move(some_lvalue)
đưa ra một giá trị cho một giá trị, do đó cho phép di chuyển tiếp theo.
Giá trị
Lưu ý rằng mặc dù std::move(a)
là một giá trị, đánh giá của nó không tạo ra một đối tượng tạm thời. Câu hỏi hóc búa này buộc ủy ban phải giới thiệu một loại giá trị thứ ba. Một cái gì đó có thể được liên kết với một tham chiếu giá trị, mặc dù nó không phải là một giá trị theo nghĩa truyền thống, được gọi là xvalue (giá trị eXpires). Các giá trị truyền thống được đổi tên thành giá trị (giá trị thuần túy).
Cả giá trị và giá trị x đều là giá trị. Xvalues và lvalues đều là glvalues ( Generalvalval ). Các mối quan hệ dễ nắm bắt hơn với sơ đồ:
expressions
/ \
/ \
/ \
glvalues rvalues
/ \ / \
/ \ / \
/ \ / \
lvalues xvalues prvalues
Lưu ý rằng chỉ xvalues là thực sự mới; phần còn lại chỉ là do đổi tên và nhóm.
Giá trị C ++ 98 được gọi là giá trị trong C ++ 11. Thay thế tinh thần tất cả các lần xuất hiện của "giá trị" trong các đoạn trước bằng "prvalue".
Di chuyển ra khỏi chức năng
Cho đến nay, chúng ta đã thấy sự di chuyển vào các biến cục bộ và vào các tham số hàm. Nhưng di chuyển cũng có thể theo hướng ngược lại. Nếu một hàm trả về theo giá trị, một số đối tượng tại trang gọi (có thể là biến cục bộ hoặc tạm thời, nhưng có thể là bất kỳ loại đối tượng nào) được khởi tạo với biểu thức sau return
câu lệnh làm đối số cho hàm tạo di chuyển:
unique_ptr<Shape> make_triangle()
{
return unique_ptr<Shape>(new Triangle);
} \-----------------------------/
|
| temporary is moved into c
|
v
unique_ptr<Shape> c(make_triangle());
Có lẽ đáng ngạc nhiên, các đối tượng tự động (biến cục bộ không được khai báo là static
) cũng có thể được chuyển hoàn toàn ra khỏi các hàm:
unique_ptr<Shape> make_square()
{
unique_ptr<Shape> result(new Square);
return result; // note the missing std::move
}
Làm thế nào mà các nhà xây dựng di chuyển chấp nhận giá trị result
như một đối số? Phạm vi của result
nó sắp kết thúc, và nó sẽ bị phá hủy trong quá trình giải nén stack. Không ai có thể phàn nàn sau đó result
đã thay đổi bằng cách nào đó; khi luồng điều khiển quay lại người gọi, result
không tồn tại nữa! Vì lý do đó, C ++ 11 có một quy tắc đặc biệt cho phép trả về các đối tượng tự động từ các chức năng mà không phải viết std::move
. Trong thực tế, bạn không bao giờ nên sử dụng std::move
để di chuyển các đối tượng tự động ra khỏi các chức năng, vì điều này ngăn cản "tối ưu hóa giá trị trả về có tên" (NRVO).
Không bao giờ sử dụng std::move
để di chuyển các đối tượng tự động ra khỏi chức năng.
Lưu ý rằng trong cả hai chức năng của nhà máy, loại trả về là một giá trị, không phải là tham chiếu giá trị. Tham chiếu Rvalue vẫn là tài liệu tham khảo và như mọi khi, bạn không bao giờ nên trả lại tham chiếu cho đối tượng tự động; người gọi sẽ kết thúc với một tham chiếu lơ lửng nếu bạn lừa trình biên dịch chấp nhận mã của bạn, như thế này:
unique_ptr<Shape>&& flawed_attempt() // DO NOT DO THIS!
{
unique_ptr<Shape> very_bad_idea(new Square);
return std::move(very_bad_idea); // WRONG!
}
Không bao giờ trả lại các đối tượng tự động bằng cách tham chiếu giá trị. Di chuyển được thực hiện độc quyền bởi các nhà xây dựng di chuyển, không phải bởi std::move
, và không chỉ bằng cách ràng buộc một giá trị với một tham chiếu giá trị.
Di chuyển thành viên
Sớm hay muộn, bạn sẽ viết mã như thế này:
class Foo
{
unique_ptr<Shape> member;
public:
Foo(unique_ptr<Shape>&& parameter)
: member(parameter) // error
{}
};
Về cơ bản, trình biên dịch sẽ phàn nàn rằng đó parameter
là một giá trị. Nếu bạn nhìn vào loại của nó, bạn sẽ thấy một tham chiếu rvalue, nhưng một tham chiếu rvalue chỉ đơn giản có nghĩa là "một tham chiếu được ràng buộc với một giá trị"; điều đó không có nghĩa là bản thân tài liệu tham khảo là một giá trị! Thật vậy, parameter
chỉ là một biến số thông thường với một tên. Bạn có thể sử dụng bao nhiêu lần parameter
tùy thích bên trong phần thân của hàm tạo và nó luôn biểu thị cùng một đối tượng. Hoàn toàn di chuyển từ nó sẽ nguy hiểm, do đó ngôn ngữ cấm nó.
Một tham chiếu rvalue có tên là một giá trị, giống như bất kỳ biến nào khác.
Giải pháp là tự kích hoạt di chuyển:
class Foo
{
unique_ptr<Shape> member;
public:
Foo(unique_ptr<Shape>&& parameter)
: member(std::move(parameter)) // note the std::move
{}
};
Bạn có thể lập luận rằng parameter
nó không được sử dụng nữa sau khi khởi tạo member
. Tại sao không có quy tắc đặc biệt nào để âm thầm chèn std::move
giống như với các giá trị trả về? Có lẽ bởi vì nó sẽ là quá nhiều gánh nặng cho những người triển khai trình biên dịch. Ví dụ, nếu cơ quan xây dựng ở một đơn vị dịch thuật khác thì sao? Ngược lại, quy tắc giá trị trả về chỉ đơn giản là phải kiểm tra các bảng ký hiệu để xác định xem có phải là định danh hay không sau khi return
từ khóa biểu thị một đối tượng tự động.
Bạn cũng có thể vượt qua parameter
theo giá trị. Đối với các loại chỉ di chuyển như unique_ptr
, có vẻ như chưa có thành ngữ nào được thiết lập. Cá nhân, tôi thích vượt qua bởi giá trị, vì nó gây ra sự lộn xộn trong giao diện.
Chức năng thành viên đặc biệt
C ++ 98 ngầm định khai báo ba hàm thành viên đặc biệt theo yêu cầu, nghĩa là khi chúng cần ở đâu đó: hàm tạo sao chép, toán tử gán sao chép và hàm hủy.
X::X(const X&); // copy constructor
X& X::operator=(const X&); // copy assignment operator
X::~X(); // destructor
Tài liệu tham khảo Rvalue đã trải qua một số phiên bản. Kể từ phiên bản 3.0, C ++ 11 tuyên bố hai hàm thành viên đặc biệt bổ sung theo yêu cầu: hàm tạo di chuyển và toán tử gán chuyển động. Lưu ý rằng cả VC10 và VC11 đều không phù hợp với phiên bản 3.0, vì vậy bạn sẽ phải tự thực hiện chúng.
X::X(X&&); // move constructor
X& X::operator=(X&&); // move assignment operator
Hai hàm thành viên đặc biệt mới này chỉ được khai báo ngầm nếu không có hàm thành viên đặc biệt nào được khai báo thủ công. Ngoài ra, nếu bạn khai báo hàm tạo di chuyển của riêng bạn hoặc toán tử chuyển nhượng di chuyển, thì hàm tạo sao chép cũng như toán tử gán gán sao chép sẽ không được khai báo ngầm.
Những quy tắc này có ý nghĩa gì trong thực tế?
Nếu bạn viết một lớp mà không có tài nguyên không được quản lý, bạn không cần phải tự khai báo bất kỳ chức năng nào trong năm chức năng thành viên đặc biệt và bạn sẽ nhận được ngữ nghĩa sao chép chính xác và di chuyển ngữ nghĩa miễn phí. Nếu không, bạn sẽ phải tự thực hiện các chức năng thành viên đặc biệt. Tất nhiên, nếu lớp của bạn không được hưởng lợi từ ngữ nghĩa di chuyển, thì không cần phải thực hiện các hoạt động di chuyển đặc biệt.
Lưu ý rằng toán tử gán gán sao chép và toán tử gán di chuyển có thể được hợp nhất thành một toán tử gán đơn thống nhất, lấy giá trị của nó theo giá trị:
X& X::operator=(X source) // unified assignment operator
{
swap(source); // see my first answer for an explanation
return *this;
}
Bằng cách này, số lượng chức năng thành viên đặc biệt để thực hiện giảm từ năm xuống còn bốn. Có một sự đánh đổi giữa an toàn ngoại lệ và hiệu quả ở đây, nhưng tôi không phải là chuyên gia về vấn đề này.
Chuyển tiếp tài liệu tham khảo ( trước đây gọi là tài liệu tham khảo phổ quát )
Hãy xem xét mẫu hàm sau:
template<typename T>
void foo(T&&);
Bạn có thể mong đợi T&&
chỉ liên kết với các giá trị, bởi vì thoạt nhìn, nó trông giống như một tham chiếu giá trị. Khi nó bật ra, T&&
cũng liên kết với giá trị:
foo(make_triangle()); // T is unique_ptr<Shape>, T&& is unique_ptr<Shape>&&
unique_ptr<Shape> a(new Triangle);
foo(a); // T is unique_ptr<Shape>&, T&& is unique_ptr<Shape>&
Nếu đối số là một giá trị của loại X
, T
được suy ra là X
, do đó T&&
có nghĩa là X&&
. Đây là những gì bất cứ ai mong đợi. Nhưng nếu đối số là một giá trị loại X
, do một quy tắc đặc biệt, T
được suy luận là X&
, do đó T&&
sẽ có nghĩa như thế X& &&
. Nhưng vì C ++ vẫn không có khái niệm về các tham chiếu đến các tham chiếu, nên kiểu X& &&
này được thu gọn vào X&
. Điều này thoạt nghe có vẻ khó hiểu và vô dụng, nhưng thu gọn tham chiếu là điều cần thiết để chuyển tiếp hoàn hảo (điều này sẽ không được thảo luận ở đây).
T && không phải là tham chiếu giá trị, mà là tham chiếu chuyển tiếp. Nó cũng liên kết với giá trị, trong trường hợp này T
và T&&
cả hai tham chiếu giá trị.
Nếu bạn muốn giới hạn một mẫu hàm theo giá trị, bạn có thể kết hợp SFINAE với các đặc điểm loại:
#include <type_traits>
template<typename T>
typename std::enable_if<std::is_rvalue_reference<T&&>::value, void>::type
foo(T&&);
Thực hiện di chuyển
Bây giờ bạn đã hiểu sự sụp đổ tham chiếu, đây là cách std::move
thực hiện:
template<typename T>
typename std::remove_reference<T>::type&&
move(T&& t)
{
return static_cast<typename std::remove_reference<T>::type&&>(t);
}
Như bạn có thể thấy, move
chấp nhận bất kỳ loại tham số nào nhờ tham chiếu chuyển tiếp T&&
và nó trả về tham chiếu giá trị. Cuộc std::remove_reference<T>::type
gọi hàm meta là cần thiết bởi vì nếu không, đối với các giá trị của kiểu X
, kiểu trả về sẽ là X& &&
, sẽ sụp đổ vào X&
. Vì t
luôn luôn là một giá trị (hãy nhớ rằng một tham chiếu rvalue có tên là một giá trị), nhưng chúng tôi muốn liên kết t
với một tham chiếu giá trị, chúng tôi phải chuyển rõ ràng t
sang loại trả về chính xác. Cuộc gọi của hàm trả về tham chiếu rvalue chính là xvalue. Bây giờ bạn biết xvalues đến từ đâu;)
Cuộc gọi của hàm trả về tham chiếu giá trị, chẳng hạn như std::move
, là một giá trị x.
Lưu ý rằng việc trả về bằng tham chiếu rvalue là tốt trong ví dụ này, vì t
không biểu thị một đối tượng tự động, mà thay vào đó là một đối tượng được người gọi chuyển qua.