Std :: move () là gì và khi nào nên sử dụng nó?


656
  1. Nó là gì?
  2. Nó làm gì?
  3. Nó nên được sử dụng lúc nào?

Liên kết tốt được đánh giá cao.


43
Bjarne Stroustrup giải thích di chuyển trong Giới thiệu ngắn gọn về Tài liệu tham khảo
Rvalue


12
Câu hỏi này được đề cập đến std::move(T && t); cũng tồn tại một std::move(InputIt first, InputIt last, OutputIt d_first)thuật toán liên quan đến std::copy. Tôi chỉ ra điều đó để những người khác không bối rối như tôi khi lần đầu tiên đối mặt với std::moveviệc đưa ra ba lập luận. en.cppreference.com/w/cpp/alacticm/move
josaphatv

Câu trả lời:


287

Trang Wikipedia về C ++ 11 Tham chiếu giá trị R và di chuyển các hàm tạo

  1. Trong C ++ 11, ngoài việc sao chép các hàm tạo, các đối tượng có thể có các hàm tạo di chuyển.
    (Và ngoài các toán tử gán gán, chúng có các toán tử gán chuyển động.)
  2. Hàm xây dựng di chuyển được sử dụng thay cho hàm tạo sao chép, nếu đối tượng có kiểu "rvalue-Reference" ( Type &&).
  3. std::move() là một diễn viên tạo ra một tham chiếu giá trị cho một đối tượng, để cho phép di chuyển từ nó.

Đó là một cách C ++ mới để tránh các bản sao. Ví dụ, bằng cách sử dụng hàm tạo di chuyển, người std::vectorta chỉ có thể sao chép con trỏ bên trong của nó vào dữ liệu sang đối tượng mới, để đối tượng được di chuyển ở trạng thái di chuyển từ trạng thái, do đó không sao chép tất cả dữ liệu. Đây sẽ là C ++ - hợp lệ.

Hãy thử googling cho di chuyển ngữ nghĩa, giá trị, chuyển tiếp hoàn hảo.


40
Move-semantics yêu cầu đối tượng di chuyển vẫn còn hiệu lực , đây không phải là trạng thái không chính xác. (Đặt vấn đề: Nó vẫn phải phá hủy, làm cho nó hoạt động.)
GManNickG

13
@GMan: tốt, nó phải ở trong trạng thái an toàn để phá hủy, nhưng, AFAIK, nó không phải sử dụng cho bất cứ điều gì khác.
Zan Lynx

8
@ZanLynx: Phải. Lưu ý rằng thư viện tiêu chuẩn bổ sung yêu cầu các đối tượng di chuyển có thể được gán, nhưng điều này chỉ dành cho các đối tượng được sử dụng trong stdlib, không phải là một yêu cầu chung.
GManNickG

25
-1 "std :: move () là cách C ++ 11 để sử dụng ngữ nghĩa di chuyển" Vui lòng sửa lỗi đó. std::move()không phải là cách sử dụng ngữ nghĩa di chuyển, ngữ nghĩa di chuyển được thực hiện trong suốt cho lập trình viên. movenó chỉ là một diễn viên để chuyển một giá trị từ điểm này sang điểm khác trong đó giá trị ban đầu sẽ không còn được sử dụng.
Manu343726

15
Tôi sẽ đi xa hơn. std::movetự nó "không có gì" - nó không có tác dụng phụ. Nó chỉ báo hiệu cho trình biên dịch rằng lập trình viên không quan tâm điều gì xảy ra với đối tượng đó nữa. tức là nó cho phép các phần khác của phần mềm di chuyển khỏi đối tượng, nhưng nó không yêu cầu nó phải được di chuyển. Trên thực tế, người nhận tham chiếu giá trị không phải thực hiện bất kỳ lời hứa nào về những gì nó sẽ hoặc sẽ không thực hiện với dữ liệu.
Aaron McDaid

241

1. "Chuyện gì vậy?"

