Sao chép hàm tạo và = toán tử quá tải trong C ++: một hàm phổ biến có thể thực hiện được không?


87

Kể từ một phương thức tạo bản sao

MyClass(const MyClass&);

và một = toán tử quá tải

MyClass& operator = (const MyClass&);

có khá nhiều mã giống nhau, cùng một tham số và chỉ khác nhau về kết quả trả về, liệu có thể có một hàm chung cho cả hai để sử dụng không?


6
"... có khá nhiều mã giống nhau ..."? Hmm ... Chắc bạn đang làm gì đó sai. Cố gắng giảm thiểu nhu cầu sử dụng các chức năng do người dùng xác định cho việc này và để trình biên dịch thực hiện tất cả các công việc khó khăn. Điều này thường có nghĩa là đóng gói các tài nguyên trong đối tượng thành viên của riêng chúng. Bạn có thể cho chúng tôi xem một số mã. Có lẽ chúng tôi có một số gợi ý thiết kế tốt.
sellibitze

Câu trả lời:


121

Đúng. Có hai lựa chọn phổ biến. Một - điều thường không được khuyến khích - là gọi operator=từ hàm tạo bản sao một cách rõ ràng:

MyClass(const MyClass& other)
{
    operator=(other);
}

Tuy nhiên, cung cấp một sản phẩm tốt operator=là một thách thức khi phải đối mặt với tình trạng cũ và các vấn đề phát sinh từ việc tự phân công. Ngoài ra, tất cả các thành viên và cơ sở được khởi tạo mặc định trước ngay cả khi chúng được chỉ định từ other. Điều này thậm chí có thể không hợp lệ cho tất cả các thành viên và cơ sở và ngay cả khi nó hợp lệ thì nó thừa về mặt ngữ nghĩa và có thể đắt tiền.

Một giải pháp ngày càng phổ biến là thực hiện operator=bằng cách sử dụng phương thức sao chép và phương thức hoán đổi.

MyClass& operator=(const MyClass& other)
{
    MyClass tmp(other);
    swap(tmp);
    return *this;
}

hoặc thậm chí:

MyClass& operator=(MyClass other)
{
    swap(other);
    return *this;
}

Một swaphàm thường đơn giản để viết vì nó chỉ hoán đổi quyền sở hữu của các bên trong và không phải xóa trạng thái hiện có hoặc phân bổ tài nguyên mới.

Ưu điểm của thành ngữ sao chép và hoán đổi là nó tự động an toàn và - miễn là hoạt động hoán đổi là không cần thiết - cũng rất an toàn ngoại lệ.

Để đảm bảo an toàn ngoại lệ mạnh mẽ, một toán tử gán tài nguyên viết tay 'thường phải cấp phát một bản sao của tài nguyên mới trước khi hủy phân bổ tài nguyên cũ của người được chuyển nhượng để nếu có ngoại lệ xảy ra, phân bổ tài nguyên mới, trạng thái cũ vẫn có thể được trả về . Tất cả điều này đều miễn phí với tính năng sao chép và trao đổi nhưng thường phức tạp hơn và do đó dễ xảy ra lỗi, phải làm lại từ đầu.

Một điều cần cẩn thận là đảm bảo rằng phương thức hoán đổi là một phương thức hoán đổi thực sự, chứ không phải là phương thức mặc định std::swapsử dụng chính hàm tạo bản sao và toán tử gán.

Thông thường một thành viên swapđược sử dụng. std::swaphoạt động và được đảm bảo 'không ném' với tất cả các kiểu cơ bản và kiểu con trỏ. Hầu hết các con trỏ thông minh cũng có thể được hoán đổi với một đảm bảo không ném.


