Tại sao tối ưu hóa cơ sở trống bị cấm khi lớp cơ sở trống cũng là một biến thành viên?


14

Tối ưu hóa cơ sở trống là tuyệt vời. Tuy nhiên, nó đi kèm với các hạn chế sau:

Tối ưu hóa cơ sở trống bị cấm nếu một trong các lớp cơ sở trống cũng là loại hoặc cơ sở của loại thành viên dữ liệu không tĩnh đầu tiên, do hai tiểu dự án cơ sở cùng loại được yêu cầu có các địa chỉ khác nhau trong biểu diễn đối tượng thuộc loại có nguồn gốc nhất.

Để giải thích hạn chế này, hãy xem xét các mã sau đây. Ý static_assertchí thất bại. Trong khi đó, việc thay đổi Foohoặc Barthay thế kế thừa từ Base2sẽ tránh được lỗi:

#include <cstddef>

struct Base  {};
struct Base2 {};

struct Foo : Base {};

struct Bar : Base {
    Foo foo;
};

static_assert(offsetof(Bar,foo)==0,"Error!");

Tôi hiểu hành vi này hoàn toàn. Điều tôi không hiểu là tại sao hành vi cụ thể này tồn tại . Nó rõ ràng đã được thêm vào vì một lý do, vì nó là một bổ sung rõ ràng, không phải là một giám sát. Một lý do cho việc này là gì?

Cụ thể, tại sao hai tiểu dự án cơ sở phải được yêu cầu có địa chỉ khác nhau? Ở trên, Barlà một loại và foolà một biến thành viên của loại đó. Tôi không thấy lý do tại sao lớp cơ sở của Barvấn đề đối với lớp cơ sở thuộc loại foohoặc ngược lại.

Thật vậy, tôi nếu có bất cứ điều gì, tôi sẽ mong đợi đó &foogiống như địa chỉ của Barcá thể có chứa nó vì nó được yêu cầu trong các tình huống khác (1) . Rốt cuộc, tôi không làm bất cứ điều gì lạ mắt với virtualsự kế thừa, các lớp cơ sở trống bất kể và việc biên dịch với các Base2chương trình cho thấy không có gì phá vỡ trong trường hợp cụ thể này.

Nhưng rõ ràng lý do này là không chính xác bằng cách nào đó, và có những tình huống khác mà giới hạn này sẽ được yêu cầu.

Giả sử câu trả lời nên dành cho C ++ 11 hoặc mới hơn (Tôi hiện đang sử dụng C ++ 17).

(1) Lưu ý: EBO đã được nâng cấp trong C ++ 11 và đặc biệt trở thành bắt buộc đối với StandardLayoutTypes (mặc dù Bar, ở trên, không phải là a StandardLayoutType).


4
Làm thế nào để lý do bạn trích dẫn (" vì hai tiểu dự án cơ sở cùng loại được yêu cầu phải có địa chỉ khác nhau ") bị thiếu? Các đối tượng khác nhau cùng loại được yêu cầu phải có địa chỉ riêng biệt và yêu cầu này đảm bảo chúng tôi không phá vỡ quy tắc đó. Nếu có sản phẩm nào tối ưu hóa cơ sở áp dụng ở đây, chúng ta có thể có Base *a = new Bar(); Base *b = a->foo;với a==b, nhưng abrõ ràng đối tượng khác nhau (có lẽ có ghi đè phương pháp ảo khác nhau).
Toby Speight

1
Câu trả lời của luật sư ngôn ngữ là trích dẫn các phần có liên quan của thông số kỹ thuật. Và dường như bạn đã biết về điều đó.
Ded repeatator

3
Tôi không chắc tôi hiểu loại câu trả lời bạn đang tìm kiếm ở đây. Mô hình đối tượng C ++ là gì. Hạn chế là có bởi vì mô hình đối tượng yêu cầu nó. Bạn đang tìm kiếm gì hơn thế nữa?
Nicol Bolas

@TobySpeight Các đối tượng khác nhau cùng loại được yêu cầu phải có địa chỉ riêng biệt Có thể dễ dàng phá vỡ quy tắc này trong một chương trình có hành vi được xác định rõ.
Luật sư ngôn ngữ

@TobySpeight Không, tôi không có ý nói rằng bạn đã quên nói về cuộc đời: "Đối tượng khác nhau cùng loại với cuộc sống của họ " . Có thể có nhiều đối tượng cùng loại, tất cả còn sống, tại cùng một địa chỉ. Có ít nhất 2 lỗi trong cách diễn đạt cho phép điều này.
Luật sư ngôn ngữ

