Quy tắc 5 - sử dụng nó hay không?


20

Quy tắc 3 ( quy tắc 5 trong tiêu chuẩn c ++ mới) nêu rõ:

Nếu bạn cần phải khai báo rõ ràng hàm hủy, sao chép hàm tạo hoặc toán tử gán gán sao chép, có lẽ bạn cần khai báo rõ ràng cả ba hàm này.

Tuy nhiên, mặt khác, " Mã sạch " của Martin khuyên nên xóa tất cả các hàm tạo và hàm hủy trống (trang 293, G12: Clutter ):

Việc sử dụng là một constructor mặc định không có triển khai? Tất cả những gì nó làm là làm lộn xộn mã với các tạo tác vô nghĩa.

Vậy, làm thế nào để xử lý hai ý kiến ​​trái chiều này? Các nhà xây dựng / phá hủy trống thực sự nên được thực hiện?


Ví dụ tiếp theo thể hiện chính xác những gì tôi muốn nói:

#include <iostream>
#include <memory>

struct A
{
    A( const int value ) : v( new int( value ) ) {}
    ~A(){}
    A( const A & other ) : v( new int( *other.v ) ) {}
    A& operator=( const A & other )
    {
        v.reset( new int( *other.v ) );
        return *this;
    }

    std::auto_ptr< int > v;
};
int main()
{
    const A a( 55 );
    std::cout<< "a value = " << *a.v << std::endl;
    A b(a);
    std::cout<< "b value = " << *b.v << std::endl;
    const A c(11);
    std::cout<< "c value = " << *c.v << std::endl;
    b = c;
    std::cout<< "b new value = " << *b.v << std::endl;
}

Biên dịch tốt bằng g ++ 4.6.1 với:

g++ -std=c++0x -Wall -Wextra -pedantic example.cpp

Các hàm hủy cho struct Atrống, và không thực sự cần thiết. Vì vậy, nó nên ở đó, hoặc nó nên được gỡ bỏ?


15
2 trích dẫn nói về những điều khác nhau. Hoặc tôi hoàn toàn bỏ lỡ quan điểm của bạn.
Benjamin Bannier

1
@honk Trong tiêu chuẩn mã hóa của nhóm tôi, chúng tôi có một quy tắc là luôn luôn khai báo cả 4 (hàm tạo, hàm hủy, hàm tạo sao chép). Tôi đã tự hỏi nếu nó thực sự có ý nghĩa để làm. Tôi có thực sự phải luôn luôn tuyên bố các hàm hủy, ngay cả khi chúng trống không?
BЈовић

Còn đối với những người giải thích trống rỗng, hãy nghĩ về điều này: codynt tổng hợp / ~boris / blog / 2012/04/04 / . Mặt khác, quy tắc 3 (5) có ý nghĩa hoàn hảo đối với tôi, không biết tại sao người ta lại muốn có quy tắc 4.
Benjamin Bannier

@honk Xem ra thông tin bạn tìm thấy trên mạng. Không phải tất cả là sự thật. Ví dụ: virtual ~base () = default;không biên dịch (với một lý do chính đáng)
BЈовић

@VJovic, Không, bạn không phải khai báo một hàm hủy rỗng, trừ khi bạn cần biến nó thành ảo. Và trong khi chúng tôi đang ở chủ đề này, bạn cũng không nên sử dụng auto_ptr.
Dima

Câu trả lời:


44

Để bắt đầu, quy tắc nói "có thể", vì vậy nó không phải lúc nào cũng được áp dụng.

Điểm thứ hai tôi thấy ở đây là nếu bạn phải khai báo một trong ba, thì đó là vì nó đang làm một việc đặc biệt như phân bổ bộ nhớ. Trong trường hợp này, những cái khác sẽ không trống vì chúng sẽ phải xử lý cùng một tác vụ (chẳng hạn như sao chép nội dung của bộ nhớ được cấp phát động trong hàm tạo sao chép hoặc giải phóng bộ nhớ đó).

Vì vậy, như một kết luận, bạn không nên khai báo các hàm tạo hoặc hàm hủy rỗng, nhưng rất có thể là nếu cần một cái, thì những cái khác cũng cần.

