Câu hỏi Thành ngữ sao chép và hoán đổi là gì?


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

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


1671
2017-07-19 08:42


gốc


gotw.ca/gotw/059.htm từ Herb Sutter - DumbCoder
Tuyệt vời, tôi đã liên kết câu hỏi này từ trả lời để di chuyển ngữ nghĩa. - fredoverflow
Ý tưởng tốt để có một lời giải thích đầy đủ cho thành ngữ này, nó rất phổ biến mà mọi người nên biết về nó. - Matthieu M.
Cảnh báo: Thành ngữ sao chép / hoán đổi được sử dụng thường xuyên hơn rất nhiều so với thành ngữ hữu ích. Nó thường có hại cho hiệu suất khi không cần đảm bảo an toàn ngoại lệ mạnh mẽ từ việc chuyển nhượng bản sao. Và khi cần có sự an toàn ngoại lệ mạnh mẽ để gán bản sao, nó có thể dễ dàng được cung cấp bởi một hàm tổng quát ngắn, ngoài một toán tử gán bản sao nhanh hơn nhiều. Xem slideshare.net/ripplelabs/howard-hinnant-accu2014 slide 43 - 53. Tóm tắt: copy / swap là một công cụ hữu ích trong hộp công cụ. Nhưng nó đã được trên thị trường và sau đó thường bị lạm dụng. - Howard Hinnant
@ HowardHinnant: Yeah, +1 cho điều đó. Tôi đã viết điều này tại một thời điểm mà gần như mọi câu hỏi C ++ là "giúp lớp của tôi gặp sự cố khi sao chép nó" và đây là câu trả lời của tôi. Nó thích hợp khi bạn chỉ muốn làm việc theo ngữ nghĩa, di chuyển, 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 tuyên bố từ chối trách nhiệm ở đầu câu trả lời của tôi nếu bạn cho rằng điều đó sẽ hữu ích. - GManNickG


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


Tổng quan

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

Bất kỳ lớp nào quản lý tài nguyên (a vỏ bánh, giống như một con trỏ thông minh) cần triển khai The Big Three. Trong khi các mục tiêu và việc thực hiện copy-constructor và destructor rất đơn giản, thì toán tử gán bản sao được cho là có sắc thái và khó khăn nhất. Làm thế nào nó nên được thực hiện? Những gì cạm bẫy cần phải tránh?

Các thành ngữ sao chép và hoán đổi là giải pháp, và thanh lịch hỗ trợ toán tử gán trong việc đạt được hai điều: tránh -sự sao chép mãvà cung cấp bảo đảm ngoại lệ mạnh mẽ.

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

Khái niệm, nó hoạt động bằng cách sử dụng chức năng của hàm tạo bản sao để tạo bản sao dữ liệu cục bộ, sau đó lấy dữ liệu đã sao chép bằng swap , trao đổi dữ liệu cũ với dữ liệu mới. Các bản sao tạm thời sau đó destructs, lấy dữ liệu cũ với nó. Chúng tôi còn lại với một bản sao của dữ liệu mới.

Để sử dụng thành ngữ sao chép và hoán đổi, chúng ta cần ba điều: một bản sao làm việc-constructor, một destructor làm việc (cả hai đều là cơ sở của bất kỳ trình bao bọc nào, vì vậy nên hoàn thành anyway), và swap chức năng.

Chức năng hoán đổi là một không ném chức năng 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::swap thay vì cung cấp cho riêng mình, nhưng điều này là không thể; std::swap sử dụng toán tử tạo bản sao và tác nhân sao chép trong quá trình thực hiện của nó, và cuối cùng chúng ta sẽ cố gắng xác định toán tử gán về 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 quá trình xây dựng không cần thiết và phá hủy lớp học của chúng tôi std::swap sẽ đòi hỏi.)


Giải thích chi tiết

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 khác vô dụng, một mảng năng động. Chúng ta bắt đầu với một hàm tạo làm việc, copy-constructor và destructor:

#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ể xem xét:

// 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; điều này 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ã như (n).

  1. Đầu tiên là bài kiểm tra tự gán. 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à bảo vệ chúng ta khỏi các lỗi tinh vi (chẳng hạn như xóa mảng chỉ để thử và sao chép nó). Nhưng trong tất cả các trường hợp khác, nó chỉ phục vụ làm chậm chương trình, và hoạt động như tiếng ồn trong mã; tự chuyển nhượng 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ể hoạt động bình thường nếu không có nó.

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

    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 ta sao chép hiệu quả tất cả mã mà chúng ta đã viết ở nơi khác, và đó là một điều khủng khiếp.

Trong trường hợp của chúng tôi, cốt lõi của nó chỉ là hai dòng (phân bổ và bản sao), nhưng với các tài nguyên phức tạp hơn, bloat mã này có thể khá phức tạp. 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, điều gì sẽ xảy ra nếu lớp của tôi quản lý nhiều tài nguyên? try/catch các mệnh đề, đây không phải là vấn đề. Đó là bởi vì một lớp nên quản lý chỉ 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à hoán đổi sẽ khắc phục tất cả các vấn đề này. Nhưng ngay bây giờ, chúng tôi có tất cả các yêu cầu ngoại trừ một yêu cầu: a swap chức năng. Trong khi Quy tắc Ba thành công đòi hỏi sự tồn tại của copy-constructor, toán tử gán và destructor của chúng ta, nó thực sự được gọi là "Big Three và Half": 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 swap chức năng.

Chúng ta cần thêm chức năng hoán đổi cho lớp của chúng ta, và chúng ta làm 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à giải thích tại sao public friend swap.) Bây giờ chúng ta không chỉ có thể hoán đổi dumb_array's, nhưng hoán đổi nói chung có thể hiệu quả hơn; nó chỉ hoán đổi con trỏ và kích 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ả, 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 ado, toán tử gán của chúng ta là:

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

    return *this;
}

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

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

Đầu tiên chúng ta nhận thấy một lựa chọn quan trọng: tham 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 sự, nhiều triển khai ngây thơ của thành ngữ làm):

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

    return *this;
}

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

Dù bằng cách nào, phương pháp thu thập tài nguyên của chúng tôi là chìa khóa để loại bỏ trùng lặp mã: chúng tôi 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ỳ phần nào của nó. Bây giờ bản sao được tạo, chúng tôi sẵn sàng trao đổi.

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

Tại thời điểm này, chúng tôi không có nhà, bởi vì swap khô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ũ đượ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.)

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

Và đó là thành ngữ sao chép và trao đổi.

Còn 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 của Ba bây giờ là Quy tắc bốn (và một nửa). Tại sao? Bởi vì không chỉ chúng ta cần có khả năng sao chép xây dựng tài nguyên của mình, chúng ta cũng cần phải xây dựng nó.

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

class dumb_array
{
public:
    // ...

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

    // ...
};

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

Vì vậy, những gì chúng tôi đã làm là đơn giản: khởi tạo thông qua hàm tạo mặc định (một tính năng C ++ 11), sau đó trao đổi với other; chúng ta biết một trường hợp đượ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 other sẽ có thể thực hiện 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 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 đáng tiếc.)

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

Đó là thay đổi duy nhất chúng ta cần phải thực hiện cho lớp học của chúng ta, vậy tại sao nó lại hoạt động? Hãy nhớ quyết định quan trọng mà chúng tôi đưa ra để làm cho thông số trở thành một 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 di chuyển xây dựng. Hoàn hảo. Trong cùng một cách C ++ 03 cho phép chúng ta tái sử dụng chức năng sao chép-constructor của chúng tôi bằng cách lấy đối số theo giá trị, C ++ 11 sẽ tự động chọn di chuyển-constructor khi thích hợp là tốt. (Và, tất nhiên, như đã đề cập trong bài viết được liên kết trước đó, việc sao chép / di chuyển của giá trị có thể chỉ đơn giản là được elided hoàn toàn.)

Và do đó kết thúc thành ngữ sao chép và trao đổi.


Chú thích

