Tại sao C ++ yêu cầu một hàm tạo mặc định do người dùng cung cấp để tạo mặc định một đối tượng const?


99

Tiêu chuẩn C ++ (phần 8.5) cho biết:

Nếu một chương trình yêu cầu khởi tạo mặc định một đối tượng thuộc kiểu const-đủ điều kiện T, T sẽ là một kiểu lớp có hàm tạo mặc định do người dùng cung cấp.

Tại sao? Tôi không thể nghĩ ra bất kỳ lý do nào tại sao lại yêu cầu hàm tạo do người dùng cung cấp trong trường hợp này.

struct B{
  B():x(42){}
  int doSomeStuff() const{return x;}
  int x;
};

struct A{
  A(){}//other than "because the standard says so", why is this line required?

  B b;//not required for this example, just to illustrate
      //how this situation isn't totally useless
};

int main(){
  const A a;
}

2
Dòng dường như sẽ không được yêu cầu trong ví dụ của bạn (xem ideone.com/qqiXR ) vì bạn đã khai báo nhưng không xác định / khởi tạo a, nhưng gcc-4.3.4 chấp nhận nó ngay cả khi bạn làm vậy (xem ideone.com/uHvFS )
Ray Toal

Ví dụ trên vừa khai báo vừa định nghĩa a. Comeau tạo ra lỗi "biến const" a "yêu cầu bộ khởi tạo - lớp" A "không có hàm tạo mặc định được khai báo rõ ràng" nếu dòng được nhận xét ra.
Karu


4
Đây là cố định trong C ++ 11, bạn có thể viết const A a{}:)
Howard Lovatt

Câu trả lời:


10

Đây được coi là một khiếm khuyết (so với tất cả các phiên bản của tiêu chuẩn) và nó đã được giải quyết bởi Nhóm làm việc cốt lõi (CWG) Lỗi 253 . Từ ngữ mới cho các trạng thái tiêu chuẩn trong http://eel.is/c++draft/dcl.init#7

Kiểu lớp T là const-default-constructible nếu việc khởi tạo mặc định của T sẽ gọi một hàm tạo do người dùng cung cấp của T (không được kế thừa từ lớp cơ sở) hoặc nếu

  • mỗi thành viên dữ liệu không tĩnh không phải biến thể trực tiếp M của T có một bộ khởi tạo thành viên mặc định hoặc, nếu M thuộc loại lớp X (hoặc mảng của nó), X là const-default-constructible,
  • nếu T là một liên hợp có ít nhất một thành viên dữ liệu không tĩnh, chính xác một thành viên biến thể có bộ khởi tạo thành viên mặc định,
  • nếu T không phải là liên hợp, đối với mỗi thành viên liên hợp ẩn danh có ít nhất một thành viên dữ liệu không tĩnh (nếu có), chính xác một thành viên dữ liệu không tĩnh có bộ khởi tạo thành viên mặc định và
  • mỗi lớp cơ sở được xây dựng tiềm năng của T là const-default-constructible.

Nếu một chương trình yêu cầu khởi tạo mặc định một đối tượng thuộc kiểu const-đủ điều kiện T, thì T sẽ là kiểu lớp hoặc mảng cấu tạo const-mặc định của nó.

Từ ngữ này về cơ bản có nghĩa là mã rõ ràng hoạt động. Nếu bạn khởi tạo tất cả các căn cứ và thành viên của mình, bạn có thể nói A const a;bất kể cách nào hoặc nếu bạn đánh vần bất kỳ hàm tạo nào.

struct A {
};
A const a;

gcc đã chấp nhận điều này kể từ 4.6.4. clang đã chấp nhận điều này kể từ 3.9.0. Visual Studio cũng chấp nhận điều này (ít nhất là trong năm 2017, không chắc là sớm hơn).


3
Nhưng điều này vẫn bị cấm struct A { int n; A() = default; }; const A a;trong khi cho phép struct B { int n; B() {} }; const B b;vì từ ngữ mới vẫn nói "do người dùng cung cấp" chứ không phải "do người dùng khai báo" và tôi đang vò đầu bứt tai tại sao ủy ban lại chọn loại trừ các hàm tạo mặc định được xác định rõ ràng khỏi DR này, buộc chúng tôi phải thực hiện các lớp của chúng ta không tầm thường nếu chúng ta muốn các đối tượng const với các thành viên chưa được khởi tạo.
Oktalist

1
Thật thú vị, nhưng vẫn còn một trường hợp phức tạp mà tôi gặp phải. Với MyPODviệc là một POD struct, static MyPOD x;- dựa vào khởi tạo bằng 0 (đó có phải là cách đúng không?) Để đặt (các) biến thành viên một cách thích hợp - biên dịch, nhưng static const MyPOD x;không. Có cơ hội nào mà điều đó sẽ được sửa không?
Joshua Green

