Làm thế nào là = = mặc định khác với khác nhau của Khác {} đối với hàm tạo và hàm hủy mặc định?


169

Ban đầu tôi chỉ đăng bài này dưới dạng câu hỏi về các hàm hủy, nhưng bây giờ tôi đang xem xét thêm về hàm tạo mặc định. Đây là câu hỏi ban đầu:

Nếu tôi muốn cung cấp cho lớp của mình một hàm hủy là ảo, nhưng khác với những gì trình biên dịch sẽ tạo ra, tôi có thể sử dụng =default:

class Widget {
public:
   virtual ~Widget() = default;
};

Nhưng dường như tôi có thể có được hiệu ứng tương tự với việc gõ ít hơn bằng cách sử dụng một định nghĩa trống:

class Widget {
public:
   virtual ~Widget() {}
};

Có cách nào để hai định nghĩa này hành xử khác nhau không?

Dựa trên các câu trả lời được đăng cho câu hỏi này, tình huống cho hàm tạo mặc định có vẻ tương tự. Cho rằng hầu như không có sự khác biệt về ý nghĩa giữa " =default" và " {}" đối với các hàm hủy, có giống nhau gần như không có sự khác biệt về ý nghĩa giữa các tùy chọn này đối với các hàm tạo mặc định không? Đó là, giả sử tôi muốn tạo một loại trong đó các đối tượng của loại đó sẽ được tạo và hủy, tại sao tôi muốn nói

Widget() = default;

thay vì

Widget() {}

?

Tôi xin lỗi nếu mở rộng câu hỏi này sau khi bài đăng ban đầu của nó vi phạm một số quy tắc SO. Đăng một câu hỏi gần như giống hệt nhau cho các nhà xây dựng mặc định đánh tôi là tùy chọn ít mong muốn hơn.


1
Không phải tôi biết, nhưng = defaultimo rõ ràng hơn, và phù hợp với sự hỗ trợ cho nó với các nhà xây dựng.
chris

11
Tôi không biết chắc chắn, nhưng tôi nghĩ cái trước phù hợp với định nghĩa của "kẻ hủy diệt tầm thường", trong khi cái sau thì không. Vì vậy, std::has_trivial_destructor<Widget>::valuetruecho thứ nhất, nhưng falsecho thứ hai. Ý nghĩa của việc đó là gì tôi cũng không biết. :)
GManNickG

10
Một kẻ hủy diệt ảo không bao giờ là tầm thường.
Luc Danton

@LucDanton: Tôi cho rằng mở mắt ra và nhìn vào mã cũng sẽ hoạt động! Cảm ơn đã sửa chữa.
GManNickG

Câu trả lời:


103

Đây là một câu hỏi hoàn toàn khác khi hỏi về các nhà xây dựng hơn là các hàm hủy.

Nếu kẻ hủy diệt của bạn là virtual, thì sự khác biệt là không đáng kể, như Howard đã chỉ ra . Tuy nhiên, nếu kẻ hủy diệt của bạn không ảo , thì đó là một câu chuyện hoàn toàn khác. Điều tương tự cũng đúng với các nhà xây dựng.

Sử dụng = defaultcú pháp cho các hàm thành viên đặc biệt (hàm tạo mặc định, sao chép / di chuyển hàm tạo / gán, hàm hủy, v.v.) có nghĩa là một cái gì đó rất khác so với cách làm đơn giản {}. Với cái sau, chức năng trở thành "do người dùng cung cấp". Và điều đó thay đổi mọi thứ.

Đây là một lớp tầm thường theo định nghĩa của C ++ 11:

struct Trivial
{
  int foo;
};

Nếu bạn cố gắng xây dựng mặc định một, trình biên dịch sẽ tự động tạo một hàm tạo mặc định. Tương tự với sao chép / chuyển động và phá hủy. Do người dùng không cung cấp bất kỳ chức năng thành viên nào trong số này, nên đặc tả C ++ 11 coi đây là lớp "tầm thường". Do đó, hợp pháp để làm điều này, như memcpy nội dung của họ xung quanh để khởi tạo chúng và vv.

