2016-01-15 22 views
6

Thông thường, đưa ra một số loại T, để thực hiện sao chép và di chuyển nhượng, người ta cần hai chức năngThực hiện sao chép và phân công di chuyển với một chức năng duy nhất

T& operator=(T&&) { ... } 
T& operator=(const T&) { ... } 

Gần đây, tôi nhận ra rằng một trong những đơn chỉ là đủ

T& operator=(T v) { 
    swap(v); 
    return *this; 
} 

Phiên bản này tận dụng lợi thế của trình tạo bản sao/di chuyển. Cho dù nhiệm vụ được sao chép hay di chuyển phụ thuộc vào cách v được tạo. Phiên bản này thậm chí có thể nhanh hơn phiên bản đầu tiên, vì giá trị truyền qua cho phép nhiều không gian hơn cho tối ưu hóa trình biên dịch [1]. Vậy, lợi thế của phiên bản đầu tiên so với phiên bản thứ hai mà ngay cả thư viện chuẩn cũng sử dụng nó là gì?

[1] Tôi đoán điều này giải thích tại sao thẻ và đối tượng hàm được chuyển theo giá trị trong thư viện chuẩn.

+1

Điều này đã bị đánh đến chết hàng triệu lần - ví dụ: http://stackoverflow.com/a/24018053/2756719 –

+0

@ T.C. Tôi bối rối. Bạn có thể xây dựng thêm về nó? Có thể đăng câu trả lời? – Lingxi

+0

Tôi nghĩ nó được đề cập khá tốt trong [thảo luận này về thành ngữ sao chép và hoán đổi] (http://stackoverflow.com/a/3279550/27678) – AndyG

Trả lời

4

std::swap được triển khai bằng cách thực hiện xây dựng di chuyển, sau đó là hai thao tác gán di chuyển. Vì vậy, trừ khi bạn thực hiện hoạt động swap của riêng bạn thay thế cho thao tác được cung cấp tiêu chuẩn, mã của bạn như được trình bày là một vòng lặp vô hạn.

Vì vậy, bạn có thể triển khai phương pháp 2 operator= hoặc triển khai một phương thức operator= và một phương pháp swap. Xét về số lượng hàm được gọi, nó hoàn toàn giống hệt nhau.

Ngoài ra, phiên bản operator= của bạn đôi khi kém hiệu quả hơn. Trừ khi việc xây dựng các tham số được elided, xây dựng sẽ được thực hiện thông qua một bản sao/di chuyển từ giá trị của người gọi. Sau đây là 1 công trình xây dựng và 2 bài tập chuyển động (hoặc bất kỳ bài tập nào của bạn swap). Trong khi đó, quá tải operator= có thể hoạt động trực tiếp với tham chiếu được đưa ra.

Và điều này giả định rằng bạn không thể viết phiên bản tối ưu của bài tập thực tế. Xem xét sao chép chỉ định một vector cho người khác. Nếu đích đến vector có đủ bộ nhớ để giữ kích thước của vectơ nguồn ... bạn không cần phân bổ. Trong khi nếu bạn sao chép xây dựng, bạn phải phân bổ bộ nhớ. Chỉ để sau đó giải phóng dung lượng mà bạn có thể đã sử dụng.

Ngay cả trong trường hợp tốt nhất, bản sao/di chuyển của bạn & trao đổi sẽ không còn nhiều hơn hiệu quả hơn sử dụng giá trị. Sau khi tất cả, bạn sẽ có một tham chiếu đến tham số; std::swap không hoạt động trên các giá trị. Vì vậy, bất cứ điều gì hiệu quả bạn nghĩ sẽ bị mất bằng cách sử dụng tài liệu tham khảo sẽ bị mất một trong hai cách.

Những lập luận về nguyên tắc có lợi cho sao chép/di chuyển & hoán đổi là:

  1. Giảm sự trùng lặp mã. Điều này chỉ thuận lợi nếu việc bạn thực hiện các thao tác gán bản sao/di chuyển sẽ ít nhiều giống với việc xây dựng sao chép/di chuyển. Điều này không đúng với nhiều loại; như đã nêu trước đây, vector có thể tối ưu hóa chính nó một chút bằng cách sử dụng bộ nhớ hiện có nếu có thể. Thật vậy, nhiều container có thể (đặc biệt là các thùng chứa chuỗi).

  2. Cung cấp bảo đảm ngoại lệ mạnh mẽ với nỗ lực tối thiểu. Giả sử constructor di chuyển của bạn là noexcept.

Cá nhân, tôi muốn tránh hoàn toàn tình huống này. Tôi thích cho phép trình biên dịch tạo ra tất cả các chức năng thành viên đặc biệt của tôi. Và nếu một loại hoàn toàn cần tôi viết những hàm thành viên đặc biệt này, thì loại này sẽ càng nhỏ càng tốt. Đó là, nó là mục đích duy nhất sẽ được quản lý bất cứ điều gì nó là yêu cầu hoạt động này.

Bằng cách đó, tôi không phải lo lắng về điều đó. Tỷ lệ các lớp học của sư tử không cần bất kỳ chức năng nào trong số này để được xác định rõ ràng.

+0

Tôi hiểu ý tưởng. Thành ngữ này không phải là OK nói chung, nhưng nên được tốt trong trường hợp cụ thể của việc thực hiện 'intrusive_ptr'. – Lingxi

1

tôi nhận ra rằng đây có một câu trả lời được chấp nhận, nhưng tôi cảm thấy tôi phải nhảy vào Có hai vấn đề khác nhau ở đây:.

  1. toán tử gán Unified. Điều này có nghĩa là bạn có một toán tử gán có giá trị, thay vì hai quá tải mất bởi const & và & &.
  2. Nhà điều hành gán bản sao và hoán đổi (CAS) sao chép.

Nếu bạn đang làm 1, bạn thường làm 2. Vì bạn cần triển khai logic gán chuyển đổi/di chuyển ở đâu đó và bạn không thể thực hiện nó trong toán tử gán thống nhất, vì vậy bạn thường thực hiện hoán đổi và gọi nó. Nhưng thực hiện 2 không có nghĩa là bạn phải làm 1:

T& operator=(T&&) { /* actually implemented */ } 
T& operator=(const T& t) { T t2(t); swap(*this, t2); return *this;} 

Trong trường hợp này, chúng tôi thực hiện chuyển đổi mặc định, nhưng sử dụng chuyển đổi mặc định và hai bài tập di chuyển).

