2011-02-02 33 views
14

Tôi không thể ngủ đêm qua và bắt đầu nghĩ về std::swap. Đây là phiên bản C++ 98 quen thuộc:Hoán đổi nhanh hơn, dễ sử dụng và ngoại lệ an toàn

template <typename T> 
void swap(T& a, T& b) 
{ 
    T c(a); 
    a = b; 
    b = c; 
} 

Nếu người dùng xác định loại Foo sử dụng tài nguyên bên ngoài, điều này không hiệu quả. Thành ngữ phổ biến là cung cấp phương thức void Foo::swap(Foo& other) và chuyên môn hóa std::swap<Foo>. Lưu ý rằng điều này không hoạt động với các mẫu lớp vì bạn không thể chuyên biệt hóa một phần mẫu chức năng và quá tải tên trong không gian tên std là bất hợp pháp. Giải pháp là viết một hàm mẫu trong không gian tên của chính nó và dựa vào tra cứu phụ thuộc đối số để tìm nó. Điều này phụ thuộc nhiều vào khách hàng để thực hiện theo "thành ngữ using std::swap" thay vì gọi trực tiếp std::swap. Rất giòn.

Trong C++ 0x, nếu Foo có một constructor di chuyển người dùng định nghĩa và một toán tử gán di chuyển, cung cấp một phương pháp swap tùy chỉnh và một std::swap<Foo> chuyên môn có ít hoặc không có hiệu quả lợi ích, vì phiên bản C++ 0x của std::swap sử dụng các di chuyển hiệu quả thay vì bản sao:

#include <utility> 

template <typename T> 
void swap(T& a, T& b) 
{ 
    T c(std::move(a)); 
    a = std::move(b); 
    b = std::move(c); 
} 

Không cần phải chịu khó với swap nữa đã mất rất nhiều gánh nặng từ lập trình viên. Trình biên dịch hiện tại không tạo ra các nhà xây dựng di chuyển và di chuyển các toán tử gán tự động, nhưng theo như tôi biết, điều này sẽ thay đổi. Vấn đề duy nhất còn lại sau đó là ngoại lệ-an toàn, bởi vì nói chung, hoạt động di chuyển được phép ném, và điều này mở ra một toàn bộ có thể của sâu. Câu hỏi "Chính xác trạng thái của đối tượng chuyển từ là gì?" làm phức tạp thêm.

Sau đó, tôi đã suy nghĩ, những gì chính xác là ngữ nghĩa của std::swap trong C++ 0x nếu mọi thứ diễn ra tốt đẹp? Trạng thái của các đối tượng trước và sau khi hoán đổi là gì? Thông thường, trao đổi thông qua các hoạt động di chuyển không chạm vào tài nguyên bên ngoài, chỉ có các đại diện "phẳng" đối tượng.

Vậy tại sao không chỉ đơn giản viết mẫu swap thực hiện chính xác điều đó: trao đổi đại diện đối tượng?

#include <cstring> 

template <typename T> 
void swap(T& a, T& b) 
{ 
    unsigned char c[sizeof(T)]; 

    memcpy(c, &a, sizeof(T)); 
    memcpy(&a, &b, sizeof(T)); 
    memcpy(&b, c, sizeof(T)); 
} 

Tính năng này hiệu quả như: nó chỉ đơn giản là thổi qua bộ nhớ thô. Nó không yêu cầu bất kỳ sự can thiệp nào từ người dùng: không có phương thức hoán đổi đặc biệt hoặc các hoạt động di chuyển phải được xác định. Điều này có nghĩa rằng nó thậm chí hoạt động trong C++ 98 (mà không có tham chiếu rvalue, tâm trí bạn). Nhưng thậm chí quan trọng hơn, bây giờ chúng ta có thể quên đi các vấn đề ngoại lệ-an toàn, bởi vì memcpy không bao giờ ném.

Tôi có thể thấy hai vấn đề tiềm ẩn với cách tiếp cận này:

Đầu tiên, không phải tất cả các đối tượng đều được đổi chỗ. Nếu một nhà thiết kế lớp ẩn bản sao xây dựng hoặc toán tử gán bản sao, cố gắng hoán đổi các đối tượng của lớp sẽ không thành công tại thời gian biên dịch. Chúng tôi chỉ có thể giới thiệu một số mã chết để kiểm tra xem việc sao chép và chuyển nhượng có hợp pháp với loại đó không:

template <typename T> 
void swap(T& a, T& b) 
{ 
    if (false) // dead code, never executed 
    { 
     T c(a); // copy-constructible? 
     a = b; // assignable? 
    } 

    unsigned char c[sizeof(T)]; 

    std::memcpy(c, &a, sizeof(T)); 
    std::memcpy(&a, &b, sizeof(T)); 
    std::memcpy(&b, c, sizeof(T)); 
} 

Bất kỳ trình biên dịch nào cũng có thể loại bỏ mã chết. (Có lẽ có nhiều cách tốt hơn để kiểm tra "sự phù hợp hoán đổi", nhưng đó không phải là vấn đề. Điều quan trọng là có thể).

Thứ hai, một số loại có thể thực hiện các hành động "bất thường" trong trình tạo bản sao và sao chép toán tử gán. Ví dụ, họ có thể thông báo cho các nhà quan sát về sự thay đổi của họ. Tôi cho rằng đây là một vấn đề nhỏ, bởi vì các loại đối tượng như vậy có lẽ không nên cung cấp các hoạt động sao chép ngay từ đầu.

Vui lòng cho tôi biết suy nghĩ của bạn về cách tiếp cận này để trao đổi. Nó có hoạt động trong thực tế không? Bạn sẽ sử dụng nó? Bạn có thể xác định các loại thư viện mà điều này sẽ phá vỡ không? Bạn có thấy các vấn đề khác không? Bàn luận!

+0

Hầu hết các trường hợp sử dụng hiện tại cho 'std :: swap' sẽ có các giải pháp tốt hơn bằng cách sử dụng ngữ nghĩa di chuyển. – aschepler

+0

Có di chuyển ngữ nghĩa và di chuyển các nhà thầu. Xem này, http://stackoverflow.com/questions/4820643/understanding-stdswap-what-is-the-purpose-of-tr1-remove-reference –

+2

xem xét 'hoán đổi (* polymorphicPtr1, * polymorphicPtr2)' ... của bạn hoán đổi chức năng sẽ trao đổi vtable của cả hai đối tượng là tốt ... mà sẽ gây ra tàn phá nếu ai đó gọi một chức năng ảo ofter cuộc gọi để trao đổi. – smerlin

Trả lời

20

Vậy tại sao không chỉ đơn giản viết mẫu swap thực hiện chính xác điều đó: hoán đổi đại diện đối tượng *?

Có nhiều cách trong đó một đối tượng, một khi được xây dựng, có thể phá vỡ khi bạn sao chép các byte nó cư trú trong. Trong thực tế, người ta có thể đưa ra một số dường như vô tận các trường hợp nơi này sẽ không làm điều đúng - mặc dù trong thực tế nó có thể làm việc trong 98% của tất cả các trường hợp.

Đó là bởi vì vấn đề cơ bản để tất cả điều này là, ngoại trừ trong C, C++ chúng ta phải không đối xử với đối tượng như thể chúng là chỉ byte thô. Đó là lý do tại sao chúng tôi đã xây dựng và phá hủy, sau khi tất cả: để biến lưu trữ thô thành các đối tượng và các đối tượng trở lại vào lưu trữ thô. Khi một hàm tạo đã chạy, bộ nhớ nơi đối tượng cư trú không chỉ là lưu trữ thô. Nếu bạn đối xử với nó như thể nó không được, bạn sẽ phá vỡ một số loại.

Tuy nhiên, về cơ bản, các đối tượng di chuyển nên không thực hiện điều đó tồi tệ hơn nhiều so với ý tưởng của bạn, bởi vì, một khi bạn bắt đầu một cách đệ quy nội tuyến các cuộc gọi đến std::move(), bạn thường cuối cùng đến nơi built-in được chuyển. (Và nếu có nhiều thứ để di chuyển cho một số loại, bạn không nên khéo léo hơn với bộ nhớ của chính mình!) Cấp, di chuyển bộ nhớ en khối thường nhanh hơn các bước đơn (và không chắc rằng trình biên dịch có thể phát hiện ra rằng nó có thể tối ưu hóa các chuyển động riêng lẻ đến một toàn bộ bao gồm std::memcpy()), nhưng đó là mức giá mà chúng tôi trả cho các đối tượng mờ đục trừu tượng cung cấp cho chúng tôi.Và nó khá nhỏ, đặc biệt là khi bạn so sánh nó với việc sao chép chúng tôi thường làm.

