Thành ngữ copy-and-exchange là gì?


2002

Thành ngữ này là gì và khi nào nên sử dụng? Những vấn đề nào nó giải quyết? Thành ngữ có thay đổi khi C ++ 11 được sử dụng không?

Mặc dù nó đã được đề cập ở nhiều nơi, chúng tôi không có câu hỏi và câu trả lời "nó là gì", vì vậy đây là. Đây là một danh sách một phần của những nơi mà nó đã được đề cập trước đây:


7
gotw.ca/gotw/059.htm từ Herb Sutter
DumbCoder

2
Tuyệt vời, tôi đã liên kết câu hỏi này từ câu trả lời của tôi để di chuyển ngữ nghĩa .
dòng chảy

4
Ý tưởng tốt để có một lời giải thích đầy đủ cho thành ngữ này, nó phổ biến đến mức mọi người nên biết về nó.
Matthieu M.

16
Cảnh báo: Thành ngữ sao chép / hoán đổi được sử dụng thường xuyên hơn nhiều so với nó hữu ích. Nó thường có hại cho hiệu suất khi không cần một đảm bảo an toàn ngoại lệ mạnh từ chuyển nhượng bản sao. Và khi cần sự an toàn ngoại lệ mạnh mẽ cho việc gán bản sao, nó có thể dễ dàng được cung cấp bởi một hàm chung ngắn, ngoài ra một toán tử gán bản sao nhanh hơn nhiều. Xem sl slideshoware.net/ripplelabs/howard-hinnant-accu2014 slide 43 - 53. Tóm tắt: copy / exchange là một công cụ hữu ích trong hộp công cụ. Nhưng nó đã được bán trên thị trường và sau đó thường bị lạm dụng.
Howard Hinnant

2
@HowardHinnant: Vâng, +1 cho điều đó. Tôi đã viết điều này vào thời điểm mà gần như mọi câu hỏi của C ++ là "giúp lớp tôi gặp sự cố khi sao chép nó" và đây là câu trả lời của tôi. Nó phù hợp khi bạn chỉ muốn làm việc sao chép / di chuyển ngữ nghĩa hoặc bất cứ điều gì để bạn có thể chuyển sang những thứ khác, nhưng nó không thực sự tối ưu. Vui lòng đặt từ chối trách nhiệm lên đầu câu trả lời của tôi nếu bạn nghĩ rằng điều đó sẽ giúp ích.
GManNickG

Câu trả lời:


2184

Tổng quat

Tại sao chúng ta cần thành ngữ sao chép và trao đổi?

Bất kỳ lớp nào quản lý tài nguyên ( trình bao bọc , như con trỏ thông minh) đều cần thực hiện The Big Three . Trong khi các mục tiêu và việc thực hiện của trình tạo bản sao và hàm hủy là đơn giản, thì toán tử gán-sao chép được cho là sắc thái và khó nhất. Nên làm thế nào? Những cạm bẫy cần phải tránh?

Thành ngữ copy-and-exchange là giải pháp và hỗ trợ một cách tao nhã cho toán tử gán trong việc đạt được hai điều: tránh sao chép mã và cung cấp một đảm bảo ngoại lệ mạnh .

Làm thế nào nó hoạt động?

Về mặt khái niệm , nó hoạt động bằng cách sử dụng chức năng của nhà xây dựng bản sao để tạo một bản sao dữ liệu cục bộ, sau đó lấy dữ liệu được sao chép bằng một swaphàm, hoán đổi dữ liệu cũ với dữ liệu mới. Các bản sao tạm thời sau đó phá hủy, lấy dữ liệu cũ với nó. Chúng tôi còn lại một bản sao của dữ liệu mới.

Để sử dụng thành ngữ copy-and-exchange, chúng ta cần ba điều: một hàm tạo sao chép hoạt động, một hàm hủy hoạt động (cả hai đều là cơ sở của bất kỳ trình bao bọc nào, vì vậy dù sao cũng phải hoàn thành) và một swaphàm.

Hàm hoán đổi là một không ném , hoán đổi hai đối tượng của một lớp, thành viên cho thành viên. Chúng tôi có thể bị cám dỗ để sử dụng std::swapthay vì cung cấp của chúng tôi, nhưng điều này là không thể; std::swapsử dụng trình xây dựng sao chép và toán tử gán sao chép trong quá trình triển khai và cuối cùng chúng tôi sẽ cố gắng xác định toán tử gán theo thuật ngữ của chính nó!

