Điều gì ngăn cản sự chồng chéo của các thành viên liền kề trong các lớp học?


12

Hãy xem xét ba structs sau đây :

class blub {
    int i;
    char c;

    blub(const blub&) {}
};

class blob {
    char s;

    blob(const blob&) {}
};

struct bla {
    blub b0;
    blob b1;
};

Trên các nền tảng điển hình intcó 4 byte, kích thước, sự sắp xếp và tổng số đệm 1 như sau:

  struct   size   alignment   padding  
 -------- ------ ----------- --------- 
  blub        8           4         3  
  blob        1           1         0  
  bla        12           4         6  

Không có sự chồng chéo giữa việc lưu trữ của các thành viên blubblobmặc dù kích thước 1 blobvề nguyên tắc có thể "phù hợp" trong phần đệm của blub.

C ++ 20 giới thiệu no_unique_addressthuộc tính, cho phép các thành viên trống liền kề chia sẻ cùng một địa chỉ. Nó cũng cho phép rõ ràng kịch bản được mô tả ở trên về việc sử dụng phần đệm của một thành viên để lưu trữ một thành viên khác. Từ cppreference (nhấn mạnh của tôi):

Chỉ ra rằng thành viên dữ liệu này không cần phải có một địa chỉ khác với tất cả các thành viên dữ liệu không tĩnh khác của lớp. Điều này có nghĩa là nếu thành viên có loại trống (ví dụ: Allocator không trạng thái), trình biên dịch có thể tối ưu hóa nó để chiếm không gian, giống như nếu nó là một cơ sở trống. Nếu thành viên không trống, bất kỳ phần đệm đuôi nào trong đó cũng có thể được sử dụng lại để lưu trữ các thành viên dữ liệu khác.

Thật vậy, nếu chúng ta sử dụng thuộc tính này blub b0, kích thước của blagiọt sẽ giảm 8, do đó, blobthực sự được lưu trữ trong blub như đã thấy trên godbolt .

Cuối cùng, chúng tôi nhận được câu hỏi của tôi:

Văn bản nào trong các tiêu chuẩn (C ++ 11 đến C ++ 20) ngăn chặn sự chồng chéo này mà không có no_unique_address, đối với các đối tượng không thể sao chép tầm thường?

Tôi cần loại trừ các đối tượng có thể sao chép (TC) tầm thường khỏi các đối tượng trên, bởi vì đối với các đối tượng TC, nó được phép chuyển std::memcpytừ đối tượng này sang đối tượng khác, bao gồm các đối tượng con thành viên và nếu lưu trữ bị chồng chéo thì điều này sẽ phá vỡ (vì tất cả hoặc một phần của lưu trữ cho các thành viên liền kề sẽ được ghi đè) 2 .


1 Chúng tôi tính toán phần đệm đơn giản là sự khác biệt giữa kích thước cấu trúc và kích thước của tất cả các thành viên cấu thành của nó, theo cách đệ quy.

2 Đây là lý do tại sao tôi có các hàm tạo sao chép được xác định: để tạo blubblobkhông thể sao chép một cách tầm thường .


Tôi chưa nghiên cứu về nó, nhưng tôi đoán quy tắc "như thể". Nếu không có sự khác biệt có thể quan sát được (một thuật ngữ có ý nghĩa rất cụ thể btw) với máy trừu tượng (đó là những gì mã của bạn được biên dịch lại) thì trình biên dịch có thể thay đổi mã theo cách nó thích.
Jesper Juhl

Khá chắc chắn đây là bản sao của điều này: stackoverflow.com/questions/53837373/
Kẻ

@JesperJuhl - đúng, nhưng tôi đang hỏi tại sao không thể , tại sao không thể , và quy tắc "như thể" thường áp dụng cho cái trước nhưng không có ý nghĩa cho cái sau. Ngoài ra, "như thể" không rõ ràng đối với bố cục cấu trúc thường là mối quan tâm toàn cầu, không phải là địa phương. Cuối cùng, trình biên dịch phải có một bộ quy tắc nhất quán để bố trí, ngoại trừ có lẽ đối với các cấu trúc, nó có thể chứng minh không bao giờ "thoát".
BeeOnRope

