Làm thế nào để std :: move () chuyển các giá trị vào RValues?


104

Tôi chỉ thấy mình không hiểu đầy đủ logic của std::move().

Lúc đầu, tôi truy cập vào nó nhưng có vẻ như chỉ có tài liệu về cách sử dụng std::move()chứ không phải cách hoạt động của cấu trúc.

Ý tôi là, tôi biết hàm thành viên mẫu là gì nhưng khi tôi nhìn vào std::move()định nghĩa trong VS2010, nó vẫn còn khó hiểu.

định nghĩa của std :: move () ở bên dưới.

template<class _Ty> inline
typename tr1::_Remove_reference<_Ty>::_Type&&
    move(_Ty&& _Arg)
    {   // forward _Arg as movable
        return ((typename tr1::_Remove_reference<_Ty>::_Type&&)_Arg);
    }

Điều kỳ lạ đối với tôi đầu tiên là tham số, (_Ty && _Arg), bởi vì khi tôi gọi hàm như bạn thấy bên dưới,

// main()
Object obj1;
Object obj2 = std::move(obj1);

về cơ bản nó tương đương với

// std::move()
_Ty&& _Arg = Obj1;

Nhưng như bạn đã biết, bạn không thể liên kết trực tiếp LValue với tham chiếu RValue, điều này khiến tôi nghĩ rằng nó phải như thế này.

_Ty&& _Arg = (Object&&)obj1;

Tuy nhiên, điều này là vô lý vì std :: move () phải hoạt động với tất cả các giá trị.

Vì vậy, tôi đoán để hiểu đầy đủ cách hoạt động của nó, tôi cũng nên xem các cấu trúc này.

template<class _Ty>
struct _Remove_reference
{   // remove reference
    typedef _Ty _Type;
};

template<class _Ty>
struct _Remove_reference<_Ty&>
{   // remove reference
    typedef _Ty _Type;
};

template<class _Ty>
struct _Remove_reference<_Ty&&>
{   // remove rvalue reference
    typedef _Ty _Type;
};

Thật không may, nó vẫn khó hiểu và tôi không hiểu.

Tôi biết rằng tất cả là do tôi thiếu kỹ năng cú pháp cơ bản về C ++. Tôi muốn biết cách thức hoạt động của những thứ này và bất kỳ tài liệu nào tôi có thể lấy trên internet sẽ được hoan nghênh hơn cả. (Nếu bạn có thể giải thích điều này, điều đó cũng sẽ tuyệt vời).


31
@NicolBolas Ngược lại, bản thân câu hỏi cho thấy OP đang tự đánh giá thấp mình trong câu nói đó. Chính sự nắm bắt của OP về các kỹ năng cú pháp cơ bản cho phép họ đặt ra câu hỏi.
Kyle Strand

Dù sao thì tôi cũng không đồng ý - bạn có thể học nhanh hoặc đã có kiến ​​thức tốt về khoa học máy tính hoặc lập trình nói chung, ngay cả trong C ++, và vẫn phải vật lộn với cú pháp. Tốt hơn hết là bạn nên dành thời gian tìm hiểu cơ chế, thuật toán, v.v. nhưng dựa vào các tài liệu tham khảo để tìm hiểu cú pháp khi cần thiết, còn hơn là phải hiểu rõ ràng về mặt kỹ thuật nhưng lại có hiểu biết nông cạn về những thứ như copy / move / forward. Làm thế nào bạn phải biết "khi nào và làm thế nào để sử dụng std :: move khi bạn muốn di chuyển cấu trúc một cái gì đó" trước khi bạn biết những gì di chuyển làm?
John P

Tôi nghĩ rằng tôi đến đây để hiểu cách thức movehoạt động hơn là cách nó được triển khai. Tôi thấy lời giải thích này thực sự hữu ích: pagefault.blog/2018/03/01/… .
Anton Daneyko

Câu trả lời:


171

Chúng tôi bắt đầu với chức năng di chuyển (mà tôi đã làm sạch một chút):

template <typename T>
typename remove_reference<T>::type&& move(T&& arg)
{
  return static_cast<typename remove_reference<T>::type&&>(arg);
}

Hãy bắt đầu với phần dễ dàng hơn - đó là khi hàm được gọi với rvalue:

Object a = std::move(Object());
// Object() is temporary, which is prvalue

movemẫu của chúng tôi được khởi tạo như sau:

// move with [T = Object]:
remove_reference<Object>::type&& move(Object&& arg)
{
  return static_cast<remove_reference<Object>::type&&>(arg);
}

remove_referencechuyển đổi T&thành Thoặc T&&thành T, và Objectkhông phải là tham chiếu, chức năng cuối cùng của chúng ta là:

Object&& move(Object&& arg)
{
  return static_cast<Object&&>(arg);
}

Bây giờ, bạn có thể tự hỏi: chúng ta có cần dàn diễn viên không? Câu trả lời là: có, chúng tôi làm. Lý do rất đơn giản; tham chiếu rvalue có tên được coi là lvalue (và việc chuyển đổi ngầm định từ tham chiếu lvalue sang rvalue bị cấm theo tiêu chuẩn).


Đây là những gì sẽ xảy ra khi chúng ta gọi movevới lvalue:

Object a; // a is lvalue
Object b = std::move(a);

movekhởi tạo tương ứng :

// move with [T = Object&]
remove_reference<Object&>::type&& move(Object& && arg)
{
  return static_cast<remove_reference<Object&>::type&&>(arg);
}

Một lần nữa, remove_referencechuyển đổi Object&thành Objectvà chúng tôi nhận được:

Object&& move(Object& && arg)
{
  return static_cast<Object&&>(arg);
}