66

Lý do là nếu lớp không có hàm tạo do người dùng định nghĩa thì nó có thể là POD và lớp POD không được khởi tạo theo mặc định. Vì vậy, nếu bạn khai báo một đối tượng const của POD chưa được khởi tạo, thì việc sử dụng nó là gì? Vì vậy, tôi nghĩ Tiêu chuẩn thực thi quy tắc này để đối tượng có thể thực sự hữu ích.

struct POD
{
  int i;
};

POD p1; //uninitialized - but don't worry we can assign some value later on!
p1.i = 10; //assign some value later on!

POD p2 = POD(); //initialized

const POD p3 = POD(); //initialized 

const POD p4; //uninitialized  - error - as we cannot change it later on!

Nhưng nếu bạn đặt lớp này thành không phải POD:

struct nonPOD_A
{
    nonPOD_A() {} //this makes non-POD
};

nonPOD_A a1; //initialized 
const nonPOD_A a2; //initialized 

Lưu ý sự khác biệt giữa POD và không phải POD.

Hàm tạo do người dùng định nghĩa là một cách để làm cho lớp không phải là POD. Có một số cách bạn có thể làm điều đó.

struct nonPOD_B
{
    virtual void f() {} //virtual function make it non-POD
};

nonPOD_B b1; //initialized 
const nonPOD_B b2; //initialized 

Lưu ý rằng nonPOD_B không xác định hàm tạo do người dùng xác định. Biên dịch nó. Nó sẽ biên dịch:

Và nhận xét hàm ảo, sau đó nó báo lỗi, như mong đợi:


Tôi nghĩ bạn đã hiểu sai đoạn văn. Đầu tiên nó nói điều này (§8.5 / 9):

Nếu không có trình khởi tạo nào được chỉ định cho một đối tượng và đối tượng thuộc loại lớp không POD (có thể đủ điều kiện cv) (hoặc mảng của nó), đối tượng sẽ được khởi tạo mặc định; [...]

Nó nói về lớp không phải POD có thể là loại cv đủ tiêu chuẩn . Tức là, đối tượng không phải POD sẽ được khởi tạo mặc định nếu không có bộ khởi tạo nào được chỉ định. Và mặc định khởi tạo là gì? Đối với không phải POD, thông số cho biết (§8.5 / 5),

Để khởi tạo mặc định một đối tượng kiểu T có nghĩa là:
- nếu T là kiểu lớp không phải POD (điều 9), hàm tạo mặc định cho T được gọi (và quá trình khởi tạo không hợp lệ nếu T không có hàm tạo mặc định có thể truy cập);

Nó chỉ đơn giản nói về phương thức khởi tạo mặc định của T, cho dù nó do người dùng định nghĩa hay do trình biên dịch tạo ra đều không liên quan.