(Không chỉ vậy, nhưng các cuộc gọi không đủ tiêu chuẩn để swap sẽ sử dụng toán tử hoán đổi tùy chỉnh của chúng tôi, bỏ qua việc xây dựng và phá hủy không cần thiết của lớp chúng tôi std::swapsẽ đòi hỏi.)


Một lời giải thích sâu sắc

Mục đích

Hãy xem xét một trường hợp cụ thể. Chúng tôi muốn quản lý, trong một lớp vô dụng, một mảng động. Chúng tôi bắt đầu với một constructor hoạt động, copy-constructor và hàm hủy:

#include <algorithm> // std::copy
#include <cstddef> // std::size_t

class dumb_array
{
public:
    // (default) constructor
    dumb_array(std::size_t size = 0)
        : mSize(size),
          mArray(mSize ? new int[mSize]() : nullptr)
    {
    }

    // copy-constructor
    dumb_array(const dumb_array& other)
        : mSize(other.mSize),
          mArray(mSize ? new int[mSize] : nullptr),
    {
        // note that this is non-throwing, because of the data
        // types being used; more attention to detail with regards
        // to exceptions must be given in a more general case, however
        std::copy(other.mArray, other.mArray + mSize, mArray);
    }

    // destructor
    ~dumb_array()
    {
        delete [] mArray;
    }

private:
    std::size_t mSize;
    int* mArray;
};

Lớp này gần như quản lý mảng thành công, nhưng nó cần operator= hoạt động chính xác.

Một giải pháp thất bại

Đây là cách thực hiện ngây thơ có thể trông:

// the hard part
dumb_array& operator=(const dumb_array& other)
{
    if (this != &other) // (1)
    {
        // get rid of the old data...
        delete [] mArray; // (2)
        mArray = nullptr; // (2) *(see footnote for rationale)

        // ...and put in the new
        mSize = other.mSize; // (3)
        mArray = mSize ? new int[mSize] : nullptr; // (3)
        std::copy(other.mArray, other.mArray + mSize, mArray); // (3)
    }

    return *this;
}

Và chúng tôi nói rằng chúng tôi đã hoàn thành; bây giờ quản lý một mảng, không có rò rỉ. Tuy nhiên, nó bị ba vấn đề, được đánh dấu tuần tự trong mã là (n).

  1. Đầu tiên là bài kiểm tra tự giao. Kiểm tra này phục vụ hai mục đích: đó là một cách dễ dàng để ngăn chúng tôi chạy mã không cần thiết khi tự gán và nó bảo vệ chúng tôi khỏi các lỗi tinh vi (chẳng hạn như chỉ xóa mảng để thử và sao chép nó). Nhưng trong tất cả các trường hợp khác, nó chỉ đơn thuần phục vụ để làm chậm chương trình và hoạt động như tiếng ồn trong mã; tự giao hiếm khi xảy ra, vì vậy hầu hết thời gian kiểm tra này là một sự lãng phí. Sẽ tốt hơn nếu người vận hành có thể làm việc đúng mà không cần nó.

  2. Thứ hai là nó chỉ cung cấp một đảm bảo ngoại lệ cơ bản. Nếu new int[mSize]thất bại, *thissẽ được sửa đổi. (Cụ thể, kích thước là sai và dữ liệu đã biến mất!) Để đảm bảo ngoại lệ mạnh mẽ, nó sẽ cần phải giống với:

    dumb_array& operator=(const dumb_array& other)
    {
        if (this != &other) // (1)
        {
            // get the new data ready before we replace the old
            std::size_t newSize = other.mSize;
            int* newArray = newSize ? new int[newSize]() : nullptr; // (3)
            std::copy(other.mArray, other.mArray + newSize, newArray); // (3)
    
            // replace the old data (all are non-throwing)
            delete [] mArray;
            mSize = newSize;
            mArray = newArray;
        }
    
        return *this;
    }
  3. Mã đã được mở rộng! Điều này dẫn chúng ta đến vấn đề thứ ba: sao chép mã. Toán tử gán của chúng tôi sao chép một cách hiệu quả tất cả các mã chúng tôi đã viết ở nơi khác và đó là một điều tồi tệ.

Trong trường hợp của chúng tôi, cốt lõi của nó chỉ có hai dòng (phân bổ và sao chép), nhưng với các tài nguyên phức tạp hơn, sự phình to mã này có thể khá rắc rối. Chúng ta nên cố gắng không bao giờ lặp lại chính mình.