Ví dụ của bạn: Trong trường hợp như vậy, bạn có thể loại bỏ hàm hủy. Nó không làm gì cả, rõ ràng. Việc sử dụng con trỏ thông minh là một ví dụ hoàn hảo về vị trí và lý do tại sao quy tắc 3 không giữ.

Đây chỉ là một hướng dẫn về nơi để xem qua mã của bạn trong trường hợp bạn có thể quên thực hiện chức năng quan trọng mà bạn có thể đã bỏ lỡ.


Với việc sử dụng các con trỏ thông minh, trong hầu hết các trường hợp, các hàm hủy đều trống rỗng (tôi muốn nói> 99% các hàm hủy trong cơ sở mã của tôi là trống, vì hầu như mọi lớp đều sử dụng thành ngữ pimpl).
BЈовић

Wow, đó là quá nhiều nổi mụn tôi gọi nó là mùi. Với nhiều trình biên dịch, pimpling sẽ khó tối ưu hóa hơn (ví dụ như khó hơn để nội tuyến).
Benjamin Bannier

@honk Ý bạn là gì khi "nhiều trình biên dịch nổi mụn"? :)
BЈовић

@VJovic: xin lỗi, lỗi đánh máy: 'mã nổi bật'
Benjamin Bannier

4

Thực sự không có mâu thuẫn ở đây. Quy tắc 3 nói về hàm hủy, hàm tạo sao chép và toán tử gán sao chép. Chú Bob nói về các nhà xây dựng mặc định trống rỗng.

Nếu bạn cần một hàm hủy, thì lớp của bạn có thể chứa các con trỏ tới bộ nhớ được cấp phát động và bạn có thể muốn có một ctor sao chép và một operator=()bản sao thực hiện một bản sao sâu. Điều này hoàn toàn trực giao cho dù bạn có cần một hàm tạo mặc định hay không.

Cũng lưu ý rằng trong C ++, có những tình huống khi bạn cần một hàm tạo mặc định, ngay cả khi nó trống. Giả sử lớp của bạn có hàm tạo không mặc định. Trong trường hợp đó, trình biên dịch sẽ không tạo ra một hàm tạo mặc định cho bạn. Điều đó có nghĩa là các đối tượng của lớp này không thể được lưu trữ trong các thùng chứa STL, bởi vì các đối tượng đó mong muốn các đối tượng có thể được mặc định.

Mặt khác, nếu bạn không định đặt các đối tượng của lớp vào các thùng chứa STL, thì một hàm tạo mặc định trống chắc chắn là sự lộn xộn vô dụng.


2

Ở đây, tiềm năng (*) của bạn tương đương với một hàm tạo / gán / hàm hủy mặc định có một mục đích: ghi lại thực tế bạn có về vấn đề và xác định rằng hành vi mặc định là chính xác. BTW, trong C ++ 11, mọi thứ chưa ổn định đủ để biết liệu =defaultcó thể phục vụ mục đích đó hay không.

(Có một mục đích tiềm năng khác: cung cấp một định nghĩa ngoài dòng thay vì định nghĩa nội tuyến mặc định, tốt hơn là ghi lại rõ ràng nếu bạn có bất kỳ lý do nào để làm như vậy).

(*) Tiềm năng vì tôi không nhớ trường hợp thực tế trong đó quy tắc ba không áp dụng, nếu tôi phải làm gì đó trong một, tôi phải làm gì đó với người khác.


Chỉnh sửa sau khi bạn thêm một ví dụ. ví dụ của bạn sử dụng auto_ptr rất thú vị. Bạn đang sử dụng một con trỏ thông minh, nhưng không phải là một con trỏ phù hợp với công việc. Tôi thà viết một cái - đặc biệt là nếu tình huống xảy ra thường xuyên - hơn là làm những gì bạn đã làm. (Nếu tôi không nhầm, cả tiêu chuẩn và boost đều không cung cấp một cái).


Ví dụ chứng minh quan điểm của tôi. Hàm hủy không thực sự cần thiết, nhưng quy tắc 3 cho biết nó nên ở đó.
BЈовић

1

Quy tắc 5 là một phần mở rộng thận trọng của quy tắc 3 là một hành vi thận trọng khiến đối tượng có thể lạm dụng.

Nếu bạn cần phải có một hàm hủy, điều đó có nghĩa là bạn đã thực hiện một số "quản lý tài nguyên" khác với mặc định (chỉ xây dựng và hủy các giá trị ).