Về std::move() mặt kỹ thuật là một chức năng - tôi sẽ nói nó không thực sự là một chức năng . Đó là một loại trình chuyển đổi giữa các cách trình biên dịch xem xét giá trị của biểu thức.

2. "Nó làm gì?"

Điều đầu tiên cần lưu ý là std::move() không thực sự di chuyển bất cứ thứ gì . Nó chuyển đổi một biểu thức từ một giá trị (như một biến được đặt tên) thành một giá trị xvalue . Một xvalue cho trình biên dịch:

Bạn có thể cướp bóc tôi, di chuyển bất cứ thứ gì tôi đang giữ và sử dụng nó ở nơi khác (vì dù sao tôi cũng sẽ bị phá hủy sớm thôi) ".

nói cách khác, khi bạn sử dụng std::move(x), bạn đang cho phép trình biên dịch ăn thịt người x. Do đó, nếu xcó, bộ đệm riêng của nó trong bộ nhớ - sau khi std::move()ing trình biên dịch có thể có một đối tượng khác sở hữu nó thay thế.

Bạn cũng có thể di chuyển từ một giá trị (chẳng hạn như tạm thời bạn đi ngang qua), nhưng điều này hiếm khi hữu ích.

3. "Khi nào nên sử dụng nó?"

Một cách khác để đặt câu hỏi này là "Tôi sẽ ăn cắp tài nguyên của một đối tượng hiện tại để làm gì?" tốt, nếu bạn đang viết mã ứng dụng, có lẽ bạn sẽ không bị rối tung nhiều với các đối tượng tạm thời được tạo bởi trình biên dịch. Vì vậy, chủ yếu bạn sẽ làm điều này ở những nơi như hàm tạo, phương thức toán tử, hàm giống như thuật toán thư viện chuẩn, v.v ... nơi các đối tượng được tạo và hủy tự động rất nhiều. Tất nhiên, đó chỉ là một quy tắc của ngón tay cái.

Một cách sử dụng thông thường là "di chuyển" tài nguyên từ đối tượng này sang đối tượng khác thay vì sao chép. @Guillaume liên kết đến trang này có một ví dụ ngắn gọn: hoán đổi hai đối tượng với ít sao chép.

template <class T>
swap(T& a, T& b) {
    T tmp(a);   // we now have two copies of a
    a = b;      // we now have two copies of b (+ discarded a copy of a)
    b = tmp;    // we now have two copies of tmp (+ discarded a copy of b)
}

sử dụng di chuyển cho phép bạn trao đổi tài nguyên thay vì sao chép chúng xung quanh:

template <class T>
swap(T& a, T& b) {
    T tmp(std::move(a));
    a = std::move(b);   
    b = std::move(tmp);
}

Hãy nghĩ về những gì xảy ra khi T, nói, vector<int>có kích thước n. Trong phiên bản đầu tiên, bạn đọc và viết các phần tử 3 * n, trong phiên bản thứ hai, về cơ bản bạn chỉ đọc và viết 3 con trỏ vào bộ đệm của vectơ, cộng với kích thước của 3 bộ đệm. Tất nhiên, lớp Tcần biết làm thế nào để di chuyển; lớp của bạn nên có một toán tử gán chuyển động và một hàm tạo di chuyển cho lớp Tđể làm việc này.


3
Trong một thời gian dài tôi đã nghe nói về những ngữ nghĩa di chuyển này, tôi không bao giờ nhìn vào chúng. Từ mô tả này, bạn đã đưa ra nó giống như là một bản sao nông thay vì một bản sao sâu.
Zebrafish

7
@TitoneMaurice: Ngoại trừ việc đó không phải là bản sao - vì giá trị ban đầu không còn sử dụng được.
einpoklum

3
@Zebrafish bạn không thể sai nhiều hơn. Một bản sao nông để lại bản gốc trong cùng một trạng thái, một động thái thường dẫn đến bản gốc bị trống hoặc ở trạng thái hợp lệ khác.
rubenvb

