Câu hỏi Ngữ nghĩa di chuyển là gì?


Tôi vừa nghe xong radio Kỹ thuật phần mềm phỏng vấn podcast với Scott Meyers về C ++ 0x. Hầu hết các tính năng mới có ý nghĩa với tôi, và tôi thực sự vui mừng về C ++ 0x bây giờ, ngoại trừ một. Tôi vẫn không hiểu di chuyển ngữ nghĩa... Chúng chính xác là gì?


1376
2018-06-23 22:46


gốc


Tôi đã tìm thấy [bài viết trên blog của Eli Bendersky] (eli.thegreenplace.net/2011/12/15/…) về lvalues ​​và rvalues ​​trong C và C ++ khá nhiều thông tin. Ông cũng đề cập đến các tài liệu tham khảo rvalue trong C ++ 11 và giới thiệu chúng với các ví dụ nhỏ. - Nils
Triển lãm của Alex Allain về chủ đề được viết rất tốt. - Patrick Sanan
Mỗi năm hay như vậy tôi tự hỏi những gì các ngữ nghĩa di chuyển "mới" trong C ++ là tất cả về, tôi google nó và nhận được vào trang này. Tôi đọc câu trả lời, bộ não của tôi tắt. Tôi quay lại C và quên hết mọi thứ! Tôi bế tắc. - sky


Các câu trả lời:


Tôi tìm thấy nó dễ nhất để hiểu ngữ nghĩa di chuyển với mã ví dụ. Hãy bắt đầu với một lớp chuỗi rất đơn giản mà chỉ giữ một con trỏ tới một khối được cấp phát bộ nhớ heap:

#include <cstring>
#include <algorithm>

class string
{
    char* data;

public:

    string(const char* p)
    {
        size_t size = strlen(p) + 1;
        data = new char[size];
        memcpy(data, p, size);
    }

Vì chúng tôi đã chọn tự quản lý bộ nhớ, chúng tôi cần phải tuân theo quy tắc của ba. Tôi sẽ trì hoãn việc viết toán tử gán và chỉ thực hiện hàm hủy và trình tạo bản sao cho bây giờ:

    ~string()
    {
        delete[] data;
    }

    string(const string& that)
    {
        size_t size = strlen(that.data) + 1;
        data = new char[size];
        memcpy(data, that.data, size);
    }

Các nhà xây dựng bản sao xác định những gì nó có nghĩa là để sao chép các đối tượng chuỗi. Thông số const string& that liên kết với tất cả các biểu thức của chuỗi kiểu cho phép bạn tạo bản sao trong các ví dụ sau:

string a(x);                                    // Line 1
string b(x + y);                                // Line 2
string c(some_function_returning_a_string());   // Line 3

Bây giờ đến cái nhìn sâu sắc chính vào ngữ nghĩa di chuyển. Lưu ý rằng chỉ trong dòng đầu tiên mà chúng tôi sao chép x bản sao sâu này thực sự cần thiết, bởi vì chúng tôi có thể muốn kiểm tra x sau đó và sẽ rất ngạc nhiên nếu x đã thay đổi bằng cách nào đó. Bạn có nhận thấy tôi đã nói như thế nào không x ba lần (bốn lần nếu bạn bao gồm câu này) và có nghĩa là chính xác cùng một đối tượng mỗi lần? Chúng tôi gọi các biểu thức như x "lvalues".

Các đối số trong dòng 2 và 3 không phải là các giá trị, mà là các giá trị, vì các đối tượng chuỗi bên dưới không có tên, vì vậy máy khách không có cách nào để kiểm tra lại chúng tại một thời điểm sau đó. rvalues ​​biểu thị các đối tượng tạm thời bị hủy tại dấu chấm phẩy tiếp theo (chính xác hơn: ở cuối biểu thức đầy đủ có chứa giá trị rexue). Điều này là quan trọng bởi vì trong quá trình khởi tạo b và c, chúng tôi có thể làm bất cứ điều gì chúng tôi muốn với chuỗi nguồn và khách hàng không thể nói sự khác biệt!

C ++ 0x giới thiệu một cơ chế mới gọi là "tham chiếu rvalue", trong số những thứ khác, cho phép chúng tôi phát hiện các đối số rvalue thông qua quá tải hàm. Tất cả những gì chúng ta phải làm là viết một hàm tạo với tham số tham chiếu rvalue. Bên trong nhà xây dựng mà chúng ta có thể làm bất cứ điều gì chúng tôi muốn với nguồn, miễn là chúng ta để nó vào một số trạng thái hợp lệ:

    string(string&& that)   // string&& is an rvalue reference to a string
    {
        data = that.data;
        that.data = nullptr;
    }

Chúng ta đã làm gì ở đây? Thay vì sao chép sâu dữ liệu heap, chúng ta vừa sao chép con trỏ và sau đó thiết lập con trỏ ban đầu thành null. Thực tế, chúng tôi đã "đánh cắp" dữ liệu ban đầu thuộc về chuỗi nguồn. Một lần nữa, thông tin chi tiết quan trọng là trong mọi trường hợp, khách hàng có thể phát hiện rằng nguồn đã được sửa đổi. Vì chúng ta không thực sự làm một bản sao ở đây, chúng ta gọi hàm tạo này là một "hàm tạo di chuyển". Công việc của nó là di chuyển tài nguyên từ một đối tượng này sang đối tượng khác thay vì sao chép chúng.

Xin chúc mừng, bây giờ bạn đã hiểu những khái niệm cơ bản về ngữ nghĩa di chuyển! Hãy tiếp tục bằng cách thực hiện toán tử gán. Nếu bạn không quen với sao chép và hoán đổi thành ngữ, tìm hiểu nó và trở lại, bởi vì nó là một thành ngữ C ++ tuyệt vời liên quan đến an toàn ngoại lệ.