Vì sao chép, gán, di chuyển và chuyển theo các giá trị sao chép mặc định , nếu bạn không chỉ giữ các giá trị , bạn phải xác định phải làm gì.

Điều đó nói rằng, C ++ xóa bản sao nếu bạn xác định di chuyển và xóa di chuyển nếu bạn xác định bản sao. Trong hầu hết các trường hợp, bạn phải xác định nếu bạn muốn mô phỏng một giá trị (do đó sao chép mut clone tài nguyên và di chuyển không có ý nghĩa) hoặc một trình quản lý tài nguyên (và do đó di chuyển tài nguyên, trong đó sao chép không có ý nghĩa: quy tắc 3 trở thành quy tắc của 3 )

Các trường hợp khi bạn phải xác định cả sao chép và di chuyển (quy tắc 5) là khá hiếm: thông thường bạn có "giá trị lớn" phải được sao chép nếu được cung cấp cho các đối tượng riêng biệt, nhưng có thể được di chuyển nếu lấy từ một đối tượng tạm thời (tránh một bản sao sau đó phá hủy ). Đó là trường hợp của container STL hoặc container số học.

Một trường hợp có thể là ma trận: chúng phải hỗ trợ sao chép vì chúng các giá trị, ( a=b; c=b; a*=2; b*=3;không được ảnh hưởng lẫn nhau) nhưng chúng có thể được tối ưu hóa bằng cách hỗ trợ di chuyển ( a = 3*b+4*c+hai thời gian và tạo tạm thời: tránh sao chép và xóa có thể hữu ích)


1

Tôi thích một cụm từ khác của quy tắc ba, có vẻ hợp lý hơn, đó là "nếu lớp của bạn cần một hàm hủy (trừ một hàm hủy ảo trống) thì có lẽ nó cũng cần một hàm tạo sao chép và toán tử gán."

Việc chỉ định nó như một mối quan hệ một chiều từ hàm hủy làm cho một vài điều rõ ràng hơn:

  1. Nó không áp dụng trong trường hợp bạn cung cấp một hàm tạo sao chép hoặc toán tử gán không mặc định như là một tối ưu hóa.

  2. Lý do cho quy tắc là hàm tạo sao chép mặc định hoặc toán tử gán có thể làm hỏng việc quản lý tài nguyên thủ công. Nếu bạn đang quản lý tài nguyên theo cách thủ công, có khả năng bạn đã nhận ra rằng bạn sẽ cần một công cụ hủy để giải phóng chúng.


-3

Có một điểm khác chưa được đề cập trong cuộc thảo luận: Một hàm hủy phải luôn là ảo.

struct A
{
    A( const int value ) : v( new int( value ) ) {}
    virtual ~A(){}
    ...
}

Hàm tạo cần được khai báo là ảo trong lớp cơ sở để biến nó thành ảo trong tất cả các lớp dẫn xuất. Vì vậy, ngay cả khi lớp cơ sở của bạn không cần một hàm hủy, cuối cùng bạn vẫn khai báo và thực hiện một hàm hủy trống.

Nếu bạn đưa ra tất cả các cảnh báo trên (-Wall -Wextra -weffc ++) g ++ sẽ cảnh báo bạn về điều này. Tôi coi đó là một cách thực hành tốt để luôn luôn khai báo một hàm hủy ảo trong bất kỳ lớp nào, bởi vì bạn không bao giờ biết, nếu cuối cùng lớp của bạn sẽ trở thành một lớp cơ sở. Nếu bộ hủy ảo không cần thiết, nó không gây hại. Nếu có, bạn tiết kiệm thời gian để tìm lỗi.


1
Nhưng tôi không muốn nhà xây dựng ảo. Nếu tôi làm điều đó, thì mọi cuộc gọi đến bất kỳ phương thức nào cũng sẽ sử dụng công văn ảo. btw lưu ý rằng không có thứ gọi là "hàm tạo ảo" trong c ++. Ngoài ra, tôi đã biên soạn ví dụ như mức cảnh báo rất cao.
BЈовић

IIRC, quy tắc mà gcc sử dụng để cảnh báo và quy tắc tôi thường tuân theo dù sao đi nữa, đó là nên có một hàm hủy ảo nếu có bất kỳ phương thức ảo nào khác trong lớp.
Jules
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.