17
@rubenvb Zebra không hoàn toàn sai. Mặc dù đúng là đối tượng bị mất khả năng ban đầu thường bị phá hoại một cách có chủ ý để tránh các lỗi khó hiểu (ví dụ: đặt con trỏ của nó thành nullptr để báo hiệu rằng nó không còn sở hữu các con trỏ), thực tế là toàn bộ di chuyển được thực hiện bằng cách sao chép con trỏ từ nguồn đến đích (và cố tình tránh làm bất cứ điều gì với con trỏ) thực sự gợi nhớ đến một bản sao nông. Trong thực tế, tôi sẽ đi xa hơn để nói rằng một động thái một bản sao nông, theo sau là tùy ý bởi sự tự hủy một phần của nguồn. (tt)
Các cuộc đua nhẹ nhàng trong quỹ đạo

3
(tt) Nếu chúng ta cho phép định nghĩa này (và tôi thích nó hơn), thì quan sát của @ Zebrafish không sai, chỉ hơi không đầy đủ.
Các cuộc đua nhẹ nhàng trong quỹ đạo

146

Bạn có thể sử dụng di chuyển khi bạn cần "chuyển" nội dung của một đối tượng ở nơi khác mà không cần sao chép (tức là nội dung không bị trùng lặp, đó là lý do tại sao nó có thể được sử dụng trên một số đối tượng không thể sao chép, như unique_ptr). Cũng có thể một đối tượng lấy nội dung của một đối tượng tạm thời mà không cần sao chép (và tiết kiệm rất nhiều thời gian), với std :: move.

Liên kết này thực sự đã giúp tôi ra:

http://thbecker.net/articles/rvalue_Vferences/section_01.html

Tôi xin lỗi nếu câu trả lời của tôi đến quá muộn, nhưng tôi cũng đang tìm kiếm một liên kết tốt cho std :: move và tôi đã tìm thấy các liên kết ở trên một chút "austere".

Điều này nhấn mạnh vào tham chiếu giá trị r, trong bối cảnh bạn nên sử dụng chúng và tôi nghĩ nó chi tiết hơn, đó là lý do tại sao tôi muốn chia sẻ liên kết này ở đây.


26
Liên kết tốt đẹp. Tôi luôn tìm thấy bài viết trên wikipedia và các liên kết khác mà tôi tình cờ thấy khá khó hiểu vì họ chỉ đưa ra sự thật cho bạn, để lại cho bạn biết ý nghĩa / lý do thực tế là gì. Mặc dù "di chuyển ngữ nghĩa" trong một hàm tạo là khá rõ ràng, nhưng tất cả các chi tiết về việc truyền && - các giá trị xung quanh không phải là ... vì vậy mô tả kiểu hướng dẫn là rất hay.
Christian Stieber

66

Q: là std::movegì?

A: std::move()là một hàm từ Thư viện chuẩn C ++ để chuyển sang tham chiếu giá trị.

Đơn giản std::move(t)là tương đương với:

static_cast<T&&>(t);

Một giá trị là tạm thời không tồn tại ngoài biểu thức xác định nó, chẳng hạn như kết quả hàm trung gian không bao giờ được lưu trữ trong một biến.

int a = 3; // 3 is a rvalue, does not exist after expression is evaluated
int b = a; // a is a lvalue, keeps existing after expression is evaluated

Việc triển khai cho std :: move () được đưa ra trong N2027: "Giới thiệu ngắn gọn về các tham chiếu Rvalue" như sau:

template <class T>
typename remove_reference<T>::type&&
std::move(T&& a)
{
    return a;
}

Như bạn có thể thấy, std::movetrả về T&&bất kể nếu được gọi với một giá trị ( T), kiểu tham chiếu ( T&) hoặc tham chiếu rvalue ( T&&).

Q: Nó làm gì?

