Lỗi khi sử dụng khởi tạo trong lớp của thành viên dữ liệu không tĩnh và phương thức tạo lớp lồng nhau


90

Đoạn mã sau đây khá đơn giản và tôi mong rằng nó sẽ biên dịch tốt.

struct A
{
    struct B
    {
        int i = 0;
    };

    B b;

    A(const B& _b = B())
        : b(_b)
    {}
};

Tôi đã thử nghiệm mã này với g ++ phiên bản 4.7.2, 4.8.1, clang ++ 3.2 và 3.3. Ngoài thực tế là g ++ 4.7.2 mặc định trên mã này ( http://gcc.gnu.org/bugzilla/show_bug.cgi?id=57770 ), các trình biên dịch được thử nghiệm khác đưa ra các thông báo lỗi không giải thích nhiều.

g ++ 4.8.1:

test.cpp: In constructor constexpr A::B::B()’:
test.cpp:3:12: error: constructor required before non-static data member for A::B::i has been parsed
     struct B
            ^
test.cpp: At global scope:
test.cpp:11:23: note: synthesized method constexpr A::B::B()’ first required here 
     A(const B& _b = B())
                       ^

clang ++ 3.2 và 3.3:

test.cpp:11:21: error: defaulted default constructor of 'B' cannot be used by non-static data member initializer which appears before end of class definition
    A(const B& _b = B())
                    ^

Làm cho mã này có thể biên dịch được và có vẻ như nó sẽ không có gì khác biệt. Có hai lựa chọn:

struct B
{
    int i = 0;
    B(){} // using B()=default; works only for clang++
};

hoặc là

struct B
{
    int i;
    B() : i(0) {} // classic c++98 initialization
};

Có phải mã này thực sự không chính xác hay các trình biên dịch sai?


3
G ++ 4.7.3 của tôi nói internal compiler error: Segmentation faultvới mã này ...
Fred Foo

2
(lỗi C2864: 'A :: B :: i': chỉ các thành viên dữ liệu tích phân const tĩnh mới có thể được khởi tạo trong một lớp) là những gì VC2010 nói. Đầu ra đó đồng ý với g ++. Clang cũng nói vậy, mặc dù nó không có ý nghĩa gì nhiều. Bạn không thể mặc định một biến trong cấu trúc bằng cách thực hiện int i = 0trừ khi nó được static const int i = 0.
Chris Cooper

@Borgleader: BTW Tôi muốn tránh sự cám dỗ khi nghĩ về biểu thức B()như một lời gọi hàm đến một hàm tạo. Bạn không bao giờ trực tiếp "gọi" một hàm tạo. Hãy coi đây là cú pháp đặc biệt tạo ra một B... tạm thời và hàm tạo được gọi chỉ là một phần của quá trình đó, nằm sâu trong cơ chế tiếp theo.
Lightness Races ở Orbit

2
Rất tiếc, việc thêm một hàm tạo vào Bdường như sẽ làm cho điều này hoạt động gcc 4.7.
Shafik Yaghmour

7
Thật thú vị, việc di chuyển định nghĩa của hàm tạo của A ra khỏi A dường như cũng làm cho nó hoạt động (g ++ 4.7); chuông nào có "hàm tạo mặc định đã mặc định không thể được sử dụng ... trước khi kết thúc định nghĩa lớp".
moonshadow,

Câu trả lời:


84

Có phải mã này thực sự không chính xác hay các trình biên dịch sai?

Chà, cũng không. Tiêu chuẩn có một khiếm khuyết - nó cho biết cả hai thứ Ađược coi là hoàn chỉnh trong khi phân tích cú pháp trình khởi tạo cho B::ivà điều đó B::B()(sử dụng trình khởi tạo cho B::i) có thể được sử dụng trong định nghĩa của A. Điều đó rõ ràng là theo chu kỳ. Xem xét điều này:

struct A {
  struct B {
    int i = (A(), 0);
  };
  A() noexcept(!noexcept(B()));
};

Điều này có một mâu thuẫn: B::B()ngầm hiểu là noexceptiff A()không ném, và A()không ném B::B()thì không noexcept . Có một số chu kỳ và mâu thuẫn khác trong lĩnh vực này.

Điều này được theo dõi bởi các vấn đề cốt lõi 13601397 . Đặc biệt lưu ý lưu ý này trong vấn đề cốt lõi 1397:

Có lẽ cách tốt nhất để giải quyết vấn đề này là làm cho bộ khởi tạo thành viên dữ liệu không tĩnh sử dụng một phương thức khởi tạo mặc định của lớp nó.

Đó là một trường hợp đặc biệt của quy tắc mà tôi đã triển khai trong Clang để giải quyết vấn đề này. Quy tắc của Clang là một phương thức khởi tạo mặc định được cài đặt sẵn cho một lớp không thể được sử dụng trước khi các bộ khởi tạo thành viên dữ liệu không tĩnh cho lớp đó được phân tích cú pháp. Do đó, Clang đưa ra chẩn đoán ở đây:

    A(const B& _b = B())
                    ^

... bởi vì Clang phân tích cú pháp các đối số mặc định trước khi nó phân tích cú pháp các bộ khởi tạo mặc định và đối số mặc định này sẽ yêu cầu Bcác bộ khởi tạo mặc định đã được phân tích cú pháp (để xác định một cách ngầm định B::B()).


Tốt để biết. Nhưng thông báo lỗi vẫn gây hiểu lầm, vì hàm tạo thực tế không được "sử dụng bởi bộ khởi tạo thành viên dữ liệu không tĩnh".
aschepler

Bạn biết điều này do kinh nghiệm cụ thể trong quá khứ về vấn đề này, hay chỉ bằng cách đọc kỹ tiêu chuẩn (và danh sách các khuyết tật)? Ngoài ra, +1.
Cornstalks

+1 cho câu trả lời chi tiết này. Vậy đâu sẽ là lối thoát? Một số loại "phân tích cú pháp lớp 2 pha", trong đó việc phân tích cú pháp của các thành viên lớp ngoài phụ thuộc vào các lớp bên trong bị trì hoãn cho đến khi các lớp bên trong đã được hình thành đầy đủ?
TemplateRex

4
@aschepler Có, chẩn đoán ở đây không tốt lắm. Tôi đã nộp llvm.org/PR16550 cho việc đó.
Richard Smith

@Cornstalks Tôi đã phát hiện ra sự cố này khi triển khai trình khởi tạo cho các thành viên dữ liệu không tĩnh trong Clang.
Richard Smith

0

Có lẽ đây là vấn đề:

§12.1 5. Một hàm tạo mặc định được mặc định và không được định nghĩa là đã xóa được định nghĩa ngầm khi nó được sử dụng (3.2) để tạo một đối tượng thuộc loại lớp của nó (1.8) hoặc khi nó được mặc định rõ ràng sau khai báo đầu tiên của nó

Vì vậy, hàm tạo mặc định được tạo ra khi được tra cứu lần đầu, nhưng việc tra cứu sẽ thất bại vì A không được xác định hoàn toàn và B bên trong A do đó sẽ không được tìm thấy.


Tôi không chắc về điều đó "do đó". Rõ ràng B bkhông phải là một vấn đề và việc tìm kiếm các phương thức rõ ràng / một hàm tạo được khai báo rõ ràng Bkhông phải là một vấn đề. Vì vậy, thật tuyệt khi xem một số định nghĩa về lý do tại sao việc tra cứu nên tiến hành khác ở đây để " không tìm thấy Bbên trong A" chỉ trong một trường hợp này mà không phải các trường hợp khác, trước khi chúng ta có thể tuyên bố mã là bất hợp pháp vì lý do này.
moonshadow,

Tôi đã tìm thấy các từ trong tiêu chuẩn mà định nghĩa lớp được coi là hoàn chỉnh trong quá trình khởi tạo trong lớp, bao gồm trong các lớp lồng nhau. Tôi không buồn ghi lại tham chiếu vì nó có vẻ không liên quan.
Mark B

@moonshadow: câu lệnh cho biết các hàm tạo mặc định ngầm được xác định khi sử dụng odr. được định nghĩa rõ ràng sau lần khai báo đầu tiên. Và B b không gọi một constructor, các nhà xây dựng của A gọi constructor của B
fscan

Nếu bất cứ điều gì như thế này là vấn đề, mã sẽ vẫn không hợp lệ nếu bạn xóa =0từ i = 0;. Nhưng nếu không có điều đó =0, mã vẫn hợp lệ và bạn sẽ không tìm thấy một trình biên dịch nào phàn nàn về việc sử dụng B()trong định nghĩa của A.
aschepler
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.