    string& operator=(string that)
    {
        std::swap(data, that.data);
        return *this;
    }
};

Huh, đúng không? "Tài liệu tham khảo rvalue ở đâu?" bạn có thể hỏi. "Chúng ta không cần nó ở đây!" là câu trả lời của tôi :)

Lưu ý rằng chúng tôi chuyển thông số that  theo giá trị, vì thế that phải được khởi tạo giống như bất kỳ đối tượng chuỗi nào khác. Chính xác như thế nào là that sẽ được khởi tạo? Trong những ngày xa xưa của C ++ 98, câu trả lời sẽ là "bởi người tạo bản sao". Trong C ++ 0x, trình biên dịch chọn giữa hàm tạo bản sao và hàm tạo di chuyển dựa trên việc đối số cho toán tử gán có phải là một giá trị hay một giá trị.

Vì vậy, nếu bạn nói a = b, các -bộ tạo bản sao sẽ khởi tạo that (bởi vì biểu thức b là một lvalue), và toán tử gán hoán đổi nội dung với một bản sao sâu, mới được tạo ra. Đó là định nghĩa của thành phần sao chép và hoán đổi - tạo một bản sao, hoán đổi nội dung với bản sao, và sau đó loại bỏ bản sao bằng cách rời khỏi phạm vi. Không có gì mới ở đây.

Nhưng nếu bạn nói a = x + y, các di chuyển constructor sẽ khởi tạo that (bởi vì biểu thức x + y là một rvalue), do đó, không có bản sao sâu tham gia, chỉ có một động thái hiệu quả. that vẫn là một đối tượng độc lập từ lập luận, nhưng việc xây dựng của nó là tầm thường, vì dữ liệu heap không phải được sao chép, chỉ cần di chuyển. Nó không cần thiết để sao chép nó bởi vì x + y là một rvalue, và một lần nữa, nó là okay để di chuyển từ các đối tượng chuỗi ký hiệu bởi rvalues.

Tóm lại, các nhà xây dựng bản sao làm cho một bản sao sâu, bởi vì nguồn phải được giữ nguyên. Mặt khác, hàm tạo di chuyển chỉ có thể sao chép con trỏ và sau đó đặt con trỏ trong nguồn thành null. Bạn có thể "vô hiệu hóa" đối tượng nguồn theo cách này, bởi vì máy khách không có cách nào kiểm tra đối tượng một lần nữa.

Tôi hy vọng ví dụ này có điểm chính trên. Có rất nhiều thứ để rvalue tài liệu tham khảo và di chuyển ngữ nghĩa mà tôi cố ý bỏ ra để giữ cho nó đơn giản. Nếu bạn muốn biết thêm chi tiết xin vui lòng xem câu trả lời bổ sung của tôi.


2038
2018-06-24 12:40



Câu trả lời tuyệt vời, làm cho nó thực sự rõ ràng. Bất kỳ liên kết đến những điều bạn bỏ ra? - Phil H
@ Nhưng nếu ctor của tôi là nhận được một rvalue, mà không bao giờ có thể được sử dụng sau này, tại sao tôi thậm chí cần phải bận tâm để lại nó trong một trạng thái nhất quán / an toàn? Thay vì thiết lập that.data = 0, tại sao không chỉ để nó? - einpoklum
@einpoklum Vì không có that.data = 0, các nhân vật sẽ bị phá hủy quá sớm (khi tạm thời chết), và cũng hai lần. Bạn muốn ăn cắp dữ liệu, không chia sẻ nó! - fredoverflow
@einpoklum Trình phá hủy theo lịch trình thường xuyên vẫn được chạy, vì vậy bạn phải đảm bảo rằng trạng thái sau di chuyển của đối tượng nguồn không gây ra sự cố. Tốt hơn, bạn nên đảm bảo đối tượng nguồn cũng có thể là người nhận bài tập hoặc bài viết khác. - CTMacUser
@pranitkothari Có, tất cả các đối tượng phải bị hủy, thậm chí di chuyển từ các đối tượng. Và vì chúng ta không muốn mảng char bị xóa khi điều đó xảy ra, chúng ta phải đặt con trỏ thành null. - fredoverflow


Câu trả lời đầu tiên của tôi là giới thiệu cực kỳ đơn giản hóa để di chuyển ngữ nghĩa, và nhiều chi tiết được bỏ ra nhằm mục đích giữ cho nó đơn giản. Tuy nhiên, có nhiều thứ hơn để di chuyển ngữ nghĩa, và tôi nghĩ đã đến lúc trả lời thứ hai để lấp đầy khoảng trống. Câu trả lời đầu tiên đã khá cũ, và nó không cảm thấy đú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ĩ nó vẫn là một phần 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 rất nhiều, Stephan!

Giới thiệu

Di chuyển ngữ nghĩa cho phép một đối tượng, trong các điều kiện nhất định, để 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 quan trọng theo hai cách:

  1. Biến các bản sao đắt tiền thành những độ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ó), việc chuyển ngữ nghĩa 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 đó, việc sao chép một đối tượng và di chuyển một đối tượng có nghĩa là chính xác điều tương tự:

    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
    
        // ...
    };
    
  2. Triển khai các loại "di chuyển" an toàn; đó là, các loại mà việc sao chép không có ý nghĩa, nhưng di chuyển thì không. Ví dụ bao gồm khóa, xử lý tệp và con trỏ thông minh với ngữ nghĩa quyền 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 được dùng nữa, được thay thế bằng std::unique_ptr trong C ++ 11. Trình lập trình C ++ trung gian có lẽ ít nhất là quen thuộc với std::auto_ptr, và vì "ngữ nghĩa di chuyển" nó hiển thị, nó có vẻ giống như 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 quyền sở hữu duy nhất được gọi là std::auto_ptr<T>. Trong trường hợp bạn không quen thuộc với auto_ptr, mục đích của nó là để đảm bảo rằng một đối tượng được cấp phát động luôn được phát hành, ngay cả khi đối mặt với 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 bất thường về 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 khởi tạo b với a làm không phải sao chép tam giác, nhưng thay vào đó chuyển quyền sở hữu của tam giác từ a đến b. Chúng tôi cũng nói "a Là di chuyển vào  b"hoặc" tam giác là đã di chuyển từ a  đến  bĐiều này nghe có vẻ khó hiểu, bởi vì bản thân hình 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 đối tượng khác.