A: Là một diễn viên, nó không làm gì trong thời gian chạy. Nó chỉ có liên quan tại thời gian biên dịch để nói với trình biên dịch rằng bạn muốn tiếp tục coi tham chiếu là một giá trị.

foo(3 * 5); // obviously, you are calling foo with a temporary (rvalue)

int a = 3 * 5;
foo(a);     // how to tell the compiler to treat `a` as an rvalue?
foo(std::move(a)); // will call `foo(int&& a)` rather than `foo(int a)` or `foo(int& a)`

Những gì nó không làm:

  • Tạo một bản sao của đối số
  • Gọi hàm tạo
  • Thay đổi đối tượng đối số

Q: Khi nào nên sử dụng?

Trả lời: Bạn nên sử dụng std::movenếu bạn muốn gọi các hàm hỗ trợ di chuyển ngữ nghĩa với một đối số không phải là giá trị (biểu thức tạm thời).

Điều này đặt ra những câu hỏi tiếp theo cho tôi:

  • Di chuyển ngữ nghĩa là gì? Di chuyển ngữ nghĩa trái ngược với ngữ nghĩa sao chép là một kỹ thuật lập trình trong đó các thành viên của một đối tượng được khởi tạo bằng cách 'chiếm lấy' thay vì sao chép các thành viên của đối tượng khác. Việc 'tiếp quản' như vậy chỉ có ý nghĩa với các con trỏ và các thẻ điều khiển tài nguyên, có thể được chuyển giá rẻ bằng cách sao chép con trỏ hoặc số nguyên xử lý thay vì dữ liệu cơ bản.

  • Những loại lớp học và đối tượng hỗ trợ di chuyển ngữ nghĩa? Với tư cách là nhà phát triển, bạn phải triển khai ngữ nghĩa di chuyển trong các lớp của riêng mình nếu những điều này sẽ có lợi từ việc chuyển các thành viên của họ thay vì sao chép chúng. Khi bạn triển khai ngữ nghĩa di chuyển, bạn sẽ trực tiếp hưởng lợi từ công việc từ nhiều lập trình viên thư viện, những người đã thêm hỗ trợ để xử lý các lớp với ngữ nghĩa di chuyển một cách hiệu quả.

  • Tại sao trình biên dịch không thể tự tìm ra? Trình biên dịch không thể gọi quá tải khác của hàm trừ khi bạn nói như vậy. Bạn phải giúp trình biên dịch chọn xem phiên bản hàm thông thường hay di chuyển sẽ được gọi.

  • Trong trường hợp nào tôi muốn nói với trình biên dịch rằng nó nên coi một biến là một giá trị? Điều này rất có thể sẽ xảy ra trong các hàm mẫu hoặc thư viện, nơi bạn biết rằng một kết quả trung gian có thể được cứu vãn.


2
+1 lớn cho các ví dụ mã với ngữ nghĩa trong các bình luận. Các câu trả lời hàng đầu khác xác định std :: move bằng cách sử dụng "move" - ​​không thực sự làm rõ bất cứ điều gì! --- Tôi tin rằng điều đáng nói là việc không tạo một bản sao của đối số có nghĩa là giá trị ban đầu không thể được sử dụng một cách đáng tin cậy.
ty

34

std :: di chuyển chính nó không thực sự làm nhiều. Tôi nghĩ rằng nó được gọi là hàm tạo di chuyển cho một đối tượng, nhưng nó thực sự chỉ thực hiện một kiểu truyền (truyền một biến lvalue thành một giá trị để biến đã nói có thể được chuyển làm đối số cho hàm tạo di chuyển hoặc toán tử gán).

Vì vậy, std :: move chỉ được sử dụng như một tiền thân để sử dụng ngữ nghĩa di chuyển. Di chuyển ngữ nghĩa về cơ bản là một cách hiệu quả để đối phó với các đối tượng tạm thời.

Xem xét đối tượng A = B + C + D + E + F;