Bạn có thể, tuy nhiên, có một tối ưu hóa sử dụng swap()std::memcpy() cho loại tổng hợp.

+0

s/praxis/practice/;-) Ngoài ra, nó sẽ là tốt đẹp để thêm một liên kết đến POD/FAQ tổng hợp. – fredoverflow

+1

@Fred, từ điển của tôi nói rằng "praxis" là một từ tiếng Anh hoàn toàn hợp lệ. Phải không? Tiếng Anh không phải là ngôn ngữ mẹ đẻ của tôi, nhưng tôi tò mò. –

+0

@Sergey: Google có 27.700.000 kết quả tìm kiếm cho "trong thực tế", nhưng chỉ 214.000 cho "trong praxis" ... – fredoverflow

7

Một số loại có thể được hoán đổi nhưng không thể sao chép được. Con trỏ thông minh độc đáo có lẽ là ví dụ tốt nhất. Kiểm tra khả năng sao chép và khả năng gán là sai.

Nếu T không phải là loại POD, việc sử dụng memcpy để sao chép/di chuyển là hành vi không xác định.


Các thành ngữ phổ biến là để cung cấp một phương pháp void Foo :: swap (Foo & khác) và một chuyên môn của std :: swap < Foo>. Lưu ý rằng thao tác này không hoạt động với các mẫu lớp học,…

Thành ngữ tốt hơn là trao đổi không thành viên và yêu cầu người dùng gọi trao đổi không đủ tiêu chuẩn, do đó ADL áp dụng. Điều này cũng hoạt động với các mẫu:

struct NonTemplate {}; 
void swap(NonTemplate&, NonTemplate&); 

template<class T> 
struct Template { 
    friend void swap(Template &a, Template &b) { 
    using std::swap; 
#define S(N) swap(a.N, b.N); 
    S(each) 
    S(data) 
    S(member) 
#undef S 
    } 
}; 

Khóa là khai báo sử dụng cho std :: swap làm dự phòng. Tình bạn cho việc trao đổi mẫu là tốt đẹp để đơn giản hóa định nghĩa; trao đổi cho NonTemplate cũng có thể là một người bạn, nhưng đó là một chi tiết thực hiện.

20

Điều này sẽ phá vỡ các phiên bản lớp có con trỏ tới các thành viên của riêng chúng. Ví dụ:

class SomeClassWithBuffer { 
    private: 
    enum { 
     BUFSIZE = 4096, 
    }; 
    char buffer[BUFSIZE]; 
    char *currentPos; // meant to point to the current position in the buffer 
    public: 
    SomeClassWithBuffer(); 
    SomeClassWithBuffer(const SomeClassWithBuffer &that); 
}; 

SomeClassWithBuffer::SomeClassWithBuffer(): 
    currentPos(buffer) 
{ 
} 

SomeClassWithBuffer::SomeClassWithBuffer(const SomeClassWithBuffer &that) 
{ 
    memcpy(buffer, that.buffer, BUFSIZE); 
    currentPos = buffer + (that.currentPos - that.buffer); 
} 

Bây giờ, nếu bạn chỉ làm memcpy(), wherePOS sẽ trỏ ở đâu? Đến vị trí cũ, rõ ràng. Điều này sẽ dẫn đến các lỗi rất buồn cười trong đó mỗi cá thể thực sự sử dụng bộ đệm của người khác.

+1

Thành thật mà nói, làm cho một đối tượng 'Reader' sao chép-xây dựng và gán được có vẻ như một lỗi thiết kế cho tôi. – fredoverflow

+1

@Fred, nó chỉ là một ví dụ trừu tượng. Tôi có lẽ nên đặt tên nó là "SomeClassWithBuffer" thay vào đó, nhưng điều đó không liên quan. –

+0

@Matthieu, vấn đề đó đã được đề cập bởi OP, vì vậy tôi đã không đề cập đến điều đó. Có lẽ có nhiều vấn đề mà nó dường như lúc đầu cũng vậy. –

1

phiên bản swap của bạn sẽ gây ra sự tàn phá nếu ai đó sử dụng nó với các loại đa hình.

xem xét:

Base *b_ptr = new Base(); // Base and Derived contain definitions 
Base *d_ptr = new Derived(); // of a virtual function called vfunc() 
yourmemcpyswap(*b_ptr, *d_ptr); 
b_ptr->vfunc(); //now calls Derived::vfunc, while it should call Base::vfunc 
d_ptr->vfunc(); //now calls Base::vfunc while it should call Derived::vfunc 
//... 

điều này là sai, bởi vì bây giờ b chứa vtable loại Derived, vì vậy Derived::vfunc được gọi trên đối tượng mà isnt loại Derived.

Các bình thường std::swap chỉ giao dịch hoán đổi các thành viên dữ liệu Base, vì vậy đây là OK với std::swap

+0

Hoán đổi chỉ các thành viên dữ liệu của 'Base' mới có thể phá vỡ các bất biến của đối tượng' Derived'. Đó là một lý do tại sao nó không có ý nghĩa nhiều để có một nhà điều hành chuyển nhượng cho các đối tượng đa hình. Lưu ý rằng Bjarne Stroustrup coi đây là một tai nạn lịch sử mà toán tử gán được cung cấp cho mọi lớp do người dùng định nghĩa theo mặc định. – fredoverflow

6

tôi xét thấy đây là một vấn đề nhỏ, bởi vì loại như vậy của các đối tượng có lẽ nên không đã cung cấp hoạt động sao chép trong nơi đầu tiên.

Đó là, khá đơn giản, một tải sai. Các lớp thông báo cho các quan sát viên và các lớp không nên sao chép hoàn toàn không liên quan. Làm thế nào về shared_ptr? Nó rõ ràng nên được sao chép, nhưng nó cũng rõ ràng thông báo cho một người quan sát - số lượng tham chiếu. Bây giờ đúng là trong trường hợp này, số lượng tham chiếu là như nhau sau khi hoán đổi, nhưng điều đó chắc chắn không đúng đối với tất cả các loại và đó là đặc biệt là không đúng nếu đa luồng tham gia, nó không đúng trong trường hợp sao chép thường xuyên thay vì hoán đổi, v.v. Đây là đặc biệt là sai đối với các lớp học có thể được di chuyển hoặc hoán đổi nhưng không phải là được sao chép.

vì nói chung, di chuyển hoạt động được phép ném

Họ là chắc chắn nhất không. Đó là hầu như không thể đảm bảo an toàn ngoại lệ mạnh mẽ trong khá nhiều trường hợp liên quan đến di chuyển khi di chuyển có thể ném. Định nghĩa C++ 0x của thư viện chuẩn, từ bộ nhớ, cho biết rõ ràng bất kỳ loại nào có thể sử dụng trong bất kỳ vùng chứa tiêu chuẩn nào không được ném khi di chuyển.

Đây là hiệu quả như nó được

Đó cũng là sai. Bạn đang giả định rằng sự di chuyển của bất kỳ đối tượng nào hoàn toàn là các biến thành viên - nhưng nó có thể không phải là tất cả chúng. Tôi có thể có bộ nhớ cache dựa trên triển khai và tôi có thể quyết định rằng trong lớp của mình, tôi không nên di chuyển bộ nhớ cache này. Là một chi tiết thực hiện, tôi hoàn toàn không có quyền chuyển bất kỳ biến thành viên nào mà tôi cho là không cần thiết phải di chuyển.Bạn, tuy nhiên, muốn di chuyển tất cả chúng.

Bây giờ, đúng là mã mẫu của bạn phải hợp lệ cho nhiều lớp. Tuy nhiên, nó cực kỳ chắc chắn không hợp lệ đối với nhiều lớp hoàn toàn và hoàn toàn hợp pháp, và quan trọng hơn, nó sẽ biên dịch xuống hoạt động đó anyway nếu hoạt động có thể được giảm xuống. Điều này đang phá vỡ các lớp học hoàn hảo tốt cho hoàn toàn không có lợi ích.

+0

+1 để chỉ ra: "... Định nghĩa C++ 0x của thư viện Chuẩn, từ bộ nhớ, cho biết rõ ràng bất kỳ loại nào có thể sử dụng trong bất kỳ vùng chứa Chuẩn nào cũng không được ném khi di chuyển. ...." –

+0

và tôi sẽ thêm một +1 khác cho "... và quan trọng hơn, nó sẽ biên dịch xuống chiến dịch đó ..." –

Các vấn đề liên quan