Người tạo bản sao của 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
}

Động thái nguy hiểm và vô hại

Điều nguy hiểm về auto_ptr là những gì mà cú pháp 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 di 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 auto_ptr sau khi nó đã được 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à luôn luôn nguy hiểm. Các chức năng của nhà máy là trường hợp sử dụng hoàn hảo 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ụ đều 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ọ gọi hành vi không xác định, trong khi một trong những khác không. Vậy sự khác nhau giữa các biểu thức là gì a và make_triangle()? Cả hai đều không cùng loại? Thật vậy, nhưng chúng có sự khác biệt danh mục giá trị.

Danh mục 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 a biểu thị một auto_ptr biến và biểu thức make_triangle() biểu thị cuộc gọi của hàm trả về auto_ptr theo giá trị, do đó tạo ra một tạm thời tươi auto_ptr đối tượng mỗi khi nó được gọi. a là một ví dụ về lvalue, trong khi make_triangle() là một ví dụ về rvalue.

Di chuyển từ các giá trị như a là nguy hiểm, bởi vì sau này chúng tôi có thể cố gắng gọi một chức năng 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 các nhà xây dựng sao chép đã thực hiện công việc của mình, chúng tôi không thể sử dụng tạm thời một lần nữa. Không có biểu hiện nào biểu thị tạm thời; nếu chúng ta chỉ viết make_triangle()một lần nữa, chúng tôi nhận được khác nhau tạm thời. Trong thực tế, di chuyển từ tạm thời đã đ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 tay phải của một bài tập. Điều này không còn đúng trong C ++ nữa, vì có các giá trị không thể xuất hiện ở phía bên tay trái của một phép gán (như các 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 kiểu lớp) với một toán tử gán).

Một rvalue 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 hoàn cảnh bình thường, không có biểu thức nào khác trong cùng một phạm vi biểu thị cùng một đối tượng tạm thời.

Tham chiếu Rvalue

Bây giờ chúng ta hiểu rằng việc di chuyển từ lvalues ​​có khả năng nguy hiểm, nhưng việc di chuyển từ rvalues ​​là vô hại. Nếu C ++ có hỗ trợ ngôn ngữ để phân biệt các đối số lvalue từ các đối số rvalue, chúng ta có thể hoàn toàn cấm chuyển từ các giá trị, hoặc ít nhất là chuyển từ các giá trị rõ ràng tại trang web cuộc gọi, để chúng tôi không còn di chuyển do tai nạn nữa.

Câu trả lời của C ++ 11 cho vấn đề này là tham chiếu rvalue. Tham chiếu rvalue là một kiểu tham chiếu mới chỉ liên kết với các giá trị và cú pháp là X&&. Tài liệu tham khảo cũ tốt X& bây giờ được gọi là tham chiếu lvalue. (Lưu ý rằng X&& Là không phải tham chiếu đến tham chiếu; không có điều gì trong C ++.)

Nếu chúng ta ném const vào hỗn hợp, chúng tôi đã có bốn loại tài liệu tham khảo khác nhau. Loại biểu thức nào của loại X 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ị giới hạn đọc từ các giá trị không phải là rất hữu ích.

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

Tham chiếu Rvalue đã trải qua nhiều phiên bản. Kể từ phiên bản 2.1, một tham chiếu rvalue X&& cũng liên kết với tất cả các loại giá trị của một loại khác Y, miễn là có chuyển đổi tiềm ẩn từ Y đến X. Trong trường hợp đó, một loại tạm thời X được tạo và tham chiếu rvalue 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 loại giá trị const char[12]. Vì có chuyển đổi tiềm ẩn từ const char[12] xuyên qua const char* đến std::string, tạm thời loại std::string được tạo và r bị ràng buộc với điều đó tạm thời. Đây là một trong những trường hợp sự khác biệt giữa các giá trị (biểu thức) và thời gian (đối tượng) hơi mờ.

Di chuyển các nhà thầu

Một ví dụ hữu ích về một hàm với một X&& tham số là di chuyển constructor  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ởi std::unique_ptr<T> lợi 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 của unique_ptr. Đầu tiên, chúng tôi gói gọn một con trỏ thô và quá tải các toán tử -> và *, vì vậy lớp học của chúng tôi cảm thấy giống 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 khởi tạo lấy 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ị, các nhà xây dựng di chuyển:

    unique_ptr(unique_ptr&& source)   // note the rvalue reference
    {
        ptr = source.ptr;
        source.ptr = nullptr;
    }

Nhà xây dựng di chuyển này thực hiện chính xác những gì auto_ptr copy constructor đã làm, nhưng nó chỉ có thể được cung cấp với rvalues:

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, bởi vì a là một giá trị, nhưng tham số unique_ptr&& source chỉ có thể bị ràng buộc với giá trị. Đây chính là điều chúng tôi muốn; di chuyển nguy hiểm không bao giờ nên ngầm. Dòng thứ ba biên dịch tốt, bởi vì make_triangle() là một giá trị. Nhà xây dựng 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 là chính xác những gì chúng tôi muốn.

