Đó là chính xác mà std::move(x)
chỉ là một diễn viên để xác định giá trị - cụ thể hơn là một giá trị x , trái ngược với một giá trị . Và cũng đúng là có một dàn diễn viên có tên move
đôi khi khiến mọi người bối rối. Tuy nhiên, mục đích của việc đặt tên này không phải là nhầm lẫn, mà là để làm cho mã của bạn dễ đọc hơn.
Lịch sử của move
ngày trở lại đề xuất di chuyển ban đầu vào năm 2002 . Bài viết này trước tiên giới thiệu tham chiếu giá trị, và sau đó cho thấy cách viết hiệu quả hơn std::swap
:
template <class T>
void
swap(T& a, T& b)
{
T tmp(static_cast<T&&>(a));
a = static_cast<T&&>(b);
b = static_cast<T&&>(tmp);
}
Người ta phải nhớ lại rằng tại thời điểm này trong lịch sử, điều duy nhất mà " &&
" có thể có nghĩa là hợp lý và . Không ai quen thuộc với các tài liệu tham khảo về giá trị, cũng không liên quan đến việc truyền một giá trị cho một giá trị (trong khi không tạo một bản sao như static_cast<T>(t)
sẽ làm). Vì vậy, độc giả của mã này sẽ tự nhiên nghĩ:
Tôi biết làm thế nào swap
để làm việc (sao chép tạm thời và sau đó trao đổi các giá trị), nhưng mục đích của những diễn viên xấu xí đó là gì?!
Cũng lưu ý rằng đó swap
thực sự chỉ là một thay thế cho tất cả các loại thuật toán thay đổi hoán vị. Cuộc thảo luận này là nhiều , lớn hơn nhiều swap
.
Sau đó, đề xuất giới thiệu cú pháp đường thay thế static_cast<T&&>
bằng thứ gì đó dễ đọc hơn, truyền tải không chính xác những gì , mà là tại sao :
template <class T>
void
swap(T& a, T& b)
{
T tmp(move(a));
a = move(b);
b = move(tmp);
}
Tức move
là chỉ là cú pháp đường cho static_cast<T&&>
, và bây giờ mã này khá gợi ý về lý do tại sao các phôi đó ở đó: để cho phép di chuyển ngữ nghĩa!
Người ta phải hiểu rằng trong bối cảnh lịch sử, rất ít người tại thời điểm này thực sự hiểu được mối liên hệ mật thiết giữa các giá trị và ngữ nghĩa di chuyển (mặc dù bài báo cũng cố gắng giải thích điều đó):
Di chuyển ngữ nghĩa sẽ tự động đi vào chơi khi đưa ra các đối số giá trị. Điều này là hoàn toàn an toàn bởi vì phần còn lại của chương trình không thể nhận thấy các tài nguyên di chuyển từ một giá trị ( không ai khác có tham chiếu đến giá trị để phát hiện sự khác biệt ).
Nếu tại thời điểm đó swap
được trình bày như thế này:
template <class T>
void
swap(T& a, T& b)
{
T tmp(cast_to_rvalue(a));
a = cast_to_rvalue(b);
b = cast_to_rvalue(tmp);
}
Sau đó mọi người sẽ nhìn vào đó và nói:
Nhưng tại sao bạn đúc để định giá?
Điểm chính:
Vì nó là, sử dụng move
, không ai từng hỏi:
Nhưng tại sao bạn di chuyển?
Khi nhiều năm trôi qua và đề xuất được cải tiến, các khái niệm về giá trị và giá trị đã được tinh chỉnh thành các loại giá trị chúng ta có ngày hôm nay:
(hình ảnh bị đánh cắp một cách đáng xấu hổ từ dirkgently )
Và hôm nay, nếu chúng ta muốn swap
nói chính xác những gì nó đang làm, thay vì tại sao , nó sẽ trông giống như:
template <class T>
void
swap(T& a, T& b)
{
T tmp(set_value_category_to_xvalue(a));
a = set_value_category_to_xvalue(b);
b = set_value_category_to_xvalue(tmp);
}
Và câu hỏi mà mọi người nên tự đặt ra là liệu đoạn mã trên có nhiều hay ít đọc hơn:
template <class T>
void
swap(T& a, T& b)
{
T tmp(move(a));
a = move(b);
b = move(tmp);
}
Hoặc thậm chí là bản gốc:
template <class T>
void
swap(T& a, T& b)
{
T tmp(static_cast<T&&>(a));
a = static_cast<T&&>(b);
b = static_cast<T&&>(tmp);
}
Trong mọi trường hợp, lập trình viên C ++ của hành trình nên biết rằng dưới vỏ bọc move
, không có gì xảy ra hơn là diễn viên. Và lập trình viên C ++ mới bắt đầu, ít nhất là với move
, sẽ được thông báo rằng ý định là chuyển từ rhs, trái ngược với việc sao chép từ rhs, ngay cả khi họ không hiểu chính xác cách thức thực hiện.
Ngoài ra, nếu một lập trình viên mong muốn chức năng này dưới một tên khác, std::move
không có độc quyền về chức năng này và không có phép thuật ngôn ngữ không di động nào liên quan đến việc thực hiện nó. Ví dụ: nếu một người muốn viết mã set_value_category_to_xvalue
và sử dụng nó thay vào đó, thì việc này là không quan trọng:
template <class T>
inline
constexpr
typename std::remove_reference<T>::type&&
set_value_category_to_xvalue(T&& t) noexcept
{
return static_cast<typename std::remove_reference<T>::type&&>(t);
}
Trong C ++ 14, nó thậm chí còn ngắn gọn hơn:
template <class T>
inline
constexpr
auto&&
set_value_category_to_xvalue(T&& t) noexcept
{
return static_cast<std::remove_reference_t<T>&&>(t);
}
Vì vậy, nếu bạn rất có khuynh hướng, hãy trang trí theo cách static_cast<T&&>
bạn nghĩ tốt nhất, và có lẽ cuối cùng bạn sẽ phát triển một thực tiễn tốt nhất mới (C ++ không ngừng phát triển).
Vì vậy, những gì move
làm về mặt mã đối tượng được tạo ra?
Hãy xem xét điều này test
:
void
test(int& i, int& j)
{
i = j;
}
Được biên dịch với clang++ -std=c++14 test.cpp -O3 -S
, điều này tạo ra mã đối tượng này:
__Z4testRiS_: ## @_Z4testRiS_
.cfi_startproc
## BB#0:
pushq %rbp
Ltmp0:
.cfi_def_cfa_offset 16
Ltmp1:
.cfi_offset %rbp, -16
movq %rsp, %rbp
Ltmp2:
.cfi_def_cfa_register %rbp
movl (%rsi), %eax
movl %eax, (%rdi)
popq %rbp
retq
.cfi_endproc
Bây giờ nếu thử nghiệm được thay đổi thành:
void
test(int& i, int& j)
{
i = std::move(j);
}
Có hoàn toàn không có sự thay đổi ở tất cả trong mã đối tượng. Người ta có thể khái quát kết quả này thành: Đối với các đối tượng di chuyển tầm thường , std::move
không có tác động.
Bây giờ hãy xem ví dụ này:
struct X
{
X& operator=(const X&);
};
void
test(X& i, X& j)
{
i = j;
}
Điều này tạo ra:
__Z4testR1XS0_: ## @_Z4testR1XS0_
.cfi_startproc
## BB#0:
pushq %rbp
Ltmp0:
.cfi_def_cfa_offset 16
Ltmp1:
.cfi_offset %rbp, -16
movq %rsp, %rbp
Ltmp2:
.cfi_def_cfa_register %rbp
popq %rbp
jmp __ZN1XaSERKS_ ## TAILCALL
.cfi_endproc
Nếu bạn chạy __ZN1XaSERKS_
qua c++filt
nó sẽ tạo ra : X::operator=(X const&)
. Không có gì bất ngờ ở đây. Bây giờ nếu thử nghiệm được thay đổi thành:
void
test(X& i, X& j)
{
i = std::move(j);
}
Sau đó, vẫn không có thay đổi nào trong mã đối tượng được tạo. std::move
đã không làm gì ngoài j
việc chuyển sang một giá trị, và sau đó giá trị đó X
liên kết với toán tử gán sao chép của X
.
Bây giờ cho phép thêm một toán tử chuyển nhượng di chuyển đến X
:
struct X
{
X& operator=(const X&);
X& operator=(X&&);
};
Bây giờ mã đối tượng không thay đổi:
__Z4testR1XS0_: ## @_Z4testR1XS0_
.cfi_startproc
## BB#0:
pushq %rbp
Ltmp0:
.cfi_def_cfa_offset 16
Ltmp1:
.cfi_offset %rbp, -16
movq %rsp, %rbp
Ltmp2:
.cfi_def_cfa_register %rbp
popq %rbp
jmp __ZN1XaSEOS_ ## TAILCALL
.cfi_endproc
Chạy __ZN1XaSEOS_
qua c++filt
cho thấy rằng X::operator=(X&&)
đang được gọi thay vì X::operator=(X const&)
.
Và đó là tất cả để có std::move
! Nó hoàn toàn biến mất trong thời gian chạy. Tác động duy nhất của nó là vào thời gian biên dịch, nơi nó có thể thay đổi những gì quá tải được gọi.
std::move
thực sự di chuyển ..