Nếu bạn hiểu rõ điều này, thì hãy hiểu thông số tiếp theo nói gì ((§8.5 / 9),

[...]; nếu đối tượng thuộc kiểu đủ điều kiện const, thì kiểu lớp bên dưới sẽ có một hàm tạo mặc định do người dùng khai báo.

Vì vậy, văn bản này ngụ ý, chương trình sẽ bị lỗi nếu đối tượng thuộc loại POD đủ điều kiện const và không có bộ khởi tạo nào được chỉ định (vì POD không được khởi tạo mặc định):

POD p1; //uninitialized - can be useful - hence allowed
const POD p2; //uninitialized - never useful  - hence not allowed - error

Nhân tiện, điều này biên dịch tốt , vì nó không phải POD và có thể được khởi tạo mặc định .


1
Tôi tin rằng ví dụ cuối cùng của bạn là một lỗi biên dịch - nonPOD_Bkhông có hàm tạo mặc định do người dùng cung cấp nên dòng const nonPOD_B b2này không được phép.
Karu

1
Một cách khác để làm cho lớp trở thành không phải POD là cấp cho nó một thành viên dữ liệu không phải là POD (ví dụ: cấu trúc của tôi Btrong câu hỏi). Nhưng hàm tạo mặc định do người dùng cung cấp vẫn được yêu cầu trong trường hợp đó.
Karu

"Nếu một chương trình yêu cầu khởi tạo mặc định đối tượng thuộc kiểu T đủ điều kiện const, T sẽ là kiểu lớp có hàm tạo mặc định do người dùng cung cấp."
Karu

@Karu: Tôi đã đọc nó. Có vẻ như có những đoạn khác trong thông số kỹ thuật, cho phép constđối tượng không phải POD được khởi tạo bằng cách gọi hàm tạo mặc định do trình biên dịch tạo ra.
Nawaz

2
Các liên kết Ideone của bạn dường như bị hỏng, và sẽ thật tuyệt nếu câu trả lời này có thể được cập nhật lên C ++ 11/14 vì §8.5 hoàn toàn không đề cập đến POD.
Oktalist

12

Tôi chỉ suy đoán thuần túy, nhưng hãy cân nhắc rằng các loại khác cũng có hạn chế tương tự:

int main()
{
    const int i; // invalid
}

Vì vậy, quy tắc này không chỉ nhất quán mà còn (đệ quy) ngăn constcác đối tượng đơn nguyên (con):

struct X {
    int j;
};
struct A {
    int i;
    X x;
}

int main()
{
    const A a; // a.i and a.x.j in unitialized states!
}

Đối với mặt khác của câu hỏi (cho phép nó đối với các loại có hàm tạo mặc định), tôi nghĩ ý tưởng là một loại có hàm tạo mặc định do người dùng cung cấp được cho là luôn ở trạng thái hợp lý sau khi xây dựng. Lưu ý rằng các quy tắc khi chúng được phép thực hiện những điều sau:

struct A {
    explicit
    A(int i): initialized(true), i(i) {} // valued constructor

    A(): initialized(false) {}

    bool initialized;
    int i;
};

const A a; // class invariant set up for the object
           // yet we didn't pay the cost of initializing a.i

Sau đó, có lẽ chúng ta có thể xây dựng một quy tắc như 'ít nhất một thành viên phải được khởi tạo hợp lý trong một hàm tạo mặc định do người dùng cung cấp', nhưng đó là cách dành quá nhiều thời gian để cố gắng bảo vệ khỏi Murphy. C ++ có xu hướng tin tưởng người lập trình vào một số điểm nhất định.


Nhưng bằng cách thêm A(){}, lỗi sẽ biến mất, vì vậy nó không ngăn cản bất cứ điều gì. Quy tắc không hoạt động đệ quy - X(){}không bao giờ cần thiết cho ví dụ đó.
Karu

2
Vâng, ít nhất bằng cách buộc các lập trình viên để thêm một constructor, ông buộc phải đưa ra suy nghĩ một phút cho vấn đề và có thể đưa ra một không tầm thường
Arne

@Karu tôi chỉ đề cập đến một nửa số câu hỏi - cố định mà :)
Luc Danton

4
@arne: Vấn đề duy nhất là lập trình viên sai. Người đang cố gắng khởi tạo lớp có thể đưa ra tất cả suy nghĩ mà anh ta muốn về vấn đề này, nhưng họ có thể không sửa đổi được lớp. Tác giả của lớp đã nghĩ về các thành viên, thấy rằng tất cả họ đều được khởi tạo hợp lý bởi hàm tạo mặc định ngầm định, vì vậy không bao giờ thêm một thành viên.
Karu

3
Những gì tôi đã lấy từ phần này của tiêu chuẩn là "luôn luôn khai báo một hàm tạo mặc định cho các loại không phải POD, trong trường hợp ai đó muốn tạo một phiên bản const vào một ngày nào đó". Điều đó có vẻ hơi quá mức cần thiết.
Karu

3

Tôi đã xem bài nói chuyện của Timur Doumler tại Cuộc họp C ++ 2018 và cuối cùng tôi đã nhận ra tại sao tiêu chuẩn yêu cầu một hàm tạo do người dùng cung cấp ở đây, không chỉ đơn thuần là một hàm do người dùng khai báo. Nó liên quan đến các quy tắc khởi tạo giá trị.

Hãy xem xét hai lớp: Acó một hàm tạo do người dùng khai báo , Bcó một hàm tạo do người dùng cung cấp :

struct A {
    int x;
    A() = default;
};
struct B {
    int x;
    B() {}
};

Thoạt nhìn, bạn có thể nghĩ rằng hai hàm tạo này sẽ hoạt động giống nhau. Nhưng hãy xem cách khởi tạo giá trị hoạt động khác nhau, trong khi chỉ khởi tạo mặc định hoạt động giống nhau:

  • A a;là khởi tạo mặc định: thành viên int xchưa được khởi tạo.
  • B b;là khởi tạo mặc định: thành viên int xchưa được khởi tạo.
  • A a{};là khởi tạo giá trị: thành viên int xđược khởi tạo bằng 0 .
  • B b{};là khởi tạo giá trị: thành viên int xchưa được khởi tạo.