Hàm khởi tạo 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. Công việc của nó là giải phóng tài nguyên cũ và thu lấy 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 thực thi này của toán tử gán di chuyển 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ữ sao chép và hoán đổi không? Nó cũng có thể được áp dụng để di chuyển ngữ nghĩa như là thành ngữ chuyển-và-hoán đổi:

    unique_ptr& operator=(unique_ptr source)   // note the missing reference
    {
        std::swap(ptr, source.ptr);
        return *this;
    }
};

Bây giờ thì source là một loại biến unique_ptr, nó sẽ được khởi tạo bởi hàm khởi tạo di chuyển; có nghĩa 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ì hàm tạo của hàm tạo di chuyển có tham số tham chiếu rvalue. Khi dòng điều khiển đạt đến cú đúp đóng của operator=, source đi ra khỏi phạm vi, tự động phát hành tài nguyên cũ.

Toán tử gán 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, giải phóng tài nguyên cũ.   Thành ngữ di chuyển và hoán đổi đơn giản hóa việc triển khai thực hiện.

Chuyển từ lvalues

Đôi khi, chúng tôi muốn chuyển từ giá trị. Đó là, đôi khi chúng ta muốn trình biên dịch xử lý một giá trị như thể nó là một rvalue, vì vậy nó có thể gọi hàm tạo di chuyển, mặc dù nó có thể có khả năng 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 chuẩn được gọi là std::move bên trong tiêu đề <utility>. Tên này hơi không may, bởi vì std::move chỉ đơn giản là đúc một giá trị bằng một giá trị; nó có không phải di chuyển bất cứ điều gì của chính nó. Nó chỉ đơn thuần là cho phép di chuyển. Có lẽ nó nên được đặt tên std::cast_to_rvalue hoặc là std::enable_move, nhưng chúng tôi đang mắc kẹt với tên của bây giờ.

Đây là cách bạn di chuyển một cách rõ ràng từ một lvalue:

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 nữa. Không sao đâu, bởi vì một cách rõ ràng viết std::move(a), chúng tôi đã thực hiện ý định của chúng tôi rõ ràng: "Dear constructor, làm bất cứ điều gì bạn muốn với a để khởi tạo c; Tôi không quan tâm a nữa không. Cảm thấy tự do để có con đường của bạn với a. "

std::move(some_lvalue) gộp một giá trị vào một giá trị, do đó cho phép di chuyển tiếp theo.

Xvalues

Lưu ý rằng mặc dù std::move(a) là một rvalue, đánh giá của nó không không phải tạo 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 đưa ra một loại giá trị thứ ba. Một cái gì đó có thể được ràng buộc với một tham chiếu rvalue, mặc dù nó không phải là một rvalue theo nghĩa truyền thống, được gọi là xvalue (giá trị eXpiring). Các giá trị truyền thống đã được đổi tên thành prvalues (Giá trị thuần túy).

Cả hai giá trị và xvalues ​​đều là giá trị. Xvalues ​​và lvalues ​​đều là glvalues (Lvalues ​​tổng quát). 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 ​​mới thực sự là mới; phần còn lại chỉ là do đổi tên và nhóm.

C ++ 98 rvalues ​​được gọi là prvalues ​​trong C ++ 11. Tinh thần thay thế tất cả các lần xuất hiện của "rvalue" trong các đoạn trước với "prvalue".

Di chuyển ra khỏi chức năng

Cho đến nay, chúng ta đã thấy chuyển động thành 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 tuyên bố như một đố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 (các biến cục bộ không được khai báo là static) cũng có thể là ngầm di chuyển ra khỏi chức năng:

unique_ptr<Shape> make_square()
{
    unique_ptr<Shape> result(new Square);
    return result;   // note the missing std::move
}

Làm thế nào đến các nhà xây dựng di chuyển chấp nhận lvalue result như một đối số? Phạm vi result sắp kết thúc, và nó sẽ bị phá hủy trong quá trình thư giãn. 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 vớ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 hàm mà không phải viết std::move. Trong thực tế, bạn nên không bao giờ sử dụng std::move để di chuyển các đối tượng tự động ra khỏi các hàm, vì điều này ức chế "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 các chức năng.

Lưu ý rằng trong cả hai hàm của nhà máy, kiểu trả về là một giá trị, không phải là một tham chiếu rvalue. 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ả về một tham chiếu đến một đố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ả về các đối tượng tự động bằng tham chiếu rvalue. Di chuyển được thực hiện độc quyền bởi hàm tạo di chuyển, không phải bởi std::movevà không phải chỉ đơn thuần ràng buộc một giá trị rvalue vào một tham chiếu rvalue.

Chuyển vào 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 lvalue. Nếu bạn nhìn vào kiểu của nó, bạn thấy một tham chiếu rvalue, nhưng một tham chiếu rvalue đơn giản có nghĩa là "một tham chiếu ràng buộc với một giá trị"; nó có không phải có nghĩa là tham chiếu chính nó là một giá trị! Thật, parameter chỉ là một biến thông thường với một cái tên. Bạn có thể dùng parameter thường xuyên như bạn thích bên trong cơ thể của nhà xây dựng, và nó luôn luôn biểu thị cùng một đối tượng. Chuyển động hoàn toàn từ nó sẽ nguy hiểm, do đó ngôn ngữ cấm nó.

Tham chiếu rvalue được đặt tên là một giá trị, giống như bất kỳ biến nào khác.

Giải pháp là để cho phép di chuyển theo cách thủ công:

class Foo
{
    unique_ptr<Shape> member;

public:

    Foo(unique_ptr<Shape>&& parameter)
    : member(std::move(parameter))   // note the std::move
    {}
};

Bạn có thể tranh luận rằng parameter 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 để chèn âm thầm 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 trình biên dịch. Ví dụ, nếu cơ thể của hàm tạo trong 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 biểu tượng để xác định liệu từ định danh có hay không sau 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 ít lộn xộn trong giao diện.