3
Trên thực tế, chúng không phải là hoạt động phổ biến. Trong khi bản sao ctor lần đầu tiên khởi tạo các thành viên của đối tượng, toán tử gán sẽ ghi đè các giá trị hiện có. Xem xét điều này, việc phân bổ operator=từ ctor bản sao trên thực tế là khá tệ, vì trước tiên nó khởi tạo tất cả các giá trị thành một số mặc định chỉ để ghi đè chúng bằng các giá trị của đối tượng khác ngay sau đó.
sbi 14/11/09

14
Có thể để "Tôi không đề nghị", hãy thêm "và bất kỳ chuyên gia C ++ nào cũng vậy". Ai đó có thể đi cùng và không nhận ra rằng bạn không chỉ thể hiện sở thích thiểu số cá nhân, mà là ý kiến ​​đồng thuận ổn định của những người đã thực sự nghĩ về nó. Và, OK, có thể tôi đã sai và một số chuyên gia C ++ thực sự khuyên bạn nên làm điều đó, nhưng cá nhân tôi vẫn muốn ai đó đưa ra tài liệu tham khảo cho đề xuất đó.
Steve Jessop

4
Đủ công bằng, dù sao thì tôi cũng đã ủng hộ bạn rồi :-). Tôi nghĩ rằng nếu điều gì đó được nhiều người coi là phương pháp hay nhất, thì tốt nhất bạn nên nói như vậy (và hãy xem xét lại nếu ai đó nói rằng nó không thực sự tốt nhất sau tất cả). Tương tự như vậy nếu ai đó hỏi "có thể sử dụng mutexes trong C ++ không", tôi sẽ không nói "một lựa chọn khá phổ biến là hoàn toàn bỏ qua RAII và viết mã an toàn không ngoại lệ gây bế tắc trong sản xuất, nhưng nó ngày càng phổ biến để viết đàng hoàng, mã làm việc ";-)
Steve Jessop

4
+1. Và tôi nghĩ rằng luôn có sự phân tích cần thiết. Tôi nghĩ là hợp lý khi có một assignhàm thành viên được sử dụng bởi cả copy ctor và toán tử gán trong một số trường hợp (đối với các lớp nhẹ). Trong các trường hợp khác (các trường hợp sử dụng / sử dụng nhiều tài nguyên, xử lý / nội dung), tất nhiên phải thực hiện sao chép / hoán đổi.
Johannes Schaub - litb 14/11/09

2
@litb: Tôi rất ngạc nhiên vì điều này nên tôi đã tra cứu Mục 41 trong Exception C ++ (cái này đã biến thành) và đề xuất cụ thể này đã biến mất và anh ấy đề xuất sao chép và hoán đổi ở vị trí của nó. Thay vào đó, anh ta lén lút bỏ "Vấn đề # 4: Bài tập không hiệu quả" cùng một lúc.
CB Bailey

13

Hàm tạo bản sao thực hiện khởi tạo lần đầu các đối tượng từng là bộ nhớ thô. Toán tử gán, OTOH, ghi đè các giá trị hiện có bằng các giá trị mới. Thường xuyên hơn không bao giờ, điều này liên quan đến việc loại bỏ các tài nguyên cũ (ví dụ: bộ nhớ) và phân bổ tài nguyên mới.

Nếu có sự giống nhau giữa cả hai, đó là toán tử gán thực hiện hủy và xây dựng bản sao. Một số nhà phát triển đã từng thực sự triển khai phân công bằng cách phá hủy tại chỗ, sau đó là xây dựng bản sao theo vị trí. Tuy nhiên, đây là một ý tưởng rất tồi. (Điều gì sẽ xảy ra nếu đây là toán tử gán của một lớp cơ sở được gọi trong khi gán một lớp dẫn xuất?)

Những gì thường được coi là thành ngữ kinh điển ngày nay đang được sử dụng swapnhư Charles đã đề xuất:

MyClass& operator=(MyClass other)
{
    swap(other);
    return *this;
}

Điều này sử dụng sao chép-xây dựng (lưu ý rằng otherđược sao chép) và phá hủy (nó bị hủy ở cuối hàm) - và nó cũng sử dụng chúng theo đúng thứ tự: xây dựng (có thể thất bại) trước khi phá hủy (không được thất bại).