1
@BeeOnRope Tôi không thể trả lời câu hỏi của bạn, xin lỗi. Đó là lý do tại sao tôi chỉ đăng một bình luận và không trả lời. Những gì bạn nhận được trong nhận xét đó là phỏng đoán tốt nhất của tôi đối với một lời giải thích, nhưng tôi không biết câu trả lời (rất thích tìm hiểu bản thân - đó là lý do tại sao bạn có một upvote).
Jesper Juhl

1
@NicolBolas - bạn đang trả lời đúng câu hỏi phải không? Đây không phải là về việc phát hiện các bản sao an toàn hoặc bất cứ điều gì khác. Thay vào đó tôi tò mò tại sao đệm không thể được sử dụng lại giữa các thành viên. Trong mọi trường hợp, bạn đã sai: có thể sao chép tầm thường là một thuộc tính của loại và luôn luôn có. Tuy nhiên, để sao chép một cách an toàn một đối tượng, cả hai phải có loại TC (thuộc tính của loại) và không phải là đối tượng có khả năng chồng chéo (thuộc tính của đối tượng, mà tôi đoán là nơi bạn nhầm lẫn). Vẫn không biết tại sao chúng ta đang nói về các bản sao ở đây tho.
BeeOnRope

Câu trả lời:


1

Tiêu chuẩn này cực kỳ im lặng khi nói về mô hình bộ nhớ và không rõ ràng lắm về một số thuật ngữ mà nó sử dụng. Nhưng tôi nghĩ rằng tôi đã tìm thấy một cuộc tranh luận làm việc (có thể hơi yếu)

Đầu tiên, chúng ta hãy tìm hiểu những gì thậm chí là một phần của một đối tượng. [basic.types] / 4 :

Biểu diễn đối tượng của một đối tượng kiểu Tlà chuỗi các N unsigned charđối tượng được đưa lên bởi đối tượng của kiểu T, trong đó Nbằng sizeof(T). Biểu diễn giá trị của một đối tượng kiểu Tlà tập hợp các bit tham gia biểu diễn một giá trị của kiểu T. Các bit trong biểu diễn đối tượng không phải là một phần của biểu diễn giá trị là các bit đệm.

Vì vậy, đại diện đối tượng b0bao gồm các sizeof(blub) unsigned charđối tượng, vì vậy 8 byte. Các bit đệm là một phần của đối tượng.

Không đối tượng nào có thể chiếm không gian của người khác nếu nó không phải là một lồng trong nó [basic. Life] /1.5 :

Thời gian tồn tại của một đối tượng okiểu Tkết thúc khi:

[...]

(1.5) bộ lưu trữ mà đối tượng chiếm giữ được giải phóng hoặc được sử dụng lại bởi một đối tượng không được lồng trong o([intro.object]).

Vì vậy, thời gian tồn tại b0sẽ kết thúc, khi bộ nhớ bị chiếm dụng sẽ được sử dụng lại bởi một đối tượng khác, tức là b1. Tôi đã không kiểm tra điều đó nhưng tôi nghĩ rằng các tiêu chuẩn bắt buộc rằng tiểu dự án của một đối tượng còn sống cũng nên tồn tại (và tôi không thể tưởng tượng điều này sẽ hoạt động khác nhau như thế nào).

Vì vậy, lưu trữ b0 chiếm có thể không được sử dụng b1. Tôi đã không tìm thấy định nghĩa về "chiếm" trong tiêu chuẩn, nhưng tôi nghĩ rằng một cách giải thích hợp lý sẽ là "một phần của biểu diễn đối tượng". Trong biểu diễn đối tượng mô tả trích dẫn, các từ "chiếm" được sử dụng 1 . Ở đây, đây sẽ là 8 byte, vì vậy blacần ít nhất một byte nữa b1.

Đặc biệt đối với các tiểu dự án (vì vậy trong số các thành viên dữ liệu không tĩnh khác) cũng có quy định [intro.object] / 9 (nhưng điều này đã được thêm vào với C ++ 20, thx @BeeOnRope)

Hai đối tượng có tuổi thọ chồng chéo không phải là trường bit có thể có cùng một địa chỉ nếu một đối tượng được lồng vào nhau hoặc nếu ít nhất một đối tượng là một tiểu thể có kích thước bằng 0 và chúng có các loại khác nhau; mặt khác, chúng có địa chỉ riêng biệt và chiếm các byte lưu trữ khác nhau .