Chức năng thành viên đặc biệt

C ++ 98 khai báo ngầm định ba hàm thành viên đặc biệt theo yêu cầu, nghĩa là khi chúng cần ở đâu đó: trình tạo bản sao, toán tử gán bản sao và hàm hủy.

X::X(const X&);              // copy constructor
X& X::operator=(const X&);   // copy assignment operator
X::~X();                     // destructor

Tham chiếu Rvalue đã trải qua nhiều phiên bản. Kể từ phiên bản 3.0, C ++ 11 khai báo 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 di chuyển. Lưu ý rằng cả VC10 lẫn 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 hoàn toàn 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 khởi tạo di chuyển của riêng bạn hoặc di chuyển toán tử gán, thì cả nhà xây dựng bản sao lẫn toán tử gán bản sao đều sẽ được khai báo ngầm định.

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 không có tài nguyên không được quản lý, bạn không cần tự khai báo bất kỳ hàm nào trong năm hàm thành viên đặc biệt này 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ự mình 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 học của bạn không được hưởng lợi từ ngữ nghĩa di chuyển, không cần phải thực hiện các thao tác di chuyển đặc biệt.

Lưu ý rằng toán tử gán bản sao 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 duy nhất, thống nhất, lấy đối số 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 các chức năng thành viên đặc biệt để thực hiện giảm từ năm đến bốn. Có một sự cân bằng giữa ngoại lệ-an toàn và hiệu quả ở đây, nhưng tôi không phải là một chuyên gia về vấn đề này.

Tham chiếu chuyển tiếp (trước đây được biết như Tham chiếu toàn cầu)

Hãy xem xét mẫu chức năng 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ị, vì ngay từ cái nhìn đầu tiên, nó trông giống như một tham chiếu rvalue. Khi nó quay ra, T&& cũng liên kết với các 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 luận là X, vì thế T&& có nghĩa X&&. Đây là những gì mọi người mong đợi. Nhưng nếu đối số là một loại giá trị của loại X, do một quy tắc đặc biệt, T được suy luận là X&, vì thế T&& có nghĩa là một cái gì đó như X& &&. Nhưng vì C ++ vẫn không có khái niệm tham chiếu đến tham chiếu, kiểu X& && Là sụp đổ vào X&. Điều này nghe có vẻ khó hiểu và vô dụng lúc đầu, nhưng sự sụp đổ tham chiếu là điều cần thiết cho chuyển tiếp hoàn hảo (sẽ không được thảo luận ở đây).

T && không phải là tham chiếu rvalue, mà là tham chiếu chuyển tiếp. Nó cũng liên kết với các giá trị, trong trường hợp này T và T&& là cả hai tham chiếu lvalue.

Nếu bạn muốn hạn chế một mẫu hàm để định giá trị, bạn có thể kết hợp SFINAE với các đặc điểm kiểu:

#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 đổ tài liệu tham khảo, đây là cách std::move được thực thi:

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

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ề một tham chiếu rvalue. Các std::remove_reference<T>::type cuộc gọi hàm meta là cần thiết bởi vì nếu không, đối với các loại giá trị X, kiểu trả về sẽ là X& &&, sẽ sụp đổ thành X&. Vì t luôn luôn là một lvalue (hãy nhớ rằng một tham chiếu rvalue được đặt tên là một lvalue), nhưng chúng ta muốn ràng buộc t với tham chiếu rvalue, chúng ta phải cast một cách rõ ràng t đúng kiểu trả về. 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 ​​xuất phát từ đâu;)

Cuộc gọi của hàm trả về tham chiếu rvalue, chẳng hạn như std::move, là một xvalue.

Lưu ý rằng việc trả về bằng tham chiếu rvalue là tốt trong ví dụ này, bởi vì t không biểu thị một đối tượng tự động, nhưng thay vào đó một đối tượng được người gọi truyền vào.


893
2017-07-18 11:24



"Chỉ có 10 trang trên màn hình của tôi" - Mooing Duck
Có một lý do thứ ba cho việc di chuyển ngữ nghĩa là quan trọng: an toàn ngoại lệ. Thông thường, nơi một hoạt động sao chép có thể ném (vì nó cần phân bổ tài nguyên và phân bổ có thể thất bại) một hoạt động di chuyển có thể không ném (vì nó có thể chuyển quyền sở hữu tài nguyên hiện có thay vì phân bổ tài nguyên mới). Có các hoạt động không thể thất bại luôn luôn là tốt đẹp, và nó có thể rất quan trọng khi viết mã cung cấp bảo đảm ngoại lệ. - Brangdon
Tôi đã ở bên phải bạn với 'Tham chiếu toàn cầu', nhưng sau đó tất cả đều quá trừu tượng để theo dõi. Tham chiếu bị thu hẹp? Chuyển tiếp hoàn hảo? Bạn đang nói rằng một tham chiếu rvalue trở thành một tham chiếu phổ quát nếu loại là templated? Tôi ước có một cách để giải thích điều này để tôi biết nếu tôi cần hiểu hay không! :) - Kylotan
Xin vui lòng viết một cuốn sách ngay bây giờ ... câu trả lời này đã cho tôi lý do để tin rằng nếu bạn bao phủ các góc khác của C ++ một cách sáng suốt như thế này, hàng ngàn người sẽ hiểu nó. - halivingston
@halivingston Cảm ơn bạn rất nhiều vì phản hồi của bạn, tôi thực sự đánh giá cao nó. Vấn đề với việc viết một cuốn sách là: nó hoạt động nhiều hơn bạn có thể tưởng tượng. Nếu bạn muốn đào sâu vào C ++ 11 và hơn thế nữa, tôi khuyên bạn nên mua "C ++ hiện đại hiệu quả" của Scott Meyers. - fredoverflow