Nên swapkhai báo virtual?

1
@Johannes: Các hàm ảo được sử dụng trong phân cấp lớp đa hình. Toán tử gán được sử dụng cho các kiểu giá trị. Cả hai hầu như không trộn lẫn.
sbi

-3

Có điều gì đó làm tôi khó chịu:

MyClass& operator=(const MyClass& other)
{
    MyClass tmp(other);
    swap(tmp);
    return *this;
}

Đầu tiên, đọc từ "hoán đổi" khi tâm trí tôi đang nghĩ "bản sao" làm tôi khó chịu. Ngoài ra, tôi đặt câu hỏi về mục tiêu của thủ thuật lạ mắt này. Có, bất kỳ ngoại lệ nào trong việc xây dựng các tài nguyên mới (được sao chép) sẽ xảy ra trước khi hoán đổi, đây có vẻ như là một cách an toàn để đảm bảo tất cả dữ liệu mới được lấp đầy trước khi đưa nó vào hoạt động.

Tốt rồi. Vì vậy, những gì về ngoại lệ xảy ra sau khi hoán đổi? (khi tài nguyên cũ bị hủy khi đối tượng tạm thời vượt ra khỏi phạm vi) Từ quan điểm của người sử dụng phép gán, hoạt động đã thất bại, ngoại trừ nó đã không. Nó có một tác dụng phụ rất lớn: bản sao đã thực sự xảy ra. Nó chỉ là một số tài nguyên dọn dẹp không thành công. Trạng thái của đối tượng đích đã được thay đổi mặc dù hoạt động từ bên ngoài có vẻ như đã thất bại.

Vì vậy, tôi đề xuất thay vì "hoán đổi" để thực hiện một "chuyển giao" tự nhiên hơn:

MyClass& operator=(const MyClass& other)
{
    MyClass tmp(other);
    transfer(tmp);
    return *this;
}

Vẫn còn việc xây dựng đối tượng tạm thời, nhưng hành động ngay lập tức tiếp theo là giải phóng tất cả tài nguyên hiện tại của đích trước khi di chuyển (và NULLing để chúng không bị giải phóng hai lần) tài nguyên của nguồn tới nó.

Thay vì {cấu tạo, di chuyển, hủy bỏ}, tôi đề xuất {cấu trúc, hủy, di chuyển}. Động thái, là hành động nguy hiểm nhất, là hành động được thực hiện cuối cùng sau khi mọi việc khác đã được giải quyết.

Có, phá hủy không thành công là một vấn đề trong cả hai chương trình. Dữ liệu bị hỏng (được sao chép khi bạn không nghĩ là có) hoặc bị mất (được giải phóng khi bạn không nghĩ là có). Mất tốt hơn là bị hỏng. Không có dữ liệu nào tốt hơn dữ liệu xấu.

Chuyển khoản thay vì hoán đổi. Đó là gợi ý của tôi anyway.


2
Một trình hủy không được thất bại, vì vậy không mong đợi các trường hợp ngoại lệ khi phá hủy. Và, tôi không hiểu lợi ích của việc di chuyển đằng sau sự phá hủy là gì, nếu di chuyển là hoạt động nguy hiểm nhất? Tức là, trong sơ đồ tiêu chuẩn, một lần di chuyển thất bại sẽ không làm hỏng trạng thái cũ, trong khi sơ đồ mới của bạn thì có. Vậy tại sao? Ngoài ra, First, reading the word "swap" when my mind is thinking "copy" irritates-> Là một người viết thư viện, bạn thường biết các phương pháp phổ biến (sao chép + hoán đổi), và mấu chốt là my mind. Tâm trí của bạn thực sự ẩn sau giao diện công khai. Đó là tất cả những gì mã có thể sử dụng lại.
Sebastian Mach
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.