* Tại sao chúng ta thiết lập mArray để null? Bởi vì nếu bất kỳ mã nào khác trong toán tử ném, hàm hủy của dumb_array có 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ố xóa bộ nhớ đã bị xóa! Chúng ta tránh điều này bằng cách thiết lập nó thành null, vì việc xóa null là không hoạt động.

† Có những tuyên bố khác mà chúng ta nên chuyên std::swap cho loại của chúng tôi, cung cấp một lớp học swap cùng một chức năng miễn phí swap, vv Nhưng điều này là tất cả không cần thiết: bất kỳ việc sử dụng thích hợp nào swap sẽ được 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ẽ được 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ể hoán đổ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.


1841
2017-07-19 08:43



@GMan: Tôi cho rằng một lớp quản lý nhiều tài nguyên cùng một lúc sẽ bị thất bại (ngoại lệ an toàn trở thành ác mộng) và tôi sẽ khuyên bạn nên hoặc là một lớp quản lý MỘT tài nguyên HOẶC nó có chức năng kinh doanh và quản lý sử dụng. - Matthieu M.
@FrEEzE: "Đó là trình biên dịch cụ thể trong đó thứ tự danh sách được xử lý." Không phải vậy. Nó được xử lý theo thứ tự chúng xuất hiện trong định nghĩa lớp. Trình biên dịch không chấp nhận std::copy theo cách đó bị hỏng, tôi không mã hóa cho các trình biên dịch bị hỏng. Và tôi không chắc tôi hiểu nhận xét cuối cùng của bạn. - GManNickG
@ Freeze: Bên cạnh đó, điểm của câu trả lời này là nói về một thành ngữ C ++. Nếu bạn cần phải hack chương trình của bạn để làm việc với một trình biên dịch không tuân thủ, đó là tốt, nhưng không cố gắng hành động như đó là trách nhiệm của tôi hoặc đó là "thực hành xấu" không, xin vui lòng. - GManNickG
Tôi không hiểu tại sao phương thức hoán đổi được khai báo là bạn ở đây? - szx
@neuviemeporte: Bạn cần swap được tìm thấy 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 phải, như boost::swap và các trường hợp hoán đổi khác nhau. Swap là một vấn đề phức tạp trong C ++, và nói chung 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 hàm miễn phí (int không thể có một thành viên trao đổi, ví dụ). Xem câu hỏi của tôi cho một số nền. - GManNickG


Nhiệm vụ, trong trái tim của nó, là hai bước: xé nát trạng thái cũ của đối tượng và xây dựng trạng thái mới của nó như là một bản sao của một số trạng thái của đối tượng khác.

Về cơ bản, đó là những gì người hủy diệt và -bộ tạo bản sao do đó, ý tưởng đầu tiên là phân công tác phẩm cho họ. Tuy nhiên, vì sự hủy diệt không thể 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 đó làm phần phá hoại. Thành ngữ sao chép và hoán đổi là một cách để thực hiện điều đó: Đầu tiên nó gọi một hàm tạo bản sao của lớp để tạo 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ủy tạm thời phá hủy trạng thái cũ.
swap() được cho là không bao giờ thất bại, phần duy nhất có thể thất bại là bản sao-xây dựng. Điều đó được thực hiện trước tiên, và nếu nó không thành công, sẽ không có gì thay đổi trong đối tượng được nhắm mục tiêu.

Trong dạng tinh vi của nó, việc 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;
}

227
2017-07-19 08:55