Đây là mã nhìn đẹp, nhưng E + F tạo ra một đối tượng tạm thời. Sau đó, D + temp tạo ra một đối tượng tạm thời khác và cứ thế. Trong mỗi toán tử "+" bình thường của một lớp, các bản sao sâu xuất hiện.

Ví dụ

Object Object::operator+ (const Object& rhs) {
    Object temp (*this);
    // logic for adding
    return temp;
}

Việc tạo đối tượng tạm thời trong chức năng này là vô ích - những đối tượng tạm thời này sẽ bị xóa ở cuối dòng khi chúng đi ra khỏi phạm vi.

Chúng ta có thể sử dụng ngữ nghĩa di chuyển để "cướp bóc" các đối tượng tạm thời và làm một cái gì đó như

 Object& Object::operator+ (Object&& rhs) {
     // logic to modify rhs directly
     return rhs;
 }

Điều này tránh các bản sao sâu không cần thiết được thực hiện. Với tham chiếu đến ví dụ, phần duy nhất có sao chép sâu xảy ra bây giờ là E + F. Phần còn lại sử dụng ngữ nghĩa di chuyển. Hàm xây dựng di chuyển hoặc toán tử gán cũng cần được thực hiện để gán kết quả cho A.


3
bạn đã nói về ngữ nghĩa di chuyển. bạn nên thêm vào câu trả lời của mình như cách std :: move có thể được sử dụng vì câu hỏi hỏi về điều đó.
Koushik Shetty

2
@Koushik std :: move không làm được gì nhiều - nhưng được sử dụng để thực hiện ngữ nghĩa di chuyển. Nếu bạn không biết về std :: move, có lẽ bạn cũng không biết về ngữ nghĩa di chuyển
user929404

1
"không làm được gì nhiều" (vâng, chỉ là một static_cast để tham chiếu giá trị). những gì thực sự làm nó và y nó làm là những gì OP yêu cầu. bạn không cần biết std :: move hoạt động như thế nào nhưng bạn phải biết ngữ nghĩa di chuyển làm gì. hơn nữa, "nhưng được sử dụng để thực hiện ngữ nghĩa di chuyển" theo cách khác. biết di chuyển ngữ nghĩa và bạn hiểu std :: di chuyển khác không. di chuyển chỉ giúp trong chuyển động và bản thân nó sử dụng ngữ nghĩa di chuyển. std :: move không làm gì ngoài việc chuyển đổi đối số của nó thành tham chiếu giá trị, đó là điều mà ngữ nghĩa di chuyển yêu cầu.
Koushik Shetty

10
"nhưng E + F tạo ra một đối tượng tạm thời" - Toán tử +đi từ trái sang phải, không phải từ trái sang trái. Do đó B+Csẽ là đầu tiên!
Ajay

8

"Nó là gì?" "Nó làm gì?" đã được giải thích ở trên.

Tôi sẽ đưa ra một ví dụ về "khi nào nó nên được sử dụng".

Ví dụ, chúng ta có một lớp có nhiều tài nguyên như mảng lớn trong đó.

class ResHeavy{ //  ResHeavy means heavy resource
    public:
        ResHeavy(int len=10):_upInt(new int[len]),_len(len){
            cout<<"default ctor"<<endl;
        }

        ResHeavy(const ResHeavy& rhs):_upInt(new int[rhs._len]),_len(rhs._len){
            cout<<"copy ctor"<<endl;
        }

        ResHeavy& operator=(const ResHeavy& rhs){
            _upInt.reset(new int[rhs._len]);
            _len = rhs._len;
            cout<<"operator= ctor"<<endl;
        }

        ResHeavy(ResHeavy&& rhs){
            _upInt = std::move(rhs._upInt);
            _len = rhs._len;
            rhs._len = 0;
            cout<<"move ctor"<<endl;
        }

    // check array valid
    bool is_up_valid(){
        return _upInt != nullptr;
    }

    private:
        std::unique_ptr<int[]> _upInt; // heavy array resource
        int _len; // length of int array
};