Điều này:

struct NotTrivial
{
  int foo;

  NotTrivial() {}
};

Như tên cho thấy, điều này không còn tầm thường nữa. Nó có một hàm tạo mặc định do người dùng cung cấp. Nó không quan trọng nếu nó trống rỗng; theo như các quy tắc của C ++ 11 có liên quan, đây không thể là một loại tầm thường.

Điều này:

struct Trivial2
{
  int foo;

  Trivial2() = default;
};

Một lần nữa như tên cho thấy, đây là một loại tầm thường. Tại sao? Bởi vì bạn đã bảo trình biên dịch tự động tạo hàm tạo mặc định. Do đó, hàm tạo không phải là "do người dùng cung cấp." Và do đó, loại được coi là tầm thường, vì nó không có hàm tạo mặc định do người dùng cung cấp.

Các = defaultcú pháp chủ yếu là ở đó làm những việc như constructor sao chép / chuyển nhượng, khi bạn thêm chức năng thành viên đó ngăn chặn việc tạo ra các chức năng như vậy. Nhưng nó cũng kích hoạt hành vi đặc biệt từ trình biên dịch, do đó, nó cũng hữu ích trong các hàm tạo / hàm hủy mặc định.


2
Vì vậy, vấn đề chính dường như là liệu lớp kết quả có tầm thường hay không, và vấn đề cơ bản là sự khác biệt giữa một hàm đặc biệt được khai báo bởi người dùng (đó là trường hợp của =defaulthàm) và hàm do người dùng cung cấp (là trường hợp cho {}). Cả hai hàm do người dùng khai báo và do người dùng cung cấp đều có thể ngăn chặn việc tạo ra hàm thành viên đặc biệt khác (ví dụ: hàm hủy do người dùng khai báo ngăn chặn việc tạo các hoạt động di chuyển), nhưng chỉ một hàm đặc biệt do người dùng cung cấp mới thể hiện một lớp không tầm thường. Đúng?
KnowIt ALLWannabe

@KnowIt ALLWannabe: Đó là ý tưởng chung, vâng.
Nicol Bolas

Tôi đang chọn đây là câu trả lời được chấp nhận, chỉ bởi vì nó bao gồm cả các hàm tạo và (bằng cách tham khảo câu trả lời của Howard).
KnowIt ALLWannabe

Có vẻ như là một từ còn thiếu ở đây "theo như các quy tắc của C ++ 11 có liên quan, bạn có quyền của một loại tầm thường" Tôi sẽ sửa nó nhưng tôi không chắc chắn 100% những gì đã được dự định.
jcoder

2
= defaultdường như rất hữu ích khi buộc trình biên dịch tạo ra một hàm tạo mặc định mặc dù có sự hiện diện của các hàm tạo khác; hàm tạo mặc định không được khai báo ngầm nếu bất kỳ hàm tạo nào do người dùng khai báo khác được cung cấp.
bgfvdu3w

42

Cả hai đều không tầm thường.

Cả hai đều có cùng đặc điểm kỹ thuật không ngoại lệ tùy thuộc vào đặc điểm kỹ thuật không ngoại lệ của các cơ sở và thành viên.

Sự khác biệt duy nhất tôi phát hiện cho đến nay là nếu Widgetchứa một cơ sở hoặc thành viên có hàm hủy không thể truy cập hoặc bị xóa:

struct A
{
private:
    ~A();
};

class Widget {
    A a_;
public:
#if 1
   virtual ~Widget() = default;
#else
   virtual ~Widget() {}
#endif
};

Sau đó, =defaultgiải pháp sẽ biên dịch, nhưng Widgetsẽ không phải là một loại có thể phá hủy. Tức là nếu bạn cố gắng phá hủy a Widget, bạn sẽ gặp lỗi thời gian biên dịch. Nhưng nếu bạn không, bạn đã có một chương trình làm việc.