Tôi nghĩ rằng đề cập đến pimpl là quan trọng như đề cập đến bản sao, trao đổi và tiêu hủy. Việc hoán đổi không phải là ngoại lệ một cách kỳ diệu. Đó là ngoại lệ an toàn bởi vì trao đổi con trỏ là ngoại lệ an toàn. Bạn không có để sử dụng một pimpl, nhưng nếu không thì bạn phải đảm bảo rằng mỗi lần trao đổ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 các thành viên này có thể thay đổi và nó là tầm thường khi họ bị giấu đằng sau một pimpl. Và sau đó, sau đó đến chi phí của pimpl. Điều này dẫn chúng ta đến kết luận rằng thường là ngoại lệ-an toàn mang lại một chi phí trong hoạt động. - wilhelmtell
std::swap(this_string, that) không cung cấp bảo đảm không ném. Nó cung cấp sự an toàn ngoại lệ mạnh mẽ, nhưng không đảm bảo an toàn. - wilhelmtell
@wilhelmtell: Trong C ++ 03, không có đề cập đến 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::swap Là noexcept và không được ném ngoại lệ. - James McNellis
@sbi @ JamesMcNellis ok, nhưng điểm vẫn là viết tắt: nếu bạn có các thành viên của loại lớp bạn phải chắc chắn trao đổi chúng là một không ném. Nếu bạn có một thành viên duy nhất đó là một con trỏ thì đó là tầm thường. Nếu không nó không phải là. - wilhelmtell
@ wilhelmtell: Tôi nghĩ đó là điểm trao đổ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


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

Thành ngữ sao chép và hoán đổi là gì?

Cách triển khai toán tử gán theo chức năng hoán đổi:

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

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

  • phần dễ bị lỗi nhất của việc gán cho một đối tượng là đảm bảo mọi tài nguyên mà các nhu cầu của tiểu bang mới được thu nhận (ví dụ: bộ nhớ, các bộ mô tả)

  • việc mua lại đó có thể được thử trước 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 thực hiện, đó là lý do tại sao rhs được chấp nhận theo giá trị (tức là sao chép) thay vì bằng cách tham chiếu

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

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

  • Khi bạn muốn gán cho đối tượng không bị ảnh hưởng bởi một nhiệm vụ mà ném một ngoại lệ, giả sử bạn có hoặc có thể viết một swap với bảo đảm ngoại lệ mạnh mẽ và lý tưởng nhất 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 về phương thức khởi tạo bản sao (đơn giản), swap và hàm hủy.

    • Tự gán được thực hiện như là một bản sao và trao đổi tránh các trường hợp cạnh không được 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 ra bởi việc 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. ⁂

swap ném: thường có thể trao đổi đáng tin cậy các thành viên dữ liệu mà các đối tượng theo dõi bởi con trỏ, nhưng các thành viên dữ liệu không phải con trỏ không có trao đổi miễn phí hoặc trao đổi phải được thực hiện như X tmp = lhs; lhs = rhs; rhs = tmp; và sao chép-xây dựng hoặc chuyển nhượng có thể ném, vẫn có khả năng thất bại để lại một số thành viên dữ liệu trao đổ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 bình luận về câu trả lời khác:

@wilhelmtell: Trong C ++ 03, không có đề cập đến 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 :: swap là noexcept và không được ném ngoại lệ. - James McNellis ngày 22 tháng 12 năm 2010 lúc 15:24


‡ việc thực thi toán tử gán có vẻ sane khi gán từ một đối tượng riêng biệt có thể dễ dàng thất bại khi tự gán. Mặc dù có vẻ như không thể tưởng tượng được rằng mã khách hàng thậm chí sẽ cố gắng tự gán, nó có thể xảy ra tương đối dễ dàng trong quá trình hoạt động của bản thân trên các thùng chứa, với x = f(x); mã nơi f là (có lẽ chỉ dành cho một số #ifdef chi nhánh) một ala vĩ mô #define f(x) x hoặc một hàm trả về một tham chiếu đến x, hoặc thậm chí (có thể không hiệu quả nhưng súc tích) 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_);
    }
    ...
};

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


