Tại sao `std :: move` có tên` std :: move`?


128

Hàm C ++ 11 std::move(x)hoàn toàn không di chuyển bất cứ thứ gì. Nó chỉ là một diễn viên để giá trị r. Tại sao điều này được thực hiện? Đây không phải là sai lệch?


Để làm cho vấn đề tồi tệ hơn, ba đối số std::movethực sự di chuyển ..
Cubbi

Và đừng quên về C ++ std::char_traits::move
98/03/11

30
Yêu thích khác của tôi là std::remove()không loại bỏ các yếu tố: Bạn vẫn phải gọi erase()để thực sự loại bỏ các yếu tố đó khỏi container. Vì vậy, movekhông di chuyển, removekhông loại bỏ. Tôi đã chọn tên mark_movable()cho move.
Ali

4
@Ali tôi cũng sẽ thấy mark_movable()khó hiểu. Nó cho thấy có một tác dụng phụ kéo dài trong khi thực tế không có.
vây

Câu trả lời:


177

Đó 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 movengà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 đó swapthự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 movelà 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:

Phân loại

(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 swapnó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::movekhô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_xvaluevà 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ì movelà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);
}

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::movekhô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++filtnó 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 jviệc chuyển sang một giá trị, và sau đó giá trị đó Xliê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++filtcho thấy rằng X::operator=(X&&)đang được gọi thay vì X::operator=(X const&).

đó 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.


7
Đây là một nguồn chấm cho biểu đồ đó: Tôi đã tạo lại nó digraph D { glvalue -> { lvalue; xvalue } rvalue -> { xvalue; prvalue } expression -> { glvalue; rvalue } }cho mục đích công cộng :) Tải xuống ở đây dưới dạng SVG
sehe

7
Điều này vẫn còn mở cho đi xe đạp? Tôi muốn đề nghị allow_move;)
dyp

2
@dyp Yêu thích của tôi vẫn là movable.
Daniel Frey

6
Scott Meyers đề nghị đổi tên std::movethành rvalue_cast: youtube.com/ Từ
nairware

6
Vì rvalue bây giờ đề cập đến cả giá trị và giá trị x, nên rvalue_castmơ hồ về ý nghĩa của nó: loại giá trị nào nó trở lại? xvalue_castsẽ là một cái tên phù hợp ở đây. Thật không may, hầu hết mọi người, tại thời điểm này, cũng sẽ không hiểu những gì nó đang làm. Trong một vài năm nữa, tuyên bố của tôi hy vọng sẽ trở thành sai.
Howard Hinnant

20

Hãy để tôi chỉ để lại ở đây một trích dẫn từ Câu hỏi thường gặp về C ++ 11 được viết bởi B. Stroustrup, đây là câu trả lời trực tiếp cho câu hỏi của OP:

di chuyển (x) có nghĩa là "bạn có thể coi x là một giá trị". Có lẽ sẽ tốt hơn nếu move () được gọi là rval (), nhưng bây giờ move () đã được sử dụng trong nhiều năm.

Nhân tiện, tôi thực sự rất thích FAQ - nó đáng để đọc.


2
Để ăn cắp ý kiến ​​của @ HowardHinnant từ một câu trả lời khác: Câu trả lời của Stroustrup là không chính xác, vì hiện nay có hai loại giá trị - giá trị và xvalues, và std :: move thực sự là một giá trị xvalue.
einpoklum
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.