(nhấn mạnh của tôi) Ở đây một lần nữa, chúng ta có một vấn đề là "chiếm" không được xác định và một lần nữa tôi sẽ tranh luận để lấy các byte trong biểu diễn đối tượng. Lưu ý rằng có một chú thích cho [basic.memobj] / chú thích này 29

Theo quy tắc của As-if, nếu việc triển khai được phép lưu trữ hai đối tượng tại cùng một địa chỉ máy hoặc hoàn toàn không lưu trữ một đối tượng nếu chương trình không thể quan sát được sự khác biệt ([intro.execut]).

Điều này có thể cho phép trình biên dịch phá vỡ điều này nếu nó có thể chứng minh rằng không có tác dụng phụ có thể quan sát được. Tôi nghĩ rằng điều này khá phức tạp đối với một thứ cơ bản như bố cục đối tượng. Có lẽ đó là lý do tại sao việc tối ưu hóa này chỉ được thực hiện khi người dùng cung cấp thông tin rằng không có lý do gì để có các đối tượng rời rạc bằng cách thêm [no_unique_address]thuộc tính.

tl; dr: Đệm có thể là một phần của đối tượng và các thành viên phải rời rạc.


1 Tôi không thể cưỡng lại việc thêm một tài liệu tham khảo có thể có nghĩa là chiếm lấy: Từ điển không sửa đổi của Webster, G. & C. Merriam, 1913 (nhấn mạnh của tôi)

  1. Để giữ, hoặc điền, kích thước của; để chiếm phòng hoặc không gian của; để che hoặc điền; Như, trại chiếm năm mẫu đất. Ngài J. Herschel.

Thu thập thông tin tiêu chuẩn nào sẽ được hoàn thành mà không thu thập thông tin từ điển?


2
Đối với tôi, phần "chiếm các byte lưu trữ khác nhau" là đủ, đối với tôi - nhưng từ ngữ này chỉ được thêm vào trong C ++ 20 như một phần của thay đổi được thêm vào no_unique_address. Nó để lại tình huống trước C ++ 20 ít rõ ràng hơn. Tôi không hiểu lý do của bạn dẫn đến "Không có đối tượng nào có thể chiếm không gian của người khác nếu nó không được lồng trong nó" từ basic.life/1.5, đặc biệt là cách lấy từ "bộ lưu trữ mà đối tượng chiếm giữ được giải phóng" đến "không đối tượng nào có thể chiếm chỗ của người khác".
BeeOnRope

1
Tôi đã thêm một làm rõ nhỏ vào đoạn đó. Tôi hy vọng điều đó làm cho nó dễ hiểu hơn. Nếu không tôi sẽ xem xét lại vào ngày mai, ngay bây giờ là khá muộn đối với tôi.
n314159

"Hai đối tượng có tuổi thọ chồng lấp không phải là trường bit có thể có cùng một địa chỉ nếu một đối tượng được lồng vào nhau hoặc nếu ít nhất một đối tượng có kích thước bằng 0 và chúng thuộc các loại khác nhau" 2 đối tượng có tuổi thọ chồng chéo, của cùng loại, có cùng địa chỉ .
Luật sư ngôn ngữ

Xin lỗi, bạn có thể giải thích? Bạn đang trích dẫn một trích dẫn tiêu chuẩn từ câu trả lời của tôi và đưa ra một ví dụ mâu thuẫn một chút với điều đó. Tôi không chắc đây có phải là một nhận xét về câu trả lời của tôi không và nếu đó là những gì nó nên nói với tôi. Về ví dụ của bạn, tôi sẽ nói rằng người ta phải xem xét các phần khác của tiêu chuẩn (có một đoạn về một mảng char không dấu cung cấp lưu trữ cho một đối tượng khác, một cái gì đó liên quan đến tối ưu hóa cơ sở có kích thước bằng 0 và vẫn cần xem xét thêm nếu vị trí mới có phụ cấp đặc biệt, tất cả những điều tôi không nghĩ là có liên quan đến ví dụ của OP)
n314159

@ n314159 Tôi nghĩ từ ngữ này có thể bị lỗi.
Luật sư ngôn ngữ
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.