Bây giờ hãy xem điều gì sẽ xảy ra khi chúng tôi thêm const:

  • const A a;là khởi tạo mặc định: điều này không đúng do quy tắc được trích dẫn trong câu hỏi.
  • const B b;là khởi tạo mặc định: thành viên int xchưa được khởi tạo.
  • const A a{};là khởi tạo giá trị: thành viên int xđược khởi tạo bằng 0 .
  • const B b{};là khởi tạo giá trị: thành viên int xchưa được khởi tạo.

Một constđại lượng vô hướng chưa được khởi tạo (ví dụ: int xthành viên) sẽ vô dụng: việc ghi vào nó là không hợp lệ (vì nó const) và đọc từ nó là UB (vì nó giữ một giá trị không xác định). Vì vậy, quy định này ngăn cản bạn từ việc tạo ra một điều như vậy, bằng cách buộc bạn hoặc thêm một initialiser hoặc opt-in để các hành vi nguy hiểm bằng cách thêm một constructor dùng cung cấp.

Tôi nghĩ sẽ rất hay nếu có một thuộc tính như [[uninitialized]]thông báo cho trình biên dịch khi bạn cố tình không khởi tạo một đối tượng. Sau đó, chúng tôi sẽ không bị buộc phải làm cho lớp của chúng tôi không phải là cấu trúc mặc định tầm thường để giải quyết trường hợp góc này. Thuộc tính này thực sự đã được đề xuất , nhưng cũng giống như tất cả các thuộc tính tiêu chuẩn khác, nó không bắt buộc bất kỳ hành vi quy chuẩn nào, chỉ là một gợi ý cho trình biên dịch.


1

Xin chúc mừng, bạn đã phát minh ra một trường hợp trong đó không cần bất kỳ hàm tạo nào do người dùng xác định để constkhai báo không có bộ khởi tạo có ý nghĩa.

Bây giờ bạn có thể đưa ra một cách diễn đạt lại hợp lý của quy tắc áp dụng cho trường hợp của bạn nhưng vẫn đưa ra các trường hợp đáng lẽ là bất hợp pháp không? Có ít hơn 5 hoặc 6 đoạn văn? Nó có dễ dàng và rõ ràng như thế nào nó nên được áp dụng trong mọi tình huống không?

Tôi cho rằng việc tạo ra một quy tắc cho phép khai báo mà bạn đã tạo có ý nghĩa thực sự khó và đảm bảo rằng quy tắc có thể được áp dụng theo cách có ý nghĩa với mọi người khi đọc mã còn khó hơn. Tôi thích một quy tắc có phần hạn chế là điều đúng đắn nên làm trong hầu hết các trường hợp hơn là một quy tắc rất phức tạp và có sắc thái khó hiểu và khó áp dụng.

Câu hỏi đặt ra là có lý do thuyết phục nào mà quy tắc nên phức tạp hơn không? Có một số mã sẽ rất khó viết hoặc khó hiểu có thể được viết đơn giản hơn nhiều nếu quy tắc phức tạp hơn không?


1
Đây là từ ngữ gợi ý của tôi: "Nếu một chương trình yêu cầu khởi tạo mặc định một đối tượng thuộc kiểu const-đủ điều kiện T, T sẽ là kiểu lớp không phải POD.". Điều này sẽ làm cho const POD x;bất hợp pháp cũng giống như const int x;bất hợp pháp (có lý, vì điều này là vô dụng đối với POD), nhưng const NonPOD x;hợp pháp (có lý, vì nó có thể có các subobject chứa các hàm tạo / hủy hữu ích hoặc có chính một hàm tạo / hủy hữu ích) .
Karu

@Karu - Cách diễn đạt đó có thể hiệu quả. Tôi đã quen với các tiêu chuẩn RFC, và vì vậy cảm thấy rằng 'T sẽ là' nên đọc là 'T phải là'. Nhưng có, điều đó có thể hoạt động.
Omnifarious

@Karu - Còn struct NonPod {int i; void ảo f () {}}? Không có ý nghĩa gì khi làm cho const NonPod x; hợp pháp.
gruzovator.

1
@gruzovator Sẽ có ý nghĩa gì hơn nếu bạn có một hàm tạo mặc định trống do người dùng khai báo? Đề xuất của tôi chỉ cố gắng loại bỏ một yêu cầu vô nghĩa của tiêu chuẩn; dù có hay không thì vẫn có vô số cách viết mã không có ý nghĩa.
Karu

1
@Karu Tôi đồng ý với bạn. Vì quy tắc này trong tiêu chuẩn nên có nhiều lớp phải có hàm tạo rỗng do người dùng định nghĩa . Tôi thích hành vi gcc. Nó cho phép ví dụ struct NonPod { std::string s; }; const NonPod x;Và đưa ra một lỗi khi NonPod làstruct NonPod { int i; std::string s; }; const NonPod x;
gruzovator
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.