Otoh, nếu bạn cung cấp hàm hủy do người dùng cung cấp , thì mọi thứ sẽ không biên dịch cho dù bạn có phá hủy hay không Widget:

test.cpp:8:7: error: field of type 'A' has private destructor
    A a_;
      ^
test.cpp:4:5: note: declared private here
    ~A();
    ^
1 error generated.

9
Thú vị: nói cách khác, với =default;trình biên dịch sẽ không tạo ra hàm hủy trừ khi nó được sử dụng và do đó sẽ không gây ra lỗi. Điều này có vẻ kỳ lạ với tôi, ngay cả khi không nhất thiết là một lỗi. Tôi không thể tưởng tượng hành vi này là bắt buộc trong tiêu chuẩn.
Nik Bougalis

"Sau đó, giải pháp = mặc định sẽ biên dịch" Không, nó sẽ không. Chỉ cần thử nghiệm so với
nano

Thông báo lỗi là gì, và phiên bản nào của VS?
Howard Hinnant

35

Sự khác biệt quan trọng giữa

class B {
    public:
    B(){}
    int i;
    int j;
};

class B {
    public:
    B() = default;
    int i;
    int j;
};

là hàm tạo mặc định được xác định với B() = default;được coi là không xác định người dùng . Điều này có nghĩa là trong trường hợp khởi tạo giá trị như trong

B* pb = new B();  // use of () triggers value-initialization

loại khởi tạo đặc biệt hoàn toàn không sử dụng hàm tạo sẽ diễn ra và đối với các kiểu dựng sẵn, điều này sẽ dẫn đến không khởi tạo . Trong trường hợp B(){}này sẽ không diễn ra. Tiêu chuẩn C ++ n3337 § 8,5 / 7 nói

Để khởi tạo giá trị một đối tượng thuộc loại T có nghĩa là:

- nếu T là loại lớp (có thể đủ điều kiện cv) (Khoản 9) với hàm tạo do người dùng cung cấp (12.1), thì hàm tạo mặc định cho T được gọi (và khởi tạo không được định dạng nếu T không có hàm tạo mặc định có thể truy cập );

- nếu T là loại lớp không liên kết (có thể đủ điều kiện cv) mà không có hàm tạo do người dùng cung cấp , thì đối tượng được khởi tạo bằng 0 và, nếu hàm tạo mặc định được khai báo ngầm của T là không tầm thường, thì hàm tạo đó được gọi.

- nếu T là một kiểu mảng, thì mỗi phần tử được khởi tạo giá trị; - nếu không, đối tượng là không khởi tạo.

Ví dụ:

#include <iostream>

class A {
    public:
    A(){}
    int i;
    int j;
};

class B {
    public:
    B() = default;
    int i;
    int j;
};

int main()
{
    for( int i = 0; i < 100; ++i) {
        A* pa = new A();
        B* pb = new B();
        std::cout << pa->i << "," << pa->j << std::endl;
        std::cout << pb->i << "," << pb->j << std::endl;
        delete pa;
        delete pb;
    }
  return 0;
}

kết quả có thể

0,0
0,0
145084416,0
0,0
145084432,0
0,0
145084416,0
//...

http://ideone.com/k8mBrd


Vậy thì, tại sao "{}" và "= default" luôn khởi tạo một std :: string ideone.com/LMv5Uf ?
nawfel bgh

1
@nawfelbgh Hàm tạo mặc định A () {} gọi hàm tạo mặc định cho std :: string vì đây không phải là loại POD. Ctor mặc định của std :: string khởi tạo nó thành chuỗi rỗng 0, kích thước. Ctor mặc định cho vô hướng không làm gì cả: các đối tượng có thời lượng lưu trữ tự động (và các tiểu dự án của chúng) được khởi tạo để xác định giá trị không xác định.
4pie0
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.