Động lực để thực hiện CAS là nhận được sự bảo đảm ngoại lệ mạnh mẽ, mặc dù là T.C. chỉ ra trong các ý kiến, bạn có thể làm:

T& operator=(const T& t) { *this = T(t); return *this;} 

đó là có khả năng hơn efficient.In hầu hết các mã mà tôi viết, hiệu suất là một vấn đề và tôi đã không bao giờ cần bảo lãnh ngoại lệ mạnh mẽ, vì vậy tôi sẽ gần như không bao giờ làm điều này, vì vậy nó chỉ phụ thuộc vào trường hợp sử dụng của bạn.

Bạn nên làm 1 không bao giờ. Tốt hơn là chúng là các hàm riêng biệt để việc gán di chuyển có thể được đánh dấu là không nhận diện.

+0

(1) cũng có thể được đánh dấu noexcept, bởi vì xây dựng các tham số xảy ra tại call-site. Đó là phản trực giác đối với hầu hết các lập trình viên. Và tất nhiên, khi gọi với một giá trị được sao chép, toàn bộ cấu trúc (= bao gồm cả "thiết lập" ở phía máy khách) sẽ * không * không được nhận diện. OTOH, nếu yêu cầu mã ngoại lệ, bạn phải cẩn thận, vì vậy điều này có thể không * quan trọng *. –

+0

@PaulGroke Bạn có thể cung cấp bất kỳ tham chiếu nào về điều này không? – Lingxi

+1

@PaulGroke Nó có thể được đánh dấu noexcept, nhưng câu hỏi là thực sự cho dù nó nên được. Tôi không nghĩ rằng nó chỉ đơn thuần là phản trực giác, tôi nghĩ đó là thực tế tồi tệ. Nó tóm tắt: nếu một cấu trúc chung ràng buộc ràng buộc một hàm functor không được nhận dạng, và bạn đã đánh dấu một hàm giá trị theo không có ngoại trừ và sử dụng nó, và xây dựng hành vi sai trái, ai có lỗi? Cá nhân tôi cảm thấy mạnh mẽ rằng người đánh dấu nó không nhận thức là có lỗi. Xem câu hỏi của tôi: http://stackoverflow.com/questions/34804922/exception-guarantees-and-pass-by-value –

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