Di chuyển ngữ nghĩa dựa trên tham chiếu rvalue.
Một rvalue là một đối tượng tạm thời, mà sẽ bị phá hủy ở cuối biểu thức. Trong C ++ hiện tại, các giá trị chỉ liên kết với const tài liệu tham khảo. C ++ 1x sẽ cho phép khôngconst tham chiếu rvalue, đánh vần T&&, là tham chiếu đến một đối tượng rvalue.
Vì giá trị sẽ chết vào cuối biểu thức, bạn có thể ăn cắp dữ liệu của nó. Thay vì sao chép nó thành một đối tượng khác, bạn di chuyển dữ liệu của nó vào đó.

class X {
public: 
  X(X&& rhs) // ctor taking an rvalue reference, so-called move-ctor
    : data_()
  {
     // since 'x' is an rvalue object, we can steal its data
     this->swap(std::move(rhs));
     // this will leave rhs with the empty data
  }
  void swap(X&& rhs);
  // ... 
};

// ...

X f();

X x = f(); // f() returns result as rvalue, so this calls move-ctor

Trong đoạn mã trên, với các trình biên dịch cũ là kết quả của f() Là đã sao chép vào x sử dụng Xcủa nhà xây dựng bản sao. Nếu trình biên dịch của bạn hỗ trợ các ngữ nghĩa di chuyển và X có một hàm khởi tạo, sau đó nó được gọi thay thế. Kể từ khi nó rhs đối số là một rvalue, chúng ta biết nó không còn cần thiết nữa và chúng ta có thể ăn cắp giá trị của nó.
Vì vậy, giá trị là đã di chuyển từ tạm thời chưa được đặt tên từ f() đến x (trong khi dữ liệu của x, được khởi tạo để trống X, được chuyển vào tạm thời, sẽ bị hủy sau khi chuyển nhượng).


67
2018-06-23 23:12



lưu ý rằng nó phải là this->swap(std::move(rhs)); vì các tham chiếu rvalue được đặt tên là lvalues - wmamrak
Điều này là sai, theo nhận xét của @ Tacyt: rhs là một lvalue trong ngữ cảnh của X::X(X&& rhs). Bạn cần gọi std::move(rhs) để có được một giá trị, nhưng điều này làm cho câu trả lời bắt đầu. - Ashe


Giả sử bạn có một hàm trả về một đối tượng đáng kể:

Matrix multiply(const Matrix &a, const Matrix &b);

Khi bạn viết mã như thế này:

Matrix r = multiply(a, b);

thì trình biên dịch C ++ bình thường sẽ tạo ra một đối tượng tạm thời cho kết quả của multiply(), hãy gọi hàm tạo bản sao để khởi tạo rvà sau đó hủy giá trị trả về tạm thời. Di chuyển ngữ nghĩa trong C ++ 0x cho phép "di chuyển constructor" được gọi để khởi tạo rbằng cách sao chép nội dung của nó, và sau đó loại bỏ giá trị tạm thời mà không cần phải hủy bỏ nó.

Điều này đặc biệt quan trọng nếu (như có lẽ Matrix ví dụ ở trên), đối tượng được sao chép phân bổ bộ nhớ thừa trên heap để lưu trữ biểu diễn bên trong của nó. Một nhà xây dựng bản sao sẽ phải tạo một bản sao đầy đủ của biểu diễn bên trong, hoặc sử dụng các phép tính tham chiếu và các ngữ nghĩa sao chép-trên-ghi lẫn nhau. Một nhà xây dựng di chuyển sẽ để lại bộ nhớ heap một mình và chỉ cần sao chép con trỏ bên trong Matrix vật.


46
2018-06-23 22:53



Làm thế nào là các nhà xây dựng di chuyển và các nhà xây dựng sao chép khác nhau? - dicroce
@ dicroce: Chúng khác nhau theo cú pháp, giống như Matrix (const Matrix & src) (copy constructor) và cái khác giống Matrix (Matrix && src) (di chuyển constructor), kiểm tra câu trả lời chính của tôi cho một ví dụ tốt hơn. - snk_kid
@dicroce: Một tạo một đối tượng trống và một đối tượng tạo một bản sao. Nếu dữ liệu được lưu trữ trong đối tượng lớn, một bản sao có thể tốn kém. Ví dụ: std :: vector. - Billy ONeal
@ kunj2aan: Nó phụ thuộc vào trình biên dịch của bạn, tôi nghi ngờ. Trình biên dịch có thể tạo một đối tượng tạm thời bên trong hàm, và sau đó di chuyển nó vào giá trị trả về của người gọi. Hoặc, nó có thể trực tiếp xây dựng đối tượng trong giá trị trả về, mà không cần sử dụng một hàm tạo di chuyển. - Greg Hewgill
@Jichao: Đó là một tối ưu hóa được gọi là RVO, xem câu hỏi này để biết thêm thông tin về sự khác biệt: stackoverflow.com/questions/5031778/… - Greg Hewgill


Nếu bạn thực sự quan tâm đến một lời giải thích sâu sắc, tốt về ngữ nghĩa di chuyển, tôi khuyên bạn nên đọc bài báo gốc trên chúng, "Đề xuất thêm hỗ trợ ngữ nghĩa di chuyển cho ngôn ngữ C ++". 

Nó rất dễ tiếp cận và dễ đọc và nó làm cho một trường hợp tuyệt vời cho những lợi ích mà họ cung cấp. Có nhiều giấy tờ gần đây và cập nhật khác về ngữ nghĩa di chuyển có sẵn trên trang web WG21, nhưng điều này có lẽ là đơn giản nhất vì nó tiếp cận mọi thứ từ một cái nhìn cấp cao nhất và không nhận được rất nhiều vào các chi tiết ngôn ngữ gritty.


27
2018-06-23 23:32