Bây giờ chúng ta đến phần khó: Object& &&thậm chí có nghĩa là gì và làm thế nào nó có thể liên kết với lvalue?

Để cho phép chuyển tiếp hoàn hảo, tiêu chuẩn C ++ 11 cung cấp các quy tắc đặc biệt để thu gọn tham chiếu, như sau:

Object &  &  = Object &
Object &  && = Object &
Object && &  = Object &
Object && && = Object &&

Như bạn có thể thấy, theo các quy tắc này Object& &&thực sự có nghĩa là Object&, đó là tham chiếu giá trị đơn giản cho phép các giá trị liên kết.

Chức năng cuối cùng là:

Object&& move(Object& arg)
{
  return static_cast<Object&&>(arg);
}

điều này không giống như khởi tạo trước đó với rvalue - cả hai đều chuyển đối số của nó thành tham chiếu rvalue và sau đó trả về nó. Sự khác biệt là bản tạo đầu tiên chỉ có thể được sử dụng với các giá trị, trong khi phần thứ hai hoạt động với các giá trị.


Để giải thích tại sao chúng ta cần remove_referencethêm một chút nữa, hãy thử hàm này

template <typename T>
T&& wanna_be_move(T&& arg)
{
  return static_cast<T&&>(arg);
}

và khởi tạo nó bằng lvalue.

// wanna_be_move [with T = Object&]
Object& && wanna_be_move(Object& && arg)
{
  return static_cast<Object& &&>(arg);
}

Áp dụng các quy tắc thu gọn tham chiếu được đề cập ở trên, bạn có thể thấy chúng ta nhận được hàm không sử dụng được move(nói một cách đơn giản, bạn gọi nó bằng lvalue, bạn nhận lại lvalue). Nếu bất cứ điều gì, chức năng này là chức năng nhận dạng.

Object& wanna_be_move(Object& arg)
{
  return static_cast<Object&>(arg);
}

3
Câu trả lời rất hay. Mặc dù tôi hiểu rằng đối với một giá trị trái nó là một ý tưởng tốt để đánh giá Tnhư Object&, tôi đã không biết rằng điều này thực sự làm. Tôi đã mong đợi Tcũng đánh giá Objecttrong trường hợp này, vì tôi nghĩ đây là lý do để giới thiệu các tham chiếu trình bao bọc và std::ref, hoặc không phải nó.
Christian Rau

2
Có một sự khác biệt giữa template <typename T> void f(T arg)(là những gì có trong bài viết Wikipedia) và template <typename T> void f(T& arg). Cái đầu tiên phân giải thành giá trị (và nếu bạn muốn chuyển tham chiếu, bạn phải bọc nó vào std::ref), trong khi cái thứ hai luôn phân giải thành tham chiếu. Đáng buồn thay, các quy tắc để suy luận đối số mẫu khá phức tạp, vì vậy tôi không thể cung cấp lý do chính xác tại sao lại T&&cộng hưởng Object& &&(nhưng nó thực sự xảy ra).
Vitus

1
Nhưng, có lý do gì mà cách tiếp cận trực tiếp này không hoạt động? template <typename T> T&& also_wanna_be_move(T& arg) { return static_cast<T&&>(arg); }
greggo

1
Điều đó giải thích tại sao remove_reference là cần thiết, nhưng tôi vẫn không hiểu tại sao hàm phải nhận T && (thay vì T &). Nếu tôi hiểu chính xác lời giải thích này, thì việc bạn nhận được T && hay T & sẽ không thành vấn đề vì cả hai cách, remove_reference sẽ chuyển nó thành T, sau đó bạn sẽ thêm lại &&. Vì vậy, tại sao không nói rằng bạn chấp nhận T & (về mặt ngữ nghĩa là những gì bạn đang chấp nhận), thay vì T && và dựa vào chuyển tiếp hoàn hảo để cho phép người gọi thông qua T &?
mgiuca

3
@mgiuca: Nếu bạn chỉ muốn std::movechuyển các giá trị thành giá trị, thì có, T&sẽ ổn. Thủ thuật này được thực hiện hầu hết vì sự linh hoạt: bạn có thể gọi std::movemọi thứ (bao gồm cả giá trị) và lấy lại giá trị.
Vitus

4

_Ty là một tham số mẫu và trong tình huống này

Object obj1;
Object obj2 = std::move(obj1);

_Ty là loại "Đối tượng &"

đó là lý do tại sao _Remove_reference là cần thiết.

Nó sẽ giống hơn

typedef Object& ObjectRef;
Object obj1;
ObjectRef&& obj1_ref = obj1;
Object&& obj2 = (Object&&)obj1_ref;

Nếu chúng tôi không xóa tham chiếu, nó sẽ giống như chúng tôi đang làm

Object&& obj2 = (ObjectRef&&)obj1_ref;

Nhưng ObjectRef && giảm thành Object &, mà chúng tôi không thể liên kết với obj2.

Lý do nó giảm theo cách này là để hỗ trợ chuyển tiếp hoàn hảo. Xem bài báo này .


Điều này không giải thích tất cả mọi thứ về lý do tại sao _Remove_reference_cần thiết. Ví dụ, nếu bạn có Object&là một typedef và bạn tham chiếu đến nó, bạn vẫn nhận được Object&. Tại sao điều đó không hoạt động với &&? có một câu trả lời đó, và nó đã làm với chuyển tiếp hoàn hảo.
Nicol Bolas

2
Thật. Và câu trả lời là rất thú vị. A & && được giảm thành A &, vì vậy nếu chúng tôi cố gắng sử dụng (ObjectRef &&) obj1_ref, chúng tôi sẽ nhận được Object & thay thế trong trường hợp này.
Vaughn Cato
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.