2011-06-20 16 views
29

Gần đây, manyquestionspop up về cách cung cấp chức năng swap của riêng bạn. Với C++ 11, std::swap sẽ sử dụng std::move và di chuyển ngữ nghĩa để hoán đổi các giá trị đã cho càng nhanh càng tốt. Điều này, tất nhiên, chỉ hoạt động nếu bạn cung cấp một hàm tạo di chuyển và một toán tử gán di chuyển (hoặc một toán tử sử dụng giá trị theo giá trị).Di chuyển ngữ nghĩa == chức năng hoán đổi tùy chỉnh lỗi thời?

Bây giờ, với điều đó, bạn có cần viết các hàm swap của riêng mình trong C++ 11 không? Tôi chỉ có thể nghĩ về các loại không thể di chuyển, nhưng sau đó một lần nữa, tùy chỉnh swap s thường làm việc thông qua một số loại "trao đổi con trỏ" (aka di chuyển). Có thể với các biến tham chiếu nhất định? Hm ...

Trả lời

19

Đó là vấn đề phán xét. Tôi thường sẽ để std::swap thực hiện công việc cho mã tạo mẫu, nhưng đối với mã phát hành, hãy viết một trao đổi tùy chỉnh. Tôi thường có thể viết một hoán đổi tùy chỉnh nhanh gấp đôi 1 lần xây dựng + 2 bài tập di chuyển + 1 sự hủy diệt resourceless. Tuy nhiên người ta có thể chờ đợi cho đến khi std::swap thực sự chứng minh là một vấn đề hiệu suất trước khi đi đến những bận tâm.

Cập nhật cho Alf P. Steinbach:

20.2.2 [utility.swap] xác định rằng std::swap(T&, T&)noexcept tương đương với:

template <class T> 
void 
swap(T& a, T& b) noexcept 
       (
        is_nothrow_move_constructible<T>::value && 
        is_nothrow_move_assignable<T>::value 
       ); 

Tức là nếu hoạt động di chuyển trên Tnoexcept, thì std::swap trên Tnoexcept.

Lưu ý rằng thông số này không yêu cầu di chuyển thành viên. Nó chỉ yêu cầu xây dựng và chuyển nhượng từ các giá trị tồn tại, và nếu nó là noexcept thì swap sẽ là noexcept. Ví dụ:

class A 
{ 
public: 
    A(const A&) noexcept; 
    A& operator=(const A&) noexcept; 
}; 

std::swap<A> không được chấp nhận, ngay cả khi không có thành viên di chuyển.

+0

Cảm ơn đã cập nhật, Howard. Tôi đoán rằng 'A (const A &) noexcept; 'là một lỗi đánh máy, hoặc có lẽ SO nuốt và không? I E. rằng bạn có nghĩa là 'A (A &&) noexcept; '(và ditto cho opperator gán)? Chúc mừng, –

+3

@Alf: Không; anh ta đang đưa ra một ví dụ trong đó 'std :: swap' sẽ là' noexcept (true) 'ngay cả khi không có việc xây dựng/chuyển nhượng thực tế. –

+0

@ Dennis: Tôi nghĩ rằng tôi cần giải thích thêm sau đó, bởi vì với tôi trông giống như một nhà xây dựng bản sao bình thường (không di chuyển), và một toán tử gán bản sao bình thường, và tôi hiểu nó không liên quan gì đến 'is_nothrow_move_constructible' hoặc' is_nothrow_move_assignable'? –

1

Có thể có một số loại có thể hoán đổi nhưng không được di chuyển. Tôi không biết bất kỳ loại không di chuyển nào, vì vậy tôi không có bất kỳ ví dụ nào.

+1

std :: mảng có thể sao chép được nhưng không có hàm khởi tạo, nếu đó là ý của bạn. –

+0

Điều không thể di chuyển: 'CRITICAL_SECTION' từ API Win32 trông giống như một. Đó là một loại mờ, nó phải được khởi tạo, nó không thể được sao chép, và nó có một con trỏ vào chính nó bên trong. Movable wrapper xung quanh một sẽ phải có một con trỏ để quản lý nó. –