Câu trả lời:


4

Ok, có vẻ như tôi đã có lỗi mọi lúc, vì đối với tất cả các ví dụ của tôi, cần phải tồn tại một vtable cho đối tượng cơ sở, điều này sẽ ngăn chặn tối ưu hóa cơ sở trống để bắt đầu. Tôi sẽ để các ví dụ đứng vững vì tôi nghĩ rằng họ đưa ra một số ví dụ thú vị về lý do tại sao các địa chỉ duy nhất thường là một điều tốt để có.

Đã nghiên cứu sâu hơn về vấn đề này, không có lý do kỹ thuật nào để tối ưu hóa lớp cơ sở trống bị vô hiệu hóa khi thành viên đầu tiên cùng loại với lớp cơ sở trống. Đây chỉ là một thuộc tính của mô hình đối tượng C ++ hiện tại.

Nhưng với C ++ 20 sẽ có một thuộc tính mới [[no_unique_address]]cho trình biên dịch biết rằng một thành viên dữ liệu không tĩnh có thể không cần một địa chỉ duy nhất (về mặt kỹ thuật, nó có khả năng chồng lấp [intro.object] / 7 ).

Điều này ngụ ý rằng (nhấn mạnh của tôi)

Thành viên dữ liệu không tĩnh có thể chia sẻ địa chỉ của thành viên dữ liệu không tĩnh khác hoặc địa chỉ của lớp cơ sở , [...]

do đó người ta có thể "kích hoạt lại" tối ưu hóa lớp cơ sở trống bằng cách cung cấp cho thành viên dữ liệu đầu tiên thuộc tính [[no_unique_address]]. Tôi đã thêm một ví dụ ở đây cho thấy cách thức này (và tất cả các trường hợp khác tôi có thể nghĩ ra) hoạt động.

Ví dụ sai về các vấn đề thông qua điều này

Vì dường như một lớp trống có thể không có các phương thức ảo, hãy để tôi thêm một ví dụ thứ ba:

int stupid_method(Base *b) {
  if( dynamic_cast<Foo*>(b) ) return 0;
  if( dynamic_cast<Bar*>(b) ) return 1;
  return 2;
}

Bar b;
stupid_method(&b);  // Would expect 0
stupid_method(&b.foo); //Would expect 1

Nhưng hai cuộc gọi cuối cùng là như nhau.

Các ví dụ cũ (Có lẽ không trả lời câu hỏi vì các lớp trống có thể không chứa các phương thức ảo, có vẻ như vậy)

Xem xét trong mã của bạn ở trên (có thêm các hàm hủy ảo) ví dụ sau

void delBase(Base *b) {
    delete b;
}

Bar *b = new Bar;
delBase(b); // One would expect this to be absolutely fine.
delBase(&b->foo); // Whoaa, we shouldn't delete a member variable.

Nhưng trình biên dịch nên phân biệt hai trường hợp này như thế nào?

Và có lẽ một chút ít giả tạo:

struct Base { 
  virtual void hi() { std::cout << "Hello\n";}
};

struct Foo : Base {
  void hi() override { std::cout << "Guten Tag\n";}
};

struct Bar : Base {
    Foo foo;
};

Bar b;
b.hi() // Hello
b.foo.hi() // Guten Tag
Base *a = &b;
Base *z = &b.foo;
a->hi() // Hello
z->hi() // Guten Tag

Nhưng hai cái cuối cùng giống nhau nếu chúng ta có tối ưu hóa lớp cơ sở trống!


1
Tuy nhiên, người ta có thể lập luận rằng cuộc gọi thứ hai có hành vi không xác định. Vì vậy, trình biên dịch không phải phân biệt bất cứ điều gì.
Người kể chuyện - Unslander Monica

1
Một lớp học với bất kỳ thành viên ảo nào không trống, vì vậy không liên quan ở đây!
Ded repeatator

1
@Ded repeatator Bạn có một trích dẫn tiêu chuẩn về điều đó? Cppref nói với chúng ta rằng một lớp trống là "một lớp hoặc cấu trúc không có thành viên dữ liệu không tĩnh".
n314159

1
@ n314159 std::is_emptytrên cppreference phức tạp hơn nhiều. Tương tự từ dự thảo hiện nay trên eel.is .
Ded repeatator

2
Bạn không thể dynamic_castkhi nó không đa hình (với các ngoại lệ nhỏ không liên quan ở đây).
TC
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.