(Người ta có thể tự hỏi: nếu cần nhiều mã này để quản lý một tài nguyên một cách chính xác, thì nếu lớp của tôi quản lý nhiều tài nguyên thì sao? Trong khi điều này có vẻ là một mối quan tâm hợp lệ và thực sự nó đòi hỏi không phải là tầm thường try/ catchmệnh đề, đây không phải là một mệnh đề phát hành. Đó là bởi vì một lớp chỉ nên quản lý một tài nguyên !)

Một giải pháp thành công

Như đã đề cập, thành ngữ sao chép và trao đổi sẽ khắc phục tất cả các vấn đề này. Nhưng ngay bây giờ, chúng ta có tất cả các yêu cầu ngoại trừ một: một swapchức năng. Mặc dù Quy tắc ba đòi hỏi thành công sự tồn tại của trình tạo bản sao, toán tử gán và hàm hủy của chúng ta, nhưng nó thực sự nên được gọi là "Ba lớn và một nửa": bất cứ khi nào lớp của bạn quản lý tài nguyên, nó cũng có ý nghĩa để cung cấp một swaphàm .

Chúng ta cần thêm chức năng trao đổi cho lớp của mình và chúng ta thực hiện điều đó như sau †:

class dumb_array
{
public:
    // ...

    friend void swap(dumb_array& first, dumb_array& second) // nothrow
    {
        // enable ADL (not necessary in our case, but good practice)
        using std::swap;

        // by swapping the members of two objects,
        // the two objects are effectively swapped
        swap(first.mSize, second.mSize);
        swap(first.mArray, second.mArray);
    }

    // ...
};

( Đây là lời giải thích tại sao public friend swap.) Bây giờ không chỉ chúng ta có thể trao đổi của chúng ta dumb_array, mà nói chung các giao dịch hoán đổi có thể hiệu quả hơn; nó chỉ hoán đổi con trỏ và kích thước, thay vì phân bổ và sao chép toàn bộ mảng. Ngoài phần thưởng này về chức năng và hiệu quả, giờ đây chúng tôi đã sẵn sàng để thực hiện thành ngữ sao chép và trao đổi.

Nếu không có thêm rắc rối, toán tử gán của chúng tôi là:

dumb_array& operator=(dumb_array other) // (1)
{
    swap(*this, other); // (2)

    return *this;
}

Và đó là nó! Với một cú trượt ngã, cả ba vấn đề được giải quyết một cách tao nhã cùng một lúc.

Tại sao nó hoạt động?

Trước tiên chúng tôi nhận thấy một lựa chọn quan trọng: đối số tham số được lấy theo giá trị . Trong khi người ta có thể dễ dàng làm như sau (và thực tế, nhiều triển khai ngây thơ của thành ngữ này):

dumb_array& operator=(const dumb_array& other)
{
    dumb_array temp(other);
    swap(*this, temp);

    return *this;
}

Chúng tôi mất một cơ hội tối ưu hóa quan trọng . Không chỉ vậy, nhưng lựa chọn này rất quan trọng trong C ++ 11, sẽ được thảo luận sau. (Trên một lưu ý chung, một hướng dẫn hữu ích đáng chú ý như sau: nếu bạn định tạo một bản sao của một thứ gì đó trong hàm, hãy để trình biên dịch thực hiện nó trong danh sách tham số. ‡)

Dù bằng cách nào, phương pháp lấy tài nguyên này của chúng tôi là chìa khóa để loại bỏ sao chép mã: chúng tôi có thể sử dụng mã từ trình tạo bản sao để tạo bản sao và không bao giờ phải lặp lại bất kỳ bit nào của nó. Bây giờ bản sao được thực hiện, chúng tôi đã sẵn sàng để trao đổi.

Quan sát rằng khi nhập chức năng, tất cả dữ liệu mới đã được phân bổ, sao chép và sẵn sàng để sử dụng. Đây là điều mang lại cho chúng tôi một bảo đảm ngoại lệ mạnh mẽ miễn phí: chúng tôi thậm chí sẽ không nhập chức năng nếu việc xây dựng bản sao thất bại và do đó không thể thay đổi trạng thái của*this . (Những gì chúng tôi đã làm thủ công trước đây để đảm bảo ngoại lệ mạnh mẽ, trình biên dịch đang làm cho chúng tôi bây giờ; như thế nào.)

Tại thời điểm này, chúng tôi không có nhà, vì swapkhông ném. Chúng tôi trao đổi dữ liệu hiện tại của chúng tôi với dữ liệu được sao chép, thay đổi trạng thái của chúng tôi một cách an toàn và dữ liệu cũ sẽ được đưa vào tạm thời. Dữ liệu cũ sau đó được giải phóng khi hàm trả về. (Khi phạm vi của tham số kết thúc và hàm hủy của nó được gọi.)