0

Theo quy ước, tùy chỉnh swap đảm bảo không đảm bảo. Tôi không biết về std::swap. Ấn tượng của tôi về công việc của ủy ban về điều đó là tất cả chính trị, vì vậy tôi sẽ không ngạc nhiên nếu họ ở đâu đó đã xác định duckbug hoặc các trò chơi từ chính trị tương tự. Vì vậy, tôi sẽ không dựa vào bất kỳ câu trả lời nào ở đây trừ khi nó cung cấp một cú đánh chi tiết bằng cách trích dẫn từ tiêu chuẩn C++ 0x, xuống chi tiết nhỏ nhất (để đảm bảo không có bug).

0

Chắc chắn, bạn có thể thực hiện hoán đổi như

template <class T> 
void swap(T& x, T& y) 
{ 
    T temp = std::move(x); 
    x = std::move(y); 
    y = std::move(temp); 
} 

Nhưng chúng ta có thể có lớp riêng của chúng tôi, nói A, mà chúng tôi có thể trao đổi một cách nhanh chóng hơn.

void swap(A& x, A& y) 
{ 
    using std::swap; 
    swap(x.ptr, y.ptr); 
} 

Mà, thay vì phải chạy một hàm tạo và hủy, chỉ cần hoán đổi các con trỏ (có thể được triển khai tốt như XCHG hoặc một cái gì đó tương tự).Tất nhiên, trình biên dịch có thể tối ưu hóa các cuộc gọi constructor/destructor trong ví dụ đầu tiên, nhưng nếu chúng có các tác dụng phụ (nghĩa là các cuộc gọi đến mới/xóa), nó có thể không đủ thông minh để tối ưu hóa chúng đi.

+0

Nếu nhà điều hành xây dựng/chuyển nhượng của bạn có tác dụng phụ không được phản ánh trong hoán đổi tùy chỉnh của bạn, tôi nghi ngờ bạn đã thực hiện chúng sai. –

0

Hãy xem xét các lớp sau đây chứa một nguồn tài nguyên bộ nhớ phân bổ (vì đơn giản, đại diện bởi một số nguyên duy nhất):

class X { 
    int* i_; 
public: 
    X(int i) : i_(new int(i)) { } 
    X(X&& rhs) noexcept : i_(rhs.i_) { rhs.i_ = nullptr; } 
// X& operator=(X&& rhs) noexcept { delete i_; i_ = rhs.i_; 
//         rhs.i_ = nullptr; return *this; } 
    X& operator=(X rhs) noexcept { swap(rhs); return *this; } 
    ~X() { delete i_; } 
    void swap(X& rhs) noexcept { std::swap(i_, rhs.i_); } 
}; 

void swap(X& lhs, X& rhs) { lhs.swap(rhs); } 

Sau đó std::swap kết quả trong xóa con trỏ null 3 lần (cả cho di chuyển toán tử gánhợp nhất toán tử gán trường hợp). Trình biên dịch có thể có vấn đề để tối ưu hóa như vậy delete, xem https://godbolt.org/g/E84ud4.

Tùy chỉnh swap không gọi bất kỳ delete và do đó có thể hiệu quả hơn. Tôi đoán đây là lý do tại sao std::unique_ptr cung cấp chuyên môn tùy chỉnh std::swap.

CẬP NHẬT

Dường như Intel và Clang trình biên dịch có thể tối ưu hóa ra xóa con trỏ null, tuy nhiên GCC thì không. Xem Why GCC doesn't optimize out deletion of null pointers in C++? để biết chi tiết.

CẬP NHẬT

Dường như với GCC, chúng ta có thể ngăn chặn gọi delete điều hành bằng cách viết lại X như sau:

// X& operator=(X&& rhs) noexcept { if (i_) delete i_; i_ = rhs.i_; 
//         rhs.i_ = nullptr; return *this; } 
    ~X() { if (i_) delete i_; }