Di chuyển toán tử gán và `if (this! = & Rhs) '


126

Trong toán tử gán của một lớp, bạn thường cần kiểm tra xem đối tượng được gán có phải là đối tượng đang gọi hay không để bạn không làm hỏng mọi thứ:

Class& Class::operator=(const Class& rhs) {
    if (this != &rhs) {
        // do the assignment
    }

    return *this;
}

Bạn có cần điều tương tự cho toán tử gán di chuyển không? Có bao giờ một tình huống this == &rhssẽ đúng không?

? Class::operator=(Class&& rhs) {
    ?
}

12
Không liên quan đến Q được hỏi và chỉ để người dùng mới đọc Q này theo dòng thời gian (tôi biết Seth đã biết điều này) không hiểu sai ý tưởng, Sao chép và Hoán đổi là cách chính xác để triển khai Toán tử gán sao chép trong đó Bạn không cần phải kiểm tra việc tự gán et-all.
Alok Save

5
@VaughnCato: A a; a = std::move(a);.
Xeo

11
@VaughnCato Việc sử dụng std::movelà bình thường. Sau đó, hãy tính đến bí danh, và khi bạn ở sâu bên trong ngăn xếp cuộc gọi và bạn có một tham chiếu đến Tvà một tham chiếu khác tới T... bạn có định kiểm tra danh tính ngay tại đây không? Bạn có muốn tìm cuộc gọi đầu tiên (hoặc các cuộc gọi) trong đó tài liệu rằng bạn không thể vượt qua cùng một đối số hai lần sẽ chứng minh một cách tĩnh rằng hai tham chiếu đó không phải là bí danh không? Hay bạn sẽ tự chỉ định công việc?
Luc Danton

2
@LucDanton Tôi muốn xác nhận trong toán tử gán. Nếu std :: move được sử dụng theo cách có thể kết thúc bằng việc tự gán giá trị, tôi sẽ coi đó là một lỗi cần được sửa.
Vaughn Cato

4
@VaughnCato Một nơi mà việc tự hoán đổi là bình thường là bên trong std::sorthoặc std::shuffle- bất kỳ lúc nào bạn hoán đổi phần tử ithứ và jthứ của một mảng mà không cần kiểm tra i != jtrước. ( std::swapđược thực hiện trong điều kiện phân công di chuyển.)
Quuxplusone

Câu trả lời:


143

Chà, có quá nhiều thứ để dọn dẹp ở đây ...

Đầu tiên, Sao chép và Hoán đổi không phải lúc nào cũng là cách chính xác để thực hiện Chỉ định Sao chép. Gần như chắc chắn trong trường hợp dumb_array, đây là một giải pháp dưới mức tối ưu.

Việc sử dụng Copy and Swapdumb_arraymột ví dụ điển hình về việc đặt hoạt động đắt tiền nhất với đầy đủ các tính năng nhất ở lớp dưới cùng. Nó hoàn hảo cho những khách hàng muốn có đầy đủ tính năng nhất và sẵn sàng trả tiền phạt cho hiệu suất. Họ nhận được chính xác những gì họ muốn.

Nhưng thật tai hại cho những khách hàng không cần tính năng đầy đủ nhất và thay vào đó họ đang tìm kiếm hiệu suất cao nhất. Đối với họ dumb_arraychỉ là một phần mềm khác mà họ phải viết lại vì nó quá chậm. Đã dumb_arrayđược thiết kế khác biệt, nó có thể làm hài lòng cả hai khách hàng mà không có bất kỳ sự thỏa hiệp nào đối với một trong hai khách hàng.

Chìa khóa để làm hài lòng cả hai khách hàng là xây dựng các hoạt động nhanh nhất ở cấp thấp nhất và sau đó thêm API lên trên để có các tính năng đầy đủ hơn với chi phí cao hơn. Tức là bạn cần đảm bảo ngoại lệ mạnh mẽ, tốt, bạn trả tiền cho nó. Bạn không cần nó? Đây là một giải pháp nhanh hơn.

Hãy tìm hiểu cụ thể: Đây là toán tử Gán sao chép cơ bản, nhanh chóng, đảm bảo ngoại lệ cho dumb_array:

dumb_array& operator=(const dumb_array& other)
{
    if (this != &other)
    {
        if (mSize != other.mSize)
        {
            delete [] mArray;
            mArray = nullptr;
            mArray = other.mSize ? new int[other.mSize] : nullptr;
            mSize = other.mSize;
        }
        std::copy(other.mArray, other.mArray + mSize, mArray);
    }
    return *this;
}

Giải trình:

Một trong những điều đắt tiền hơn bạn có thể làm trên phần cứng hiện đại là thực hiện một chuyến đi đến đống. Bất cứ điều gì bạn có thể làm để tránh bị mất thời gian và công sức. Khách hàng của dumb_arraycó thể muốn thường xuyên gán các mảng có cùng kích thước. Và khi họ làm vậy, tất cả những gì bạn cần làm là memcpy(ẩn bên dưới std::copy). Bạn không muốn phân bổ một mảng mới có cùng kích thước và sau đó phân bổ mảng cũ có cùng kích thước!

Bây giờ dành cho khách hàng của bạn, những người thực sự muốn an toàn ngoại lệ mạnh mẽ:

template <class C>
C&
strong_assign(C& lhs, C rhs)
{
    swap(lhs, rhs);
    return lhs;
}

Hoặc có thể nếu bạn muốn tận dụng lợi thế của việc chuyển nhượng trong C ++ 11 thì nên:

template <class C>
C&
strong_assign(C& lhs, C rhs)
{
    lhs = std::move(rhs);
    return lhs;
}

Nếu dumb_arraykhách hàng coi trọng tốc độ, họ nên gọi operator=. Nếu họ cần an toàn ngoại lệ mạnh mẽ, có những thuật toán chung mà họ có thể gọi sẽ hoạt động trên nhiều đối tượng và chỉ cần thực hiện một lần.

Bây giờ trở lại câu hỏi ban đầu (có kiểu chữ o tại thời điểm này):

Class&
Class::operator=(Class&& rhs)
{
    if (this == &rhs)  // is this check needed?
    {
       // ...
    }
    return *this;
}

Đây thực sự là một câu hỏi gây tranh cãi. Một số sẽ nói có, hoàn toàn, một số sẽ nói không.

Ý kiến ​​cá nhân của tôi là không, bạn không cần kiểm tra này.

Cơ sở lý luận:

Khi một đối tượng liên kết với một tham chiếu rvalue thì đó là một trong hai điều:

  1. Tạm thời.
  2. Đối tượng mà người gọi muốn bạn tin tưởng chỉ là tạm thời.

Nếu bạn có một tham chiếu đến một đối tượng thực sự là tạm thời, thì theo định nghĩa, bạn có một tham chiếu duy nhất đến đối tượng đó. Nó không thể được tham chiếu bởi bất kỳ nơi nào khác trong toàn bộ chương trình của bạn. Tức this == &temporary là không thể .

Bây giờ, nếu khách hàng của bạn đã nói dối bạn và hứa với bạn rằng bạn sẽ nhận được tạm thời khi không có, thì khách hàng có trách nhiệm đảm bảo rằng bạn không phải quan tâm. Nếu bạn muốn thực sự cẩn thận, tôi tin rằng đây sẽ là cách triển khai tốt hơn:

Class&
Class::operator=(Class&& other)
{
    assert(this != &other);
    // ...
    return *this;
}

Tức là Nếu bạn được thông qua tự tham chiếu, đây là một lỗi ở phần máy khách cần được sửa.

Để hoàn thiện, đây là một toán tử gán di chuyển cho dumb_array:

dumb_array& operator=(dumb_array&& other)
{
    assert(this != &other);
    delete [] mArray;
    mSize = other.mSize;
    mArray = other.mArray;
    other.mSize = 0;
    other.mArray = nullptr;
    return *this;
}

Trong trường hợp sử dụng điển hình của phép chuyển nhượng, *thissẽ là một đối tượng được chuyển đến và do đó, delete [] mArray;nên là một không chọn. Điều quan trọng là việc triển khai thực hiện xóa trên nullptr càng nhanh càng tốt.

Cảnh báo:

Một số người sẽ cho rằng đó swap(x, x)là một ý tưởng tốt, hay chỉ là một điều ác cần thiết. Và điều này, nếu hoán đổi chuyển sang hoán đổi mặc định, có thể gây ra sự tự chuyển nhượng.

Tôi không đồng ý rằng swap(x, x)bao giờ là một ý tưởng tốt. Nếu được tìm thấy trong mã của riêng tôi, tôi sẽ coi đó là lỗi hiệu suất và sửa nó. Nhưng trong trường hợp bạn muốn cho phép nó, hãy nhận ra rằng swap(x, x)chỉ có self-move-assignemnet trên một giá trị chuyển từ. Và trong dumb_arrayví dụ của chúng tôi, điều này sẽ hoàn toàn vô hại nếu chúng tôi đơn giản bỏ qua khẳng định hoặc hạn chế nó trong trường hợp chuyển từ:

dumb_array& operator=(dumb_array&& other)
{
    assert(this != &other || mSize == 0);
    delete [] mArray;
    mSize = other.mSize;
    mArray = other.mArray;
    other.mSize = 0;
    other.mArray = nullptr;
    return *this;
}

Nếu bạn tự gán hai mục chuyển đến từ (trống) dumb_array, bạn không làm gì sai ngoài việc chèn các hướng dẫn vô ích vào chương trình của mình. Quan sát tương tự này có thể được thực hiện cho đại đa số các đối tượng.

<Cập nhật>

Tôi đã suy nghĩ thêm về vấn đề này và thay đổi quan điểm của mình phần nào. Bây giờ tôi tin rằng việc chuyển nhượng nên được chấp nhận cho việc tự chuyển nhượng, nhưng điều kiện đăng bài về chuyển nhượng bản sao và chuyển nhượng chuyển nhượng là khác nhau:

Đối với nhiệm vụ sao chép:

x = y;

người ta phải có một điều kiện hậu mà giá trị của ykhông được thay đổi. Khi &x == &yđó hậu điều kiện này chuyển thành: việc tự sao chép sẽ không ảnh hưởng đến giá trị của x.

Đối với nhiệm vụ di chuyển:

x = std::move(y);

ta nên có một hậu điều kiện ycó trạng thái hợp lệ nhưng không xác định. Khi &x == &yđó hậu điều kiện này chuyển thành: xcó trạng thái hợp lệ nhưng không xác định. Tức là việc chỉ định tự di chuyển không phải là một việc không cần làm. Nhưng nó không nên sụp đổ. Điều kiện hậu này phù hợp với việc cho phép swap(x, x)chỉ hoạt động:

template <class T>
void
swap(T& x, T& y)
{
    // assume &x == &y
    T tmp(std::move(x));
    // x and y now have a valid but unspecified state
    x = std::move(y);
    // x and y still have a valid but unspecified state
    y = std::move(tmp);
    // x and y have the value of tmp, which is the value they had on entry
}

Những điều trên hoạt động, miễn là x = std::move(x)không sụp đổ. Nó có thể rời khỏi xbất kỳ trạng thái hợp lệ nhưng không xác định.

Tôi thấy ba cách để lập trình toán tử gán di chuyển dumb_arrayđể đạt được điều này:

dumb_array& operator=(dumb_array&& other)
{
    delete [] mArray;
    // set *this to a valid state before continuing
    mSize = 0;
    mArray = nullptr;
    // *this is now in a valid state, continue with move assignment
    mSize = other.mSize;
    mArray = other.mArray;
    other.mSize = 0;
    other.mArray = nullptr;
    return *this;
}

Việc triển khai ở trên cho phép tự gán, nhưng *thisotherkết thúc là một mảng có kích thước bằng 0 sau khi tự chuyển, bất kể giá trị ban đầu của *thislà bao nhiêu. Điều này là tốt.

dumb_array& operator=(dumb_array&& other)
{
    if (this != &other)
    {
        delete [] mArray;
        mSize = other.mSize;
        mArray = other.mArray;
        other.mSize = 0;
        other.mArray = nullptr;
    }
    return *this;
}

Việc triển khai ở trên chấp nhận việc tự gán giống như cách mà toán tử gán bản sao thực hiện, bằng cách biến nó thành no-op. Điều này cũng ổn.

dumb_array& operator=(dumb_array&& other)
{
    swap(other);
    return *this;
}

Ở trên là ok chỉ khi dumb_arraykhông giữ tài nguyên cần được hủy "ngay lập tức". Ví dụ, nếu tài nguyên duy nhất là bộ nhớ, điều trên là tốt. Nếu dumb_arraycó thể giữ khóa mutex hoặc trạng thái đang mở của tệp, khách hàng có thể mong đợi một cách hợp lý những tài nguyên đó trên lhs của nhiệm vụ di chuyển ngay lập tức và do đó việc triển khai này có thể có vấn đề.

Chi phí đầu tiên là hai cửa hàng phụ. Chi phí của thứ hai là thử nghiệm và chi nhánh. Cả hai đều hoạt động. Cả hai đều đáp ứng tất cả các yêu cầu của Bảng 22 Yêu cầu MoveAssignable trong tiêu chuẩn C ++ 11. Thứ ba cũng hoạt động theo mô-đun không quan tâm đến tài nguyên bộ nhớ.

Tất cả ba cách triển khai có thể có chi phí khác nhau tùy thuộc vào phần cứng: Chi nhánh đắt như thế nào? Có rất nhiều đăng ký hay rất ít?

Việc mang đi là việc tự chuyển nhượng, không giống như tự sao chép, không cần phải bảo toàn giá trị hiện tại.

</ Cập nhật>

Một bản chỉnh sửa cuối cùng (hy vọng) lấy cảm hứng từ nhận xét của Luc Danton:

Nếu bạn đang viết một lớp cấp cao không trực tiếp quản lý bộ nhớ (nhưng có thể có các cơ sở hoặc thành viên có chức năng này), thì cách triển khai tốt nhất của việc chuyển nhượng thường là:

Class& operator=(Class&&) = default;

Thao tác này sẽ chuyển giao lần lượt từng cơ sở và từng thành viên, và sẽ không bao gồm this != &otherséc. Điều này sẽ mang lại cho bạn hiệu suất cao nhất và an toàn ngoại lệ cơ bản giả sử không có bất biến nào cần được duy trì giữa các căn cứ và thành viên của bạn. Đối với khách hàng của bạn yêu cầu an toàn ngoại lệ mạnh mẽ, hãy hướng họ tới strong_assign.


6
Tôi không biết phải cảm thấy thế nào về câu trả lời này. Nó làm cho nó trông giống như việc triển khai các lớp như vậy (quản lý bộ nhớ của chúng rất rõ ràng) là một việc thường làm. Đúng là khi bạn làm ghi như một lớp người ta phải rất cực kỳ cẩn thận về đảm bảo an toàn ngoại lệ và việc tìm kiếm các điểm nhạy cảm cho giao diện để được súc tích nhưng thuận tiện, nhưng câu hỏi dường như được yêu cầu cho lời khuyên chung.
Luc Danton

Vâng, tôi chắc chắn không bao giờ sử dụng copy-and-swap bởi vì nó rất lãng phí thời gian cho các lớp quản lý tài nguyên và mọi thứ (tại sao lại đi và tạo một bản sao toàn bộ khác của tất cả dữ liệu của bạn?). Và cảm ơn, điều này trả lời câu hỏi của tôi.
Seth Carnegie

5
Downvoted cho gợi ý rằng động thái-phân-từ-tự nên bao giờ khẳng định-thất bại hoặc tạo ra một "không xác định" kết quả. Chỉ định từ bản thân thực sự là trường hợp dễ dàng nhất để làm đúng. Nếu lớp của bạn gặp sự cố std::swap(x,x), thì tại sao tôi nên tin tưởng nó để xử lý các hoạt động phức tạp hơn một cách chính xác?
Quuxplusone

1
@Quuxplusone: Tôi đã đồng ý với bạn về khẳng định không thành công, như đã được lưu ý trong bản cập nhật cho câu trả lời của tôi. Về phía trước std::swap(x,x), nó chỉ hoạt động ngay cả khi x = std::move(x)tạo ra một kết quả không xác định. Thử nó! Bạn không cần phải tin tôi.
Howard Hinnant

@HowardHinnant điểm tốt, swaphoạt động miễn là x = move(x)rời khỏi xbất kỳ trạng thái nào có thể di chuyển. Và các thuật toán std::copy/ std::moveđược định nghĩa để tạo ra hành vi không xác định trên các bản sao no-op đã có (ouch; thanh niên 20 tuổi memmovehiểu đúng trường hợp nhỏ nhưng std::movekhông!). Vì vậy, tôi đoán tôi vẫn chưa nghĩ ra một "slam dunk" để tự phân công. Nhưng rõ ràng việc tự gán là điều xảy ra rất nhiều trong code thực, cho dù Standard có ban phước cho nó hay không.
Quuxplusone

11

Đầu tiên, bạn có chữ ký của toán tử chuyển nhượng sai. Vì các động thái đánh cắp tài nguyên từ đối tượng nguồn, nguồn phải là một consttham chiếu không có giá trị r.

Class &Class::operator=( Class &&rhs ) {
    //...
    return *this;
}

Lưu ý rằng bạn vẫn trả về thông qua tham chiếu giá trị l (không phải const) .

Đối với cả hai loại phân công trực tiếp, tiêu chuẩn không phải để kiểm tra việc tự phân công, nhưng để đảm bảo rằng việc tự phân công không gây ra sự cố và ghi. Nói chung, không ai thực hiện x = xhoặc y = std::move(y)gọi một cách rõ ràng , nhưng bí danh, đặc biệt là thông qua nhiều chức năng, có thể dẫn đến a = bhoặc c = std::move(d)trở thành sự tự gán. Kiểm tra rõ ràng cho việc tự gán, tức là this == &rhsbỏ qua phần thịt của hàm khi đúng là một cách để đảm bảo an toàn cho việc tự gán. Nhưng đó là một trong những cách tồi tệ nhất, vì nó tối ưu hóa một trường hợp hiếm (hy vọng) trong khi đó là một cách chống tối ưu hóa cho trường hợp phổ biến hơn (do phân nhánh và có thể bỏ sót bộ nhớ cache).

Bây giờ khi (ít nhất) một trong các toán hạng là một đối tượng tạm thời trực tiếp, bạn không bao giờ có thể có một kịch bản tự gán. Một số người ủng hộ việc giả định trường hợp đó và tối ưu hóa mã cho nó đến mức mã trở nên ngu ngốc một cách tự tử khi giả định là sai. Tôi nói rằng việc đổ kiểm tra cùng một đối tượng cho người dùng là vô trách nhiệm. Chúng tôi không đưa ra đối số đó cho việc sao chép-gán; tại sao lại đảo ngược vị trí để chuyển-nhượng?

Hãy làm một ví dụ, được thay đổi từ một người trả lời khác:

dumb_array& dumb_array::operator=(const dumb_array& other)
{
    if (mSize != other.mSize)
    {
        delete [] mArray;
        mArray = nullptr;  // clear this...
        mSize = 0u;        // ...and this in case the next line throws
        mArray = other.mSize ? new int[other.mSize] : nullptr;
        mSize = other.mSize;
    }
    std::copy(other.mArray, other.mArray + mSize, mArray);
    return *this;
}

Bản sao-gán này xử lý tự chuyển nhượng một cách duyên dáng mà không cần kiểm tra rõ ràng. Nếu kích thước nguồn và đích khác nhau, thì việc phân bổ và phân bổ lại sẽ diễn ra trước quá trình sao chép. Nếu không, chỉ cần sao chép là xong. Tự gán không có được một đường dẫn được tối ưu hóa, nó bị dồn vào cùng một đường dẫn như khi kích thước nguồn và đích bắt đầu bằng nhau. Về mặt kỹ thuật, việc sao chép là không cần thiết khi hai đối tượng tương đương nhau (kể cả khi chúng là cùng một đối tượng), nhưng đó là cái giá phải trả khi không thực hiện kiểm tra bình đẳng (theo giá trị hoặc theo địa chỉ) vì đã nói bản thân kiểm tra sẽ là lãng phí nhất của thời gian. Lưu ý rằng đối tượng tự gán ở đây sẽ gây ra một loạt các tự gán cấp phần tử; loại phần tử phải an toàn để thực hiện việc này.

Giống như ví dụ nguồn của nó, việc gán bản sao này cung cấp bảo đảm an toàn ngoại lệ cơ bản. Nếu bạn muốn đảm bảo mạnh mẽ, hãy sử dụng toán tử gán hợp nhất từ Bản sao và Hoán đổi ban đầu truy vấn truy vấn này xử lý cả sao chép và chuyển nhượng. Nhưng điểm của ví dụ này là giảm độ an toàn xuống một bậc để đạt được tốc độ. (BTW, chúng tôi giả định rằng các giá trị của các phần tử riêng lẻ là độc lập; rằng không có ràng buộc bất biến giới hạn một số giá trị so với các giá trị khác.)

Hãy xem xét một chuyển nhượng cho cùng loại này:

class dumb_array
{
    //...
    void swap(dumb_array& other) noexcept
    {
        // Just in case we add UDT members later
        using std::swap;

        // both members are built-in types -> never throw
        swap( this->mArray, other.mArray );
        swap( this->mSize, other.mSize );
    }

    dumb_array& operator=(dumb_array&& other) noexcept
    {
        this->swap( other );
        return *this;
    }
    //...
};

void  swap( dumb_array &l, dumb_array &r ) noexcept  { l.swap( r ); }

Một kiểu có thể hoán đổi cần tùy chỉnh phải có một hàm miễn phí hai đối số được gọi swaptrong cùng một không gian tên với kiểu. (Hạn chế không gian tên cho phép các lệnh gọi hoán đổi không đủ điều kiện hoạt động.) Một loại vùng chứa cũng nên thêm một swaphàm thành viên công khai để khớp với các vùng chứa tiêu chuẩn. Nếu một thành viên swapkhông được cung cấp, thì chức năng miễn phí swapcó thể cần được đánh dấu là bạn của loại có thể hoán đổi. Nếu bạn tùy chỉnh các bước di chuyển để sử dụng swap, thì bạn phải cung cấp mã hoán đổi của riêng mình; mã tiêu chuẩn gọi mã di chuyển của loại, điều này sẽ dẫn đến đệ quy lẫn nhau vô hạn cho các loại di chuyển tùy chỉnh.

Giống như hàm hủy, các hàm hoán đổi và các hoạt động di chuyển không bao giờ được ném nếu có thể và có thể được đánh dấu như vậy (trong C ++ 11). Các loại thư viện tiêu chuẩn và quy trình có các tối ưu hóa cho các loại di chuyển không thể ném.

Phiên bản chuyển nhượng đầu tiên này hoàn thành hợp đồng cơ bản. Các điểm đánh dấu tài nguyên của nguồn được chuyển đến đối tượng đích. Các tài nguyên cũ sẽ không bị rò rỉ vì đối tượng nguồn hiện quản lý chúng. Và đối tượng nguồn được để ở trạng thái có thể sử dụng được, nơi các hoạt động tiếp theo, bao gồm cả gán và hủy, có thể được áp dụng cho nó.

Lưu ý rằng việc chuyển nhượng này tự động an toàn cho việc tự chuyển nhượng, vì lệnh swapgọi là như vậy. Nó cũng rất an toàn ngoại lệ. Vấn đề là lưu giữ tài nguyên không cần thiết. Các tài nguyên cũ cho đích về mặt khái niệm không còn cần thiết nữa, nhưng ở đây chúng vẫn chỉ tồn tại xung quanh để đối tượng nguồn có thể giữ nguyên giá trị. Nếu việc hủy đối tượng nguồn theo lịch trình còn lâu mới diễn ra, chúng ta đang lãng phí không gian tài nguyên hoặc tệ hơn nếu tổng không gian tài nguyên bị giới hạn và các kiến ​​nghị tài nguyên khác sẽ xảy ra trước khi đối tượng nguồn (mới) chính thức chết.

Vấn đề này là nguyên nhân gây ra các lời khuyên hiện tại gây tranh cãi về việc tự nhắm mục tiêu trong quá trình chuyển công tác. Cách để viết chuyển nhượng mà không kéo dài tài nguyên là một cái gì đó như:

class dumb_array
{
    //...
    dumb_array& operator=(dumb_array&& other) noexcept
    {
        delete [] this->mArray;  // kill old resources
        this->mArray = other.mArray;
        this->mSize = other.mSize;
        other.mArray = nullptr;  // reset source
        other.mSize = 0u;
        return *this;
    }
    //...
};

Nguồn được đặt lại về điều kiện mặc định, trong khi tài nguyên đích cũ bị phá hủy. Trong trường hợp tự giao, đối tượng hiện tại của bạn sẽ tự sát. Cách chính xung quanh nó là bao quanh mã hành động bằng một if(this != &other)khối hoặc vặn nó và cho phép khách hàng ăn một assert(this != &other)dòng ban đầu (nếu bạn cảm thấy tốt).

Một giải pháp thay thế là nghiên cứu cách thực hiện việc sao chép chuyển nhượng ngoại lệ an toàn, không có sự phân công thống nhất và áp dụng nó để chuyển nhượng:

class dumb_array
{
    //...
    dumb_array& operator=(dumb_array&& other) noexcept
    {
        dumb_array  temp{ std::move(other) };

        this->swap( temp );
        return *this;
    }
    //...
};

Khi otherthiskhác biệt, otherđược làm trống bởi việc di chuyển đến tempvà giữ nguyên như vậy. Sau đó, thismất tài nguyên cũ của nó temptrong khi nhận được tài nguyên ban đầu other. Sau đó, các tài nguyên cũ của thisbị giết khi tempnào.

Khi sự tự phân công xảy ra, việc otherlàm temptrống thiscũng như trống . Sau đó đối tượng đích lấy lại tài nguyên của nó khi nào tempthishoán đổi. Cái chết của các tempyêu cầu bồi thường một đối tượng trống rỗng, mà trên thực tế phải là một cấm. Các this/ otherđối tượng giữ nguồn tài nguyên của nó.

Việc chuyển nhượng không bao giờ được thực hiện miễn là việc di chuyển-xây dựng và hoán đổi cũng được thực hiện. Cái giá phải trả của việc an toàn trong quá trình tự chuyển nhượng là một vài hướng dẫn hơn đối với các loại cấp thấp, sẽ được đưa ra bởi lệnh gọi phân bổ.


Bạn có cần kiểm tra xem có bất kỳ bộ nhớ nào đã được cấp phát trước khi gọi deletetrong khối mã thứ hai của bạn không?
user3728501

3
Mẫu mã thứ hai của bạn, toán tử gán sao chép mà không kiểm tra tự gán, là sai. std::copygây ra hành vi không xác định nếu phạm vi nguồn và đích trùng nhau (kể cả trường hợp chúng trùng nhau). Xem C ++ 14 [alg.copy] / 3.
MM

6

Tôi ở trong nhóm những người muốn các toán tử an toàn tự chỉ định, nhưng không muốn viết các kiểm tra tự chỉ định trong việc triển khai operator= . Và trên thực tế, tôi thậm chí không muốn triển khai operator=chút nào, tôi muốn hành vi mặc định hoạt động 'ngay lập tức'. Các thành viên đặc biệt tốt nhất là những người đến miễn phí.

Điều đó đang được nói, các yêu cầu MoveAssignable hiện có trong Tiêu chuẩn được mô tả như sau (từ 17.6.3.1 Yêu cầu đối số mẫu [tiện ích.arg.requirements], n3290):

Biểu thức Loại trả lại Giá trị trả lại Hậu điều kiện
t = rv T & tt tương đương với giá trị của rv trước khi gán

trong đó trình giữ chỗ được mô tả là: " t[là] giá trị có thể sửa đổi thuộc loại T;" và " rvlà một giá trị kiểu T;". Lưu ý rằng đó là những yêu cầu được đặt trên các kiểu được sử dụng làm đối số cho các mẫu của thư viện Chuẩn, nhưng nhìn vào phần khác trong Chuẩn, tôi nhận thấy rằng mọi yêu cầu khi chuyển nhượng đều tương tự như yêu cầu này.

Điều này có nghĩa là nó a = std::move(a)phải 'an toàn'. Nếu những gì bạn cần là một bài kiểm tra nhận dạng (ví dụ this != &other), thì hãy làm đi, nếu không, bạn thậm chí sẽ không thể đưa các đối tượng của mình vào std::vector! (Trừ khi bạn không sử dụng các thành viên / hoạt động yêu cầu MoveAssignable; nhưng đừng bận tâm đến điều đó.) Lưu ý rằng với ví dụ trước a = std::move(a), sau đó this == &otherthực sự sẽ giữ.


Bạn có thể giải thích cách a = std::move(a)không hoạt động sẽ khiến một lớp không hoạt động được std::vectorkhông? Thí dụ?
Paul J. Lucas

@ PaulJ.Lucas Gọi std::vector<T>::erasekhông được phép trừ khi TMoveAssignable. (Như một IIRC sang một bên, một số yêu cầu MoveAssignable đã được nới lỏng thành MoveInsertable thay vào đó trong C ++ 14.)
Luc Danton

OK, vì vậy Tphải là MoveAssignable, nhưng tại sao lại erase()phụ thuộc vào việc di chuyển một phần tử đến chính nó ?
Paul J. Lucas

@ PaulJ.Lucas Không có câu trả lời thỏa đáng cho câu hỏi đó. Tất cả mọi thứ đều kết thúc để 'không phá vỡ hợp đồng'.
Luc Danton

2

Khi operator=hàm hiện tại của bạn được viết, vì bạn đã tạo đối số tham chiếu giá trị const, nên không có cách nào bạn có thể "ăn cắp" các con trỏ và thay đổi giá trị của tham chiếu giá trị đến ... bạn chỉ không thể thay đổi nó, bạn chỉ có thể đọc từ nó. Tôi sẽ chỉ gặp sự cố nếu bạn bắt đầu gọi deletecon trỏ, v.v. trong thisđối tượng của mình giống như bạn làm trong một operator=phương thức tham chiếu lvaue thông thường , nhưng kiểu đó đánh bại điểm của phiên bản rvalue ... tức là, nó sẽ có vẻ thừa khi sử dụng phiên bản rvalue về cơ bản thực hiện các thao tác tương tự thường được để lại cho phương thức const-lvalue operator=.

Bây giờ nếu bạn đã định nghĩa của mình operator=để lấy consttham chiếu không có giá trị, thì cách duy nhất tôi có thể thấy kiểm tra được yêu cầu là nếu bạn chuyển thisđối tượng vào một hàm cố ý trả về tham chiếu giá trị thay vì tạm thời.

Ví dụ: giả sử ai đó đã cố gắng viết một operator+hàm và sử dụng kết hợp các tham chiếu rvalue và tham chiếu lvalue để "ngăn chặn" các khoảng thời gian thừa được tạo ra trong một số hoạt động cộng xếp chồng trên kiểu đối tượng:

struct A; //defines operator=(A&& rhs) where it will "steal" the pointers
          //of rhs and set the original pointers of rhs to NULL

A&& operator+(A& rhs, A&& lhs)
{
    //...code

    return std::move(rhs);
}

A&& operator+(A&& rhs, A&&lhs)
{
    //...code

    return std::move(rhs);
}

int main()
{
    A a;

    a = (a + A()) + A(); //calls operator=(A&&) with reference bound to a

    //...rest of code
}

Bây giờ, từ những gì tôi hiểu về tham chiếu rvalue, không khuyến khích làm như trên (tức là, bạn chỉ nên trả về một tham chiếu tạm thời, không phải tham chiếu rvalue), nhưng, nếu ai đó vẫn làm điều đó, thì bạn muốn kiểm tra để thực hiện chắc chắn rằng tham chiếu giá trị đến không tham chiếu đến cùng một đối tượng như thiscon trỏ.


Lưu ý rằng "a = std :: move (a)" là một cách nhỏ để xảy ra tình huống này. Câu trả lời của bạn là hợp lệ mặc dù.
Vaughn Cato

1
Hoàn toàn đồng ý rằng đó là cách đơn giản nhất, mặc dù tôi nghĩ rằng hầu hết mọi người sẽ không cố tình làm điều đó :-) ... Hãy nhớ rằng nếu tham chiếu rvalue là const, thì bạn chỉ có thể đọc từ nó, vì vậy chỉ cần thực hiện kiểm tra nếu bạn quyết định operator=(const T&&)thực hiện việc khởi tạo lại tương tự như thisbạn sẽ làm trong một operator=(const T&)phương pháp điển hình thay vì một hoạt động kiểu hoán đổi (ví dụ: đánh cắp con trỏ, v.v. thay vì tạo bản sao sâu).
Jason

1

Câu trả lời của tôi vẫn là nhiệm vụ di chuyển không nhất thiết phải được lưu lại so với tự chuyển nhượng, nhưng nó có một cách giải thích khác. Hãy xem xét std :: unique_ptr. Nếu tôi thực hiện một cái, tôi sẽ làm như thế này:

unique_ptr& operator=(unique_ptr&& x) {
  delete ptr_;
  ptr_ = x.ptr_;
  x.ptr_ = nullptr;
  return *this;
}

Nếu bạn nhìn Scott Meyers giải thích điều này , anh ấy cũng làm điều gì đó tương tự. (Nếu bạn đi lang thang tại sao không thực hiện trao đổi - nó có thêm một lần ghi). Và điều này không an toàn cho việc tự phân công.

Đôi khi điều này là không may. Xem xét chuyển ra khỏi vectơ tất cả các số chẵn:

src.erase(
  std::partition_copy(src.begin(), src.end(),
                      src.begin(),
                      std::back_inserter(even),
                      [](int num) { return num % 2; }
                      ).first,
  src.end());

Điều này là tốt cho số nguyên nhưng tôi không tin rằng bạn có thể làm cho một cái gì đó như thế này hoạt động với ngữ nghĩa di chuyển.

Để kết luận: việc chuyển quyền gán cho bản thân đối tượng là không ổn và bạn phải đề phòng nó.

Cập nhật nhỏ.

  1. Tôi không đồng ý với Howard, đó là một ý kiến ​​tồi, nhưng tôi vẫn nghĩ - tôi nghĩ rằng việc tự chuyển các đối tượng "đã chuyển đi" nên hiệu quả, bởi vì nó swap(x, x)nên hiệu quả. Thuật toán thích những thứ này! Nó luôn luôn tốt đẹp khi một hộp góc hoạt động. (Và tôi vẫn chưa thấy trường hợp nào mà nó không miễn phí. Tuy nhiên, không có nghĩa là nó không tồn tại).
  2. Đây là cách thực hiện gán unique_ptrs trong libc ++: unique_ptr& operator=(unique_ptr&& u) noexcept { reset(u.release()); ...} An toàn cho việc tự chuyển nhượng.
  3. Nguyên tắc cốt lõi cho rằng bạn nên tự chuyển nhiệm vụ.

0

Có một tình huống (cái này == rhs) tôi có thể nghĩ đến. Đối với tuyên bố này: Myclass obj; std :: move (obj) = std :: move (obj)


Myclass obj; std :: move (obj) = std :: move (obj);
little_monster
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.