Mã kiểm tra:

void test_std_move2(){
    ResHeavy rh; // only one int[]
    // operator rh

    // after some operator of rh, it becomes no-use
    // transform it to other object
    ResHeavy rh2 = std::move(rh); // rh becomes invalid

    // show rh, rh2 it valid
    if(rh.is_up_valid())
        cout<<"rh valid"<<endl;
    else
        cout<<"rh invalid"<<endl;

    if(rh2.is_up_valid())
        cout<<"rh2 valid"<<endl;
    else
        cout<<"rh2 invalid"<<endl;

    // new ResHeavy object, created by copy ctor
    ResHeavy rh3(rh2);  // two copy of int[]

    if(rh3.is_up_valid())
        cout<<"rh3 valid"<<endl;
    else
        cout<<"rh3 invalid"<<endl;
}

đầu ra như sau:

default ctor
move ctor
rh invalid
rh2 valid
copy ctor
rh3 valid

Chúng ta có thể thấy rằng std::movevới move constructorviệc biến đổi tài nguyên dễ dàng.

Trường hợp khác là std::movehữu ích?

std::movecũng có thể hữu ích khi sắp xếp một mảng các phần tử. Nhiều thuật toán sắp xếp (như sắp xếp lựa chọn và sắp xếp bong bóng) hoạt động bằng cách hoán đổi các cặp phần tử. Trước đây, chúng tôi đã phải dùng đến ngữ nghĩa sao chép để thực hiện việc hoán đổi. Bây giờ chúng ta có thể sử dụng ngữ nghĩa di chuyển, hiệu quả hơn.

Nó cũng có thể hữu ích nếu chúng ta muốn di chuyển nội dung được quản lý bởi một con trỏ thông minh sang một con trỏ thông minh khác.

Trích dẫn:


0

Dưới đây là một ví dụ đầy đủ, sử dụng std :: move cho một vectơ tùy chỉnh (đơn giản)

Sản lượng dự kiến:

 c: [10][11]
 copy ctor called
 copy of c: [10][11]
 move ctor called
 moved c: [10][11]

Biên dịch thành:

  g++ -std=c++2a -O2 -Wall -pedantic foo.cpp

Mã số:

#include <iostream>
#include <algorithm>

template<class T> class MyVector {
private:
    T *data;
    size_t maxlen;
    size_t currlen;
public:
    MyVector<T> () : data (nullptr), maxlen(0), currlen(0) { }
    MyVector<T> (int maxlen) : data (new T [maxlen]), maxlen(maxlen), currlen(0) { }

    MyVector<T> (const MyVector& o) {
        std::cout << "copy ctor called" << std::endl;
        data = new T [o.maxlen];
        maxlen = o.maxlen;
        currlen = o.currlen;
        std::copy(o.data, o.data + o.maxlen, data);
    }

    MyVector<T> (const MyVector<T>&& o) {
        std::cout << "move ctor called" << std::endl;
        data = o.data;
        maxlen = o.maxlen;
        currlen = o.currlen;
    }

    void push_back (const T& i) {
        if (currlen >= maxlen) {
            maxlen *= 2;
            auto newdata = new T [maxlen];
            std::copy(data, data + currlen, newdata);
            if (data) {
                delete[] data;
            }
            data = newdata;
        }
        data[currlen++] = i;
    }

    friend std::ostream& operator<<(std::ostream &os, const MyVector<T>& o) {
        auto s = o.data;
        auto e = o.data + o.currlen;;
        while (s < e) {
            os << "[" << *s << "]";
            s++;
        }
        return os;
    }
};

int main() {
    auto c = new MyVector<int>(1);
    c->push_back(10);
    c->push_back(11);
    std::cout << "c: " << *c << std::endl;
    auto d = *c;
    std::cout << "copy of c: " << d << std::endl;
    auto e = std::move(*c);
    delete c;
    std::cout << "moved c: " << e << std::endl;
}
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.