⁂ Thành ngữ sao chép và hoán đổi có thể giới thiệu sự thiếu hiệu quả hoặc hạn chế do việc sử dụng thêm tạm thời (khi tham số của toán tử được 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 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ủ với rhs (có thể gửi một mã "reset" nếu có ích), trong khi cách tiếp cận copy-and-swap sẽ gọi hàm tạo bản sao có khả năng được viết để mở một kết nối socket riêng biệt, sau đó đóng kết nối ban đầu. Điều này 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 đổi trong quá trình đơn giản, nó có thể chạy afoul giới hạn máy khách hoặc máy chủ trên tài nguyên ổ cắm hoặc kết nối. (Tất nhiên lớp này có giao diện khá kinh khủng, nhưng đó là một vấn đề khác ;-P).


32
2018-03-06 14:51



Điều đó nói rằng, một kết nối socket 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ò / hiệu chỉnh phần cứng, tạo một nhóm các chủ đề hoặc số ngẫu nhiên, một số nhiệm vụ mật mã, cache, quét hệ thống tệp, cơ sở dữ liệu kết nối v.v. - Tony Delroy
Có thêm một con (khổng lồ). Theo thông số kỹ thuật hiện tại kỹ thuật đối tượng sẽ không có nhà điều hành 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ó move-ctor được tạo tự động! Nguồn: youtu.be/mYrbivnruYw?t=43m14s - user362515
Vấn đề chính với toán tử gán bản sao của Client là nhiệm vụ đó không bị cấm. - sbi


Câu trả lời này giống như một bổ sung và 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ể trình biên dịch khác) có một lỗi đó là 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 swap chức năng 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ẽ hét lên với bạn khi bạn gọi swap chức năng:

enter image description here

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


Một cách xung quanh điều này là không sử dụng friend từ khóa và xác định lại swap chứ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 swap và vượt qua other, do đó làm cho trình biên dịch hài lòng:

enter image description here


Sau khi tất cả, bạn không nhu cầu sử dụng một friend để hoán đổi 2 đối tượng. Nó tạo ra nhiều ý nghĩa để tạo ra swap một hàm thành viên có một hàm other đối tượng làm tham số.

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


19
2017-09-04 04:50



Bạn có thể chia sẻ ví dụ của bạn sao chép lỗi? - GManNickG
@GManNickG dropbox.com/s/o1mitwcpxmawcot/example.cpp  dropbox.com/s/jrjrn5dh1zez5vy/Untitled.jpg. Đây là một phiên bản đơn giản. Một lỗi dường như xảy ra mỗi lần friend chức năng được gọi với *this tham số - Oleksiy
@GManNickG nó sẽ không phù hợp trong một bình luận với tất cả các hình ảnh và ví dụ mã. Và nó là ok nếu mọi người downvote, tôi chắc chắn rằng có ai đó ra có những người nhận được cùng một lỗi; thông tin trong bài đăng này có thể chỉ là những gì họ cần. - Oleksiy
lưu ý rằng đây chỉ là một lỗi trong mã đánh dấu IDE (IntelliSense) ... Nó sẽ biên dịch tốt mà không có cảnh báo / lỗi. - Amro
Vui lòng báo cáo lỗi VS ở đây nếu bạn chưa làm như vậy (và nếu nó chưa được sửa) connect.microsoft.com/VisualStudio - Matt


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

Để cụ thể, chúng ta hãy xem xét một container std::vector<T, A>, Ở đâu A là một số loại cấp phát trạng thái và chúng tôi 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 fs và fm là cho a trạng thái b 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ư. Cùng viết nào AT = std::allocator_traits<A>.

  • Nếu AT::propagate_on_container_move_assignment Là std::true_type, sau đó fm chỉ định lại người cấp phát a với giá trị của b.get_allocator(), nếu không thì không, và a tiếp tục sử dụng phân bổ ban đầu của nó. Trong trường hợp đó, các phần tử dữ liệu cần phải được hoán đổi riêng lẻ, vì việc lưu trữ a và b không tương thích.

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

  • Nếu AT::propagate_on_container_swap Là std::false_type, sau đó chúng tôi cần kiểm tra động.

    • Nếu a.get_allocator() == b.get_allocator(), sau đó hai vùng chứa sử dụng bộ nhớ tương thích và trao đổi số tiền thu được 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 (cf. [container.requirements.general / 8].

Kết quả là trao đổi đã trở thành một hoạt động không tầm thường trong C ++ 11 ngay khi vùng chứa của bạn bắt đầu hỗ trợ các trình phân bổ stateful. Đó là một "trường hợp sử dụng nâng cao", nhưng không hoàn toàn khó, vì việc 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.


10
2018-06-24 08:16