Di chuyển ngữ nghĩa nói về chuyển tài nguyên thay vì sao chép chúng khi không ai cần giá trị nguồn nữa.

Trong C ++ 03, các đối tượng thường được sao chép, chỉ bị hủy hoặc được gán trước khi bất kỳ mã nào sử dụng lại giá trị. Ví dụ, khi bạn trả về bằng giá trị từ một hàm - trừ khi RVO khởi động — giá trị bạn đang quay trở lại được sao chép vào khung ngăn xếp của người gọi, và sau đó nó nằm ngoài phạm vi và bị hủy. Đây chỉ là một trong nhiều ví dụ: xem giá trị theo thời gian khi đối tượng nguồn là tạm thời, các thuật toán như sort chỉ sắp xếp lại các mục, phân bổ lại vector khi nó capacity() bị vượt quá, v.v.

Khi các cặp sao chép / phá hủy như vậy là tốn kém, nó thường là bởi vì đối tượng sở hữu một số tài nguyên nặng. Ví dụ, vector<string> có thể sở hữu một khối bộ nhớ được cấp phát động có chứa một mảng string các đối tượng, mỗi bộ nhớ có bộ nhớ động riêng. Sao chép một đối tượng như vậy là tốn kém: bạn phải cấp phát bộ nhớ mới cho từng khối được phân bổ động trong nguồn và sao chép tất cả các giá trị trên. Sau đó bạn cần deallocate tất cả những bộ nhớ mà bạn vừa sao chép. Tuy nhiên, di chuyển một lớn vector<string> có nghĩa là chỉ cần sao chép một vài con trỏ (tham chiếu đến khối bộ nhớ động) đến đích và đánh số chúng ra trong nguồn.


21
2018-04-08 19:47





Trong điều kiện dễ thực tế:

Sao chép một đối tượng có nghĩa là sao chép các thành viên "tĩnh" của nó và gọi new toán tử cho các đối tượng động của nó. Đúng?

class A
{
   int i, *p;

public:
   A(const A& a) : i(a.i), p(new int(*a.p)) {}
   ~A() { delete p; }
};

Tuy nhiên, để di chuyển một đối tượng (tôi lặp lại, theo quan điểm thực tế) ngụ ý chỉ sao chép các con trỏ của các đối tượng động và không tạo ra các đối tượng mới.

Nhưng, điều đó không nguy hiểm sao? Tất nhiên, bạn có thể hủy một đối tượng động hai lần (lỗi phân đoạn). Vì vậy, để tránh điều đó, bạn nên "vô hiệu hóa" các con trỏ nguồn để tránh hủy chúng hai lần:

class A
{
   int i, *p;

public:
   // Movement of an object inside a copy constructor.
   A(const A& a) : i(a.i), p(a.p)
   {
     a.p = nullptr; // pointer invalidated.
   }

   ~A() { delete p; }
   // Deleting NULL, 0 or nullptr (address 0x0) is safe. 
};

Ok, nhưng nếu tôi di chuyển một đối tượng, đối tượng nguồn sẽ trở thành vô ích, phải không? Tất nhiên, nhưng trong một số tình huống rất hữu ích. Điều hiển nhiên nhất là khi tôi gọi một hàm với một đối tượng ẩn danh (đối tượng thời gian, rvalue, ..., bạn có thể gọi nó bằng các tên khác nhau):

void heavyFunction(HeavyType());

Trong tình huống đó, một đối tượng ẩn danh được tạo ra, tiếp theo được sao chép vào tham số hàm, và sau đó bị xóa. Vì vậy, ở đây tốt hơn là di chuyển đối tượng, bởi vì bạn không cần đối tượng ẩn danh và bạn có thể tiết kiệm thời gian và bộ nhớ.

Điều này dẫn đến khái niệm về một tham chiếu "rvalue". Chúng tồn tại trong C ++ 11 chỉ để phát hiện nếu đối tượng nhận được là vô danh hay không. Tôi nghĩ bạn đã biết rằng một "lvalue" là một thực thể được gán (phần bên trái của = toán tử), vì vậy bạn cần một tham chiếu có tên cho một đối tượng để có thể hoạt động như một giá trị. Một rvalue chính xác là đối diện, một đối tượng không có tham chiếu được đặt tên. Do đó, đối tượng ẩn danh và rvalue là từ đồng nghĩa. Vì thế:

class A
{
   int i, *p;

public:
   // Copy
   A(const A& a) : i(a.i), p(new int(*a.p)) {}

   // Movement (&& means "rvalue reference to")
   A(A&& a) : i(a.i), p(a.p)
   {
      a.p = nullptr;
   }

   ~A() { delete p; }
};

Trong trường hợp này, khi một đối tượng thuộc loại A nên được "sao chép", trình biên dịch tạo ra một tham chiếu lvalue hoặc một tham chiếu rvalue theo nếu đối tượng được thông qua được đặt tên hay không. Khi không, hàm khởi tạo của bạn được gọi và bạn biết đối tượng là thời gian và bạn có thể di chuyển các đối tượng động của nó thay vì sao chép chúng, tiết kiệm không gian và bộ nhớ.

Điều quan trọng cần nhớ là các đối tượng "tĩnh" luôn được sao chép. Không có cách nào để "di chuyển" một đối tượng tĩnh (đối tượng trong ngăn xếp và không phải trên đống). Vì vậy, sự khác biệt "di chuyển" / "sao chép" khi một đối tượng không có thành viên năng động (trực tiếp hoặc gián tiếp) là không liên quan.

Nếu đối tượng của bạn phức tạp và hàm hủy có các hiệu ứng phụ khác, như gọi hàm của thư viện, hãy gọi đến các hàm toàn cầu khác hoặc bất kỳ hàm nào, có lẽ tốt hơn là báo hiệu chuyển động bằng cờ:

class Heavy
{
   bool b_moved;
   // staff

public:
   A(const A& a) { /* definition */ }
   A(A&& a) : // initialization list
   {
      a.b_moved = true;
   }

   ~A() { if (!b_moved) /* destruct object */ }
};

Vì vậy, mã của bạn ngắn hơn (bạn không cần phải làm nullptr phân công cho từng thành viên năng động) và tổng quát hơn.

Câu hỏi điển hình khác: sự khác biệt giữa A&& và const A&&? Tất nhiên, trong trường hợp đầu tiên, bạn có thể sửa đổi đối tượng và trong lần thứ hai không, nhưng, ý nghĩa thực tế? Trong trường hợp thứ hai, bạn không thể sửa đổi nó, vì vậy bạn không có cách nào để làm mất hiệu lực đối tượng (ngoại trừ với cờ có thể thay đổi hoặc thứ gì đó tương tự), và không có sự khác biệt thực tế đối với một hàm tạo bản sao.

Và cái gì chuyển tiếp hoàn hảo? Điều quan trọng là phải biết rằng "tham chiếu rvalue" là một tham chiếu đến một đối tượng được đặt tên trong "phạm vi của người gọi". Nhưng trong phạm vi thực tế, một tham chiếu rvalue là một tên cho một đối tượng, do đó, nó hoạt động như một đối tượng được đặt tên. Nếu bạn chuyển một tham chiếu rvalue cho một hàm khác, bạn đang truyền một đối tượng có tên, vì vậy, đối tượng không được nhận như một đối tượng thời gian.

void some_function(A&& a)
{
   other_function(a);
}

Đối tượng a sẽ được sao chép vào thông số thực tế của other_function. Nếu bạn muốn đối tượng a tiếp tục được đối xử như một đối tượng tạm thời, bạn nên sử dụng std::move chức năng:

other_function(std::move(a));

Với dòng này, std::move sẽ truyền a để một rvalue và other_function sẽ nhận đối tượng dưới dạng đối tượng chưa được đặt tên. Tất nhiên nếu other_functionkhông quá tải cụ thể để làm việc với các đối tượng chưa được đặt tên, sự khác biệt này không quan trọng.

Đó có phải là sự chuyển tiếp hoàn hảo không? Không, nhưng chúng tôi rất thân thiết. Việc chuyển tiếp hoàn hảo chỉ hữu ích khi làm việc với các khuôn mẫu, với mục đích để nói: nếu tôi cần truyền một đối tượng sang một hàm khác, tôi cần rằng nếu tôi nhận được một đối tượng được đặt tên, đối tượng sẽ được chuyển như một đối tượng có tên, và khi không, Tôi muốn truyền nó như một đối tượng chưa được đặt tên:

template<typename T>
void some_function(T&& a)
{
   other_function(std::forward<T>(a));
}

Đó là chữ ký của một hàm nguyên mẫu sử dụng chuyển tiếp hoàn hảo, được triển khai trong C ++ 11 bằng phương tiện std::forward. Hàm này khai thác một số quy tắc về việc khởi tạo mẫu:

 `A& && == A&`
 `A&& && == A&&`

Do đó, nếu T là một tham chiếu lvalue A (T = A &), a cũng thế (A & && => A &). Nếu T là một tham chiếu rvalue A, a cũng (A && && => A &&). Trong cả hai trường hợp, a là một đối tượng được đặt tên trong phạm vi thực tế, nhưng T chứa thông tin về "loại tham chiếu" của nó từ quan điểm của phạm vi người gọi. Thông tin này (T) được chuyển thành tham số mẫu forward và 'a' được di chuyển hay không theo loại T.


19
2017-08-18 15:57





Nó giống như sao chép ngữ nghĩa, nhưng thay vì phải sao chép tất cả các dữ liệu bạn nhận được để ăn cắp dữ liệu từ đối tượng được "di chuyển" từ.


17
2018-06-23 22:56





Bạn biết ngữ nghĩa sao chép có nghĩa là đúng không? nó có nghĩa là bạn có các loại có thể sao chép được, đối với các loại do người dùng định nghĩa, bạn xác định điều này hoặc mua một cách rõ ràng bằng cách viết một hàm tạo bản sao và toán tử gán hoặc trình biên dịch tạo ra chúng ngầm. Điều này sẽ làm một bản sao.

Di chuyển ngữ nghĩa về cơ bản là kiểu người dùng định nghĩa với hàm tạo tham chiếu giá trị r (kiểu tham chiếu mới sử dụng && (có hai ký hiệu), không phải là const, được gọi là hàm khởi tạo, giống như toán tử gán. Vì vậy, những gì hiện một nhà xây dựng di chuyển làm, thay vì sao chép bộ nhớ từ đối số nguồn của nó nó 'di chuyển' bộ nhớ từ nguồn đến đích.

Khi nào bạn muốn làm điều đó? cũng std :: vector là một ví dụ, nói rằng bạn tạo ra một std :: vector tạm thời và bạn trả lại nó từ một hàm nói:

std::vector<foo> get_foos();

Bạn sẽ có chi phí từ hàm tạo bản sao khi hàm trả về, nếu (và nó sẽ trong C ++ 0x) std :: vector có một hàm tạo di chuyển thay vì sao chép nó chỉ có thể đặt con trỏ của nó và 'di chuyển' được phân bổ động bộ nhớ cho cá thể mới. Nó giống như ngữ nghĩa chuyển giao quyền sở hữu với std :: auto_ptr.


13
2018-06-23 22:58



Tôi không nghĩ rằng đây là một ví dụ tuyệt vời, bởi vì trong các ví dụ về giá trị trả về hàm này, giá trị trả về tối ưu hóa có lẽ đã loại bỏ hoạt động sao chép. - Zan Lynx