Vì thành ngữ lặp lại không có mã, chúng tôi không thể đưa ra các lỗi trong toán tử. Lưu ý rằng điều này có nghĩa là chúng tôi không cần kiểm tra tự gán, cho phép thực hiện thống nhất một lần duy nhấtoperator= . (Ngoài ra, chúng tôi không còn có hình phạt về hiệu suất đối với việc không tự gán.)

Và đó là thành ngữ copy-and-exchange.

C ++ 11 thì sao?

Phiên bản tiếp theo của C ++, C ++ 11, tạo ra một thay đổi rất quan trọng đối với cách chúng ta quản lý tài nguyên: Quy tắc ba bây giờ là Quy tắc bốn (và một nửa). Tại sao? Bởi vì chúng ta không chỉ cần có khả năng sao chép-xây dựng tài nguyên của mình, mà chúng ta còn cần phải di chuyển-xây dựng nó .

May mắn cho chúng tôi, điều này là dễ dàng:

class dumb_array
{
public:
    // ...

    // move constructor
    dumb_array(dumb_array&& other) noexcept ††
        : dumb_array() // initialize via default constructor, C++11 only
    {
        swap(*this, other);
    }

    // ...
};

Những gì đang xảy ra ở đây? Nhắc lại mục tiêu của xây dựng di chuyển: để lấy tài nguyên từ một thể hiện khác của lớp, để nó ở trạng thái được đảm bảo có thể gán và phá hủy được.

Vì vậy, những gì chúng tôi đã làm rất đơn giản: khởi tạo thông qua hàm tạo mặc định (tính năng C ++ 11), sau đó trao đổi với other; chúng ta biết một thể hiện được xây dựng mặc định của lớp chúng ta có thể được gán và hủy một cách an toàn, vì vậy chúng ta biết othersẽ có thể làm tương tự, sau khi hoán đổi.

(Lưu ý rằng một số trình biên dịch không hỗ trợ ủy quyền của hàm tạo; trong trường hợp này, chúng ta phải mặc định xây dựng lớp theo cách thủ công. Đây là một nhiệm vụ không may nhưng rất may mắn.)

Tại sao nó hoạt động?

Đó là thay đổi duy nhất chúng ta cần thực hiện cho lớp của mình, vậy tại sao nó hoạt động? Hãy nhớ quyết định cực kỳ quan trọng mà chúng tôi đưa ra để biến tham số thành giá trị và không phải là tham chiếu:

dumb_array& operator=(dumb_array other); // (1)

Bây giờ, nếu otherđang được khởi tạo với một giá trị, nó sẽ được xây dựng di chuyển . Hoàn hảo. Theo cách tương tự C ++ 03, chúng ta hãy sử dụng lại chức năng xây dựng bản sao bằng cách lấy tham số theo giá trị, C ++ 11 sẽ tự động chọn công cụ xây dựng di chuyển khi thích hợp. (Và, tất nhiên, như đã đề cập trong bài viết được liên kết trước đây, việc sao chép / di chuyển giá trị có thể chỉ đơn giản là bị loại bỏ hoàn toàn.)

Và như vậy kết luận thành ngữ sao chép và trao đổi.


Chú thích

* Tại sao chúng ta đặt mArraythành null? Bởi vì nếu có thêm mã nào trong toán tử ném, hàm hủy của dumb_arraycó thể được gọi; và nếu điều đó xảy ra mà không đặt nó thành null, chúng tôi sẽ cố gắng xóa bộ nhớ đã bị xóa! Chúng tôi tránh điều này bằng cách đặt nó thành null, vì xóa null là không hoạt động.

Có những tuyên bố khác mà chúng tôi nên chuyên môn hóa std::swapcho loại của mình, cung cấp một swapchức năng miễn phí bên cạnh lớp swap, v.v. Nhưng điều này là không cần thiết: mọi hoạt động sử dụng hợp lý swapsẽ thông qua một cuộc gọi không đủ tiêu chuẩn và chức năng của chúng tôi sẽ tìm thấy thông qua ADL . Một chức năng sẽ làm.

Lý do rất đơn giản: một khi bạn có tài nguyên cho chính mình, bạn có thể trao đổi và / hoặc di chuyển nó (C ++ 11) bất cứ nơi nào nó cần. Và bằng cách tạo bản sao trong danh sách tham số, bạn tối đa hóa tối ưu hóa.

Trình xây dựng di chuyển thường là noexcept, nếu không, một số mã (ví dụ std::vectorthay đổi kích thước logic) sẽ sử dụng hàm tạo sao chép ngay cả khi di chuyển sẽ có ý nghĩa. Tất nhiên, chỉ đánh dấu nó không có ngoại lệ nếu mã bên trong không ném ngoại lệ.


17
@GMan: Tôi sẽ lập luận rằng một lớp quản lý nhiều tài nguyên cùng một lúc sẽ thất bại (an toàn ngoại lệ trở thành ác mộng) và tôi rất khuyến nghị rằng một lớp sẽ quản lý MỘT tài nguyên HOẶC nó có chức năng kinh doanh và sử dụng các trình quản lý.
Matthieu M.

22
Tôi không hiểu tại sao phương thức trao đổi được tuyên bố là bạn ở đây?
szx

9
@asd: Để cho phép nó được tìm thấy thông qua ADL.
GManNickG

8
@neuviemeporte: Với dấu ngoặc đơn, các phần tử mảng được khởi tạo mặc định. Không có, họ không được khởi tạo. Vì trong trình tạo bản sao, chúng ta sẽ ghi đè lên các giá trị, nên chúng ta có thể bỏ qua việc khởi tạo.
GManNickG

10
@neuviemeporte: Bạn cần swaptìm thấy bạn trong ADL nếu bạn muốn nó hoạt động trong hầu hết các mã chung mà bạn sẽ gặp, như boost::swapvà các trường hợp hoán đổi khác nhau. Hoán đổi là một vấn đề khó khăn trong C ++ và nói chung tất cả chúng ta đều đồng ý rằng một điểm truy cập duy nhất là tốt nhất (cho tính nhất quán) và cách duy nhất để làm điều đó nói chung là một chức năng miễn phí ( intkhông thể có thành viên trao đổi, ví dụ). Xem câu hỏi của tôi cho một số nền tảng.
GManNickG

274

Chuyển nhượng, tại trung tâm của nó, là hai bước: phá bỏ trạng thái cũ của đối tượngxây dựng trạng thái mới của nó như một bản sao của trạng thái của một đối tượng khác.

Về cơ bản, đó là những gì kẻ hủy diệt và người xây dựng bản sao làm, vì vậy ý ​​tưởng đầu tiên sẽ là ủy thác công việc cho họ. Tuy nhiên, vì sự phá hủy không được thất bại, trong khi xây dựng có thể, chúng tôi thực sự muốn làm điều đó theo cách khác : đầu tiên thực hiện phần xây dựng và, nếu điều đó thành công, sau đó thực hiện phần phá hoại . Thành ngữ copy-and-exchange là một cách để làm điều đó: Đầu tiên, nó gọi hàm tạo sao chép của lớp để tạo một đối tượng tạm thời, sau đó hoán đổi dữ liệu của nó với tạm thời và sau đó cho phép hàm hủy tạm thời phá hủy trạng thái cũ.
Từswap()được cho là không bao giờ thất bại, phần duy nhất có thể thất bại là việc xây dựng bản sao. Điều đó được thực hiện đầu tiên và nếu thất bại, sẽ không có gì thay đổi trong đối tượng được nhắm mục tiêu.

Ở dạng tinh chế, sao chép và trao đổi được thực hiện bằng cách thực hiện sao chép bằng cách khởi tạo tham số (không tham chiếu) của toán tử gán:

T& operator=(T tmp)
{
    this->swap(tmp);
    return *this;
}

1
Tôi nghĩ rằng việc đề cập đến pimpl cũng quan trọng như đề cập đến bản sao, hoán đổi và phá hủy. Việc hoán đổi không phải là ngoại lệ an toàn. Nó an toàn ngoại lệ vì hoán đổi con trỏ là ngoại lệ an toàn. Bạn không phải sử dụng pimpl, nhưng nếu không thì bạn phải đảm bảo rằng mỗi lần hoán đổi của một thành viên là ngoại lệ - an toàn. Đó có thể là một cơn ác mộng khi những thành viên này có thể thay đổi và thật tầm thường khi họ bị giấu sau một cái mụn. Và sau đó, đến chi phí của pimpl. Điều này dẫn chúng ta đến kết luận rằng sự an toàn ngoại lệ thường phải trả giá khi thực hiện.
wilmustell

7
std::swap(this_string, that)không cung cấp một đảm bảo không ném. Nó cung cấp sự an toàn ngoại lệ mạnh mẽ, nhưng không phải là một đảm bảo không ném.
wilmustell

11
@wilmustell: Trong C ++ 03, không có đề cập đến các trường hợp ngoại lệ có khả năng bị ném bởi std::string::swap(được gọi bởi std::swap). Trong C ++ 0x, std::string::swapnoexceptvà không phải ném ngoại lệ.
James McNellis

2
@sbi @JamesMcNellis ok, nhưng vấn đề vẫn còn tồn tại: nếu bạn có thành viên của loại lớp, bạn phải đảm bảo trao đổi chúng là không ném. Nếu bạn có một thành viên là con trỏ thì đó là chuyện nhỏ. Nếu không thì không.
wilmustell

2
@wilmustell: Tôi nghĩ đó là điểm của sự hoán đổi: nó không bao giờ ném và nó luôn luôn là O (1) (vâng, tôi biết, std::array...)
sbi

44

Có một số câu trả lời tốt rồi. Tôi sẽ tập trung chủ yếu vào những gì tôi nghĩ họ thiếu - một lời giải thích về "khuyết điểm" với thành ngữ sao chép và hoán đổi ....

Thành ngữ copy-and-exchange là gì?

Một cách để thực hiện toán tử gán theo hàm trao đổi:

X& operator=(X rhs)
{
    swap(rhs);
    return *this;
}

Ý tưởng cơ bản là:

  • phần dễ bị lỗi nhất khi gán cho đối tượng là đảm bảo mọi tài nguyên mà trạng thái mới cần có được (ví dụ: bộ nhớ, mô tả)

  • việc mua lại đó có thể được thử trước khi sửa đổi trạng thái hiện tại của đối tượng (tức là *this) nếu một bản sao của giá trị mới được tạo ra, đó là lý do tại sao rhsđược chấp nhận bởi giá trị (nghĩa là sao chép) thay vì tham chiếu

  • trao đổi tình trạng của bản sao cục bộ rhs*thisthường tương đối dễ dàng mà không có thất bại / trường hợp ngoại lệ tiềm năng, đưa ra bản sao cục bộ không cần bất kỳ trạng thái đặc biệt sau đó (chỉ cần phù hợp với trạng thái cho các destructor để chạy, nhiều càng tốt cho một đối tượng được di chuyển từ trong> = C ++ 11)

Nó nên được sử dụng lúc nào? (Vấn đề nào giải quyết [/ tạo] ?)

  • Khi bạn muốn đối tượng được chỉ định không bị ảnh hưởng bởi một nhiệm vụ ném ngoại lệ, giả sử bạn có hoặc có thể viết một swapbảo đảm ngoại lệ mạnh và lý tưởng là không thể thất bại / throw..

  • Khi bạn muốn một cách rõ ràng, dễ hiểu, mạnh mẽ để xác định toán tử gán theo thuật ngữ của hàm tạo sao chép (đơn giản hơn) swapvà hàm hủy.

    • Tự gán được thực hiện như một bản sao và trao đổi để tránh các trường hợp cạnh bị bỏ qua.

  • Khi bất kỳ hình phạt hiệu suất hoặc sử dụng tài nguyên cao hơn trong giây lát được tạo bằng cách có thêm một đối tượng tạm thời trong quá trình gán không quan trọng đối với ứng dụng của bạn. ⁂

Ing swapném: nói chung có thể hoán đổi đáng tin cậy các thành viên dữ liệu mà các đối tượng theo dõi bằng con trỏ, nhưng các thành viên dữ liệu không phải là con trỏ không có trao đổi miễn phí, hoặc việc hoán đổi phải được thực hiện như là X tmp = lhs; lhs = rhs; rhs = tmp;sao chép hoặc xây dựng hoặc gán có thể ném, vẫn có khả năng thất bại khiến một số thành viên dữ liệu bị tráo đổi và những người khác thì không. Tiềm năng này áp dụng ngay cả với C ++ 03 std::stringnhư James nhận xét về câu trả lời khác:

@wilmustell: Trong C ++ 03, không có đề cập đến các trường hợp ngoại lệ có khả năng bị ném bởi std :: string :: exchange (được gọi bởi std :: exchange). Trong C ++ 0x, std :: string :: exchange là không có ngoại lệ và không được ném ngoại lệ. - James McNellis ngày 22 tháng 12 năm 10 lúc 15:24


Việc thực thi toán tử gán có vẻ lành mạnh khi gán từ một đối tượng riêng biệt có thể dễ dàng thất bại cho việc tự gán. Mặc dù có vẻ như không thể tưởng tượng được rằng mã máy khách thậm chí sẽ tự gán, nhưng nó có thể xảy ra tương đối dễ dàng trong các hoạt động của algo trên các container, với x = f(x);f(có lẽ chỉ dành cho một số #ifdefnhánh) một ala macro #define f(x) xhoặc một hàm trả về tham chiếu xhoặc thậm chí (có thể không hiệu quả nhưng ngắn gọn) mã như x = c1 ? x * 2 : c2 ? x / 2 : x;). Ví dụ:

struct X
{
    T* p_;
    size_t size_;
    X& operator=(const X& rhs)
    {
        delete[] p_;  // OUCH!
        p_ = new T[size_ = rhs.size_];
        std::copy(p_, rhs.p_, rhs.p_ + rhs.size_);
    }
    ...
};

Khi tự gán, mã ở trên sẽ xóa x.p_;, chỉ p_vào một vùng heap mới được phân bổ, sau đó cố gắng đọc dữ liệu chưa được xác nhận trong đó (Hành vi không xác định), nếu điều đó không làm gì quá kỳ lạ, hãy copythử tự gán cho mọi người- phá hủy 'T'!


Id Thành ngữ sao chép và hoán đổi có thể đưa ra sự thiếu hiệu quả hoặc hạn chế do sử dụng thêm tạm thời (khi tham số của toán tử được xây dựng sao chép):

struct Client
{
    IP_Address ip_address_;
    int socket_;
    X(const X& rhs)
      : ip_address_(rhs.ip_address_), socket_(connect(rhs.ip_address_))
    { }
};

Ở đây, một văn bản viết tay Client::operator=có thể kiểm tra xem *thisđã được kết nối với cùng một máy chủ chưa rhs(có thể gửi mã "đặt lại" nếu hữu ích), trong khi phương pháp sao chép và trao đổi sẽ gọi trình tạo bản sao có thể được viết để mở một kết nối ổ cắm riêng biệt sau đó đóng cái ban đầu. Điều đó không chỉ có nghĩa là một tương tác mạng từ xa thay vì một bản sao biến trong quá trình đơn giản, nó có thể chạy các giới hạn máy khách hoặc máy chủ đối với các tài nguyên hoặc kết nối ổ cắm. (Tất nhiên lớp này có giao diện khá kinh khủng, nhưng đó là vấn đề khác ;-P).


4
Điều đó nói rằng, kết nối ổ cắm chỉ là một ví dụ - nguyên tắc tương tự áp dụng cho bất kỳ khởi tạo đắt tiền nào, chẳng hạn như thăm dò / khởi tạo / hiệu chỉnh phần cứng, tạo ra một chuỗi các luồng hoặc số ngẫu nhiên, các tác vụ mã hóa nhất định, bộ nhớ cache, quét hệ thống tệp, cơ sở dữ liệu kết nối vv ..
Tony Delroy

Có thêm một con (đồ sộ). Tính đến thông số kỹ thuật hiện hành về mặt kỹ thuật đối tượng sẽ không có một nhà điều hành di chuyển-nhượng! Nếu sau này được sử dụng làm thành viên của một lớp, lớp mới sẽ không được tạo tự động move-ctor! Nguồn: youtu.be/mYrbivnruYw?t=43m14s
user362515

3
Vấn đề chính với toán tử gán sao chép Clientlà gán không bị cấm.
sbi

Trong ví dụ máy khách, lớp nên được làm cho không thể sao chép được.
John Z. Li

25

Câu trả lời này giống như một sự bổ sung và một sửa đổi nhỏ cho các câu trả lời ở trên.

Trong một số phiên bản của Visual Studio (và có thể cả các trình biên dịch khác), có một lỗi thực sự gây phiền nhiễu và không có ý nghĩa. Vì vậy, nếu bạn khai báo / xác định swapchức năng của bạn như thế này:

friend void swap(A& first, A& second) {

    std::swap(first.size, second.size);
    std::swap(first.arr, second.arr);

}

... trình biên dịch sẽ mắng bạn khi bạn gọi swaphàm:

nhập mô tả hình ảnh ở đây

Điều này có liên quan đến một friendhàm được gọi và thisđối tượng được truyền dưới dạng tham số.


Một cách khác là không sử dụng friendtừ khóa và xác định lại swapchức năng:

void swap(A& other) {

    std::swap(size, other.size);
    std::swap(arr, other.arr);

}

Lần này, bạn chỉ có thể gọi swapvà chuyển qua other, do đó làm cho trình biên dịch hài lòng:

nhập mô tả hình ảnh ở đây


Rốt cuộc, bạn không cần sử dụng friendhàm để hoán đổi 2 đối tượng. Nó có ý nghĩa nhiều như làm cho swapmột hàm thành viên có một otherđối tượng là một tham số.

Bạn đã có quyền truy cập vào thisđối tượng, vì vậy việc chuyển nó vào dưới dạng tham số là không cần thiết về mặt kỹ thuật.


1
@GManNickG dropbox.com/s/o1mitwcpxmawcot/example.cpp dropbox.com/s/jrjrn5dh1zez5vy/Untitle.jpg . Đây là một phiên bản đơn giản hóa. Một lỗi dường như xảy ra mỗi khi một friendhàm được gọi với *thistham số
Oleksiy

1
@GManNickG như tôi đã nói, đó là một lỗi và có thể hoạt động tốt cho những người khác. Tôi chỉ muốn giúp đỡ một số người có thể có cùng vấn đề với tôi. Tôi đã thử điều này với cả Visual Studio 2012 Express và 2013 Preview và điều duy nhất khiến nó biến mất, là sửa đổi của tôi
Oleksiy

8
@GManNickG nó sẽ không phù hợp với một bình luận với tất cả các hình ảnh và ví dụ mã. Và sẽ ổn thôi nếu mọi người downvote, tôi chắc chắn có ai đó đang gặp phải lỗi tương tự; thông tin trong bài viết này có thể chỉ là những gì họ cần.
Oleksiy

14
lưu ý rằng đây chỉ là một lỗi trong tô sáng mã IDE (IntelliSense) ... Nó sẽ biên dịch tốt mà không có cảnh báo / lỗi.
Amro

3
Vui lòng báo cáo lỗi VS tại đây nếu bạn chưa thực hiện (và nếu nó chưa được sửa) connect.microsoft.com/VisualStudio
Matt

15

Tôi muốn thêm một lời cảnh báo khi bạn đang xử lý các thùng chứa nhận biết phân bổ kiểu C ++ 11. Trao đổi và phân công có ngữ nghĩa khác nhau tinh tế.

Để cụ thể, chúng ta hãy xem xét một container std::vector<T, A>, Amột số loại phân bổ trạng thái và chúng ta sẽ so sánh các chức năng sau:

void fs(std::vector<T, A> & a, std::vector<T, A> & b)
{ 
    a.swap(b);
    b.clear(); // not important what you do with b
}

void fm(std::vector<T, A> & a, std::vector<T, A> & b)
{
    a = std::move(b);
}

Mục đích của cả hai chức năng fsfmlà để cung cấp cho anhà nước bđã có ban đầu. Tuy nhiên, có một câu hỏi ẩn: Điều gì xảy ra nếu a.get_allocator() != b.get_allocator()? Câu trả lơi con phụ thuộc vao nhiêu thư. Hãy viết AT = std::allocator_traits<A>.

  • Nếu AT::propagate_on_container_move_assignmentstd::true_type, sau đó fmgán lại cho người cấp phát avới giá trị của b.get_allocator(), nếu không thì không, và atiếp tục sử dụng công cụ cấp phát ban đầu của nó. Trong trường hợp đó, các yếu tố dữ liệu cần được hoán đổi riêng lẻ, vì việc lưu trữ abkhông tương thích.

  • Nếu AT::propagate_on_container_swapstd::true_type, sau đó fshoán đổi cả dữ liệu và phân bổ theo cách mong đợi.

  • Nếu AT::propagate_on_container_swapstd::false_type, thì chúng ta cần kiểm tra động.

    • Nếu a.get_allocator() == b.get_allocator(), sau đó hai container sử dụng lưu trữ tương thích và trao đổi tiến hành theo cách thông thường.
    • Tuy nhiên, nếu a.get_allocator() != b.get_allocator(), chương trình có hành vi không xác định (xem [container.requirements.general / 8].

Kết quả cuối cùng là việc hoán đổi đã trở thành một hoạt động không hề nhỏ trong C ++ 11 ngay khi container của bạn bắt đầu hỗ trợ các bộ cấp phát trạng thái. Đó là một "trường hợp sử dụng nâng cao", nhưng không hoàn toàn khó xảy ra, vì tối ưu hóa di chuyển thường chỉ trở nên thú vị khi lớp của bạn quản lý tài nguyên và bộ nhớ là một trong những tài nguyên phổ biến nhất.

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.