Tiếp cận thành viên công đoàn không hoạt động và hành vi không xác định?


129

Tôi có ấn tượng rằng việc truy cập một unionthành viên khác ngoài bộ cuối cùng là UB, nhưng dường như tôi không thể tìm thấy một tài liệu tham khảo chắc chắn (ngoài các câu trả lời khẳng định đó là UB nhưng không có bất kỳ sự hỗ trợ nào từ tiêu chuẩn).

Vì vậy, nó là hành vi không xác định?


3
C99 (và tôi tin rằng C ++ 11 cũng vậy) rõ ràng cho phép loại hình với các hiệp hội. Vì vậy, tôi nghĩ rằng nó thuộc hành vi "thực hiện được xác định".
Bí ẩn

1
Tôi đã sử dụng nó nhiều lần để chuyển đổi từ int int thành char. Vì vậy, tôi chắc chắn biết nó không phải là không xác định. Tôi đã sử dụng nó trên trình biên dịch Sun CC. Vì vậy, nó vẫn có thể phụ thuộc vào trình biên dịch.
go4sri

42
@ go4sri: Rõ ràng, bạn không biết ý nghĩa của hành vi không được xác định. Thực tế là nó dường như hoạt động với bạn trong một số trường hợp không mâu thuẫn với sự không xác định của nó.
Benjamin Lindley


4
@Mysticial, bài đăng blog bạn liên kết đến rất cụ thể về C99; Câu hỏi này chỉ được gắn thẻ cho C ++.
davmac

Câu trả lời:


131

Sự nhầm lẫn là C rõ ràng cho phép loại bỏ thông qua một liên minh, trong khi C ++ () không có sự cho phép như vậy.

6.5.2.3 Cơ cấu và đoàn viên

95) Nếu thành viên được sử dụng để đọc nội dung của đối tượng hợp nhất không giống với thành viên cuối cùng được sử dụng để lưu trữ một giá trị trong đối tượng, phần thích hợp của biểu diễn đối tượng của giá trị được diễn giải lại dưới dạng đại diện đối tượng trong mới loại như được mô tả trong 6.2.6 (một quy trình đôi khi được gọi là '' loại pucky ''). Đây có thể là một đại diện bẫy.

Tình huống với C ++:

9.5 Liên hiệp [class.union]

Trong một liên minh, nhiều nhất một trong số các thành viên dữ liệu không tĩnh có thể hoạt động bất cứ lúc nào, nghĩa là, giá trị của nhiều nhất một trong số các thành viên dữ liệu không tĩnh có thể được lưu trữ trong một liên kết bất cứ lúc nào.

C ++ sau này có ngôn ngữ cho phép sử dụng các công đoàn có chứa structs với các chuỗi ban đầu phổ biến; tuy nhiên điều này không cho phép loại hình.

Để xác định xem công đoàn type-punning được cho phép trong C ++, chúng ta phải tìm kiếm thêm nữa. Nhớ lại rằng là một tài liệu tham khảo quy phạm cho C ++ 11 (và C99 có ngôn ngữ tương tự như C11 cho phép loại hình liên minh):

3.9 Các loại [basic.types]

4 - Biểu diễn đối tượng của một đối tượng thuộc loại T là chuỗi các đối tượng char không dấu được lấy bởi đối tượng của loại T, trong đó N bằng sizeof (T). Biểu diễn giá trị của một đối tượng là tập hợp các bit chứa giá trị của loại T. Đối với các loại có thể sao chép tầm thường, biểu diễn giá trị là một tập hợp các bit trong biểu diễn đối tượng xác định một giá trị, là một yếu tố riêng biệt của việc triển khai- định nghĩa các giá trị. 42
42) Mục đích là mô hình bộ nhớ của C ++ tương thích với mô hình lập trình ISO / IEC 9899 C.

Nó đặc biệt thú vị khi chúng ta đọc

3.8 Đối tượng trọn đời [basic. Life]

Thời gian tồn tại của một đối tượng loại T bắt đầu khi: - lưu trữ với sự liên kết và kích thước phù hợp cho loại T được lấy và - nếu đối tượng có khởi tạo không tầm thường, việc khởi tạo của nó hoàn tất.

Vì vậy, đối với một kiểu nguyên thủy (mà ipso facto có khởi tạo tầm thường) có trong một liên minh, thời gian tồn tại của đối tượng bao gồm ít nhất là thời gian tồn tại của chính liên minh. Điều này cho phép chúng tôi gọi

3.9.2 Các loại hợp chất [basic.compound]

Nếu một đối tượng của loại T được đặt tại một địa chỉ A, một con trỏ của loại cv T * có giá trị là địa chỉ A được cho là trỏ đến đối tượng đó, bất kể giá trị thu được như thế nào.

Giả sử rằng hoạt động mà chúng ta quan tâm là kiểu xảo quyệt, tức là lấy giá trị của một thành viên công đoàn không hoạt động, và theo như trên, chúng ta có một tham chiếu hợp lệ đến đối tượng được đề cập bởi thành viên đó, hoạt động đó là giá trị chuyển đổi giá trị:

4.1 Chuyển đổi Lvalue-to-rvalue [conv.lval]

Một giá trị của một loại không có chức năng, không phải là mảng Tcó thể được chuyển đổi thành một giá trị. Nếu Tlà một loại không đầy đủ, một chương trình yêu cầu chuyển đổi này không được định dạng. Nếu đối tượng mà glvalue tham chiếu không phải là đối tượng của loại Tvà không phải là đối tượng của loại có nguồn gốc Thoặc nếu đối tượng chưa được khởi tạo, một chương trình yêu cầu chuyển đổi này có hành vi không được thực hiện.

Câu hỏi sau đó là liệu một đối tượng là thành viên công đoàn không hoạt động có được khởi tạo bằng cách lưu trữ cho thành viên công đoàn đang hoạt động hay không. Theo như tôi có thể nói, đây không phải là trường hợp và mặc dù vậy:

  • một liên minh được sao chép vào charbộ lưu trữ mảng và ngược lại (3.9: 2), hoặc
  • một liên minh được tạm dừng sao chép sang một liên minh khác cùng loại (3.9: 3), hoặc
  • một liên kết được truy cập qua các ranh giới ngôn ngữ bởi một yếu tố chương trình tuân thủ ISO / IEC 9899 (cho đến khi được xác định) (3.9: 4 lưu ý 42), sau đó

truy cập vào một liên minh bởi một thành viên không hoạt động được xác định và được xác định để tuân theo biểu diễn đối tượng và giá trị, truy cập mà không có một trong các can thiệp trên là hành vi không xác định. Điều này có ý nghĩa đối với các tối ưu hóa được phép thực hiện trên một chương trình như vậy, vì việc triển khai dĩ nhiên có thể cho rằng hành vi không xác định không xảy ra.

Đó là, mặc dù chúng ta có thể tạo thành một giá trị hợp pháp cho một thành viên công đoàn không hoạt động (đó là lý do tại sao việc giao cho một thành viên không hoạt động mà không xây dựng là ok) nó được coi là không được khởi tạo.


5
3.8 / 1 cho biết thời gian tồn tại của một đối tượng kết thúc khi bộ nhớ của nó được sử dụng lại. Điều đó cho tôi thấy rằng một thành viên không hoạt động trong suốt cuộc đời của công đoàn đã kết thúc vì bộ nhớ của nó đã được sử dụng lại cho thành viên tích cực. Điều đó có nghĩa là bạn bị giới hạn trong cách bạn sử dụng thành viên (3.8 / 6).
bames53

2
Theo cách giải thích đó, mỗi bit bộ nhớ đồng thời chứa các đối tượng thuộc tất cả các loại có thể bắt đầu một cách tầm thường và có sự liên kết phù hợp ... Vì vậy, thời gian tồn tại của bất kỳ loại nào không thể bắt đầu ngay lập tức khi bộ lưu trữ của nó được sử dụng lại cho tất cả các loại khác ( và không khởi động lại vì chúng không thể bắt đầu một cách tầm thường)?
bames53

3
Từ ngữ 4.1 hoàn toàn và hoàn toàn bị phá vỡ và kể từ đó đã được viết lại. Nó không cho phép tất cả các loại điều hoàn toàn hợp lệ: nó không cho phép memcpytriển khai tùy chỉnh (truy cập các đối tượng sử dụng unsigned chargiá trị), nó không cho phép truy cập vào *psau int *p = 0; const int *const *pp = &p;(mặc dù chuyển đổi ngầm định int**thành const int*const*hợp lệ), nó không cho phép truy cập csau struct S s; const S &c = s;. Vấn đề CWG 616 . Liệu từ ngữ mới cho phép nó? Ngoài ra còn có [basic.lval].

2
@Omnifarious: Điều đó sẽ có ý nghĩa, mặc dù cũng cần phải làm rõ (và Tiêu chuẩn C cũng cần làm rõ, btw) ý nghĩa của toán tử đơn nguyên &khi áp dụng cho thành viên công đoàn. Tôi nghĩ rằng con trỏ kết quả nên có thể sử dụng để truy cập thành viên ít nhất cho đến lần tiếp theo sử dụng trực tiếp hoặc gián tiếp tiếp theo của bất kỳ thành viên nào khác, nhưng trong gcc, con trỏ không thể sử dụng được lâu, điều đó đặt ra câu hỏi về điều gì các &nhà điều hành có nghĩa vụ phải trung bình.
supercat

4
Một câu hỏi liên quan đến "Nhắc lại rằng c99 là một tài liệu tham khảo quy phạm cho C ++ 11" Không phải chỉ có liên quan, trong đó tiêu chuẩn c ++ đề cập rõ ràng đến tiêu chuẩn C (ví dụ cho các chức năng của thư viện c)?
MikeMB

28

Tiêu chuẩn C ++ 11 nói theo cách này

9,5 công đoàn

Trong một liên minh, nhiều nhất một trong số các thành viên dữ liệu không tĩnh có thể hoạt động bất cứ lúc nào, nghĩa là, giá trị của nhiều nhất một trong số các thành viên dữ liệu không tĩnh có thể được lưu trữ trong một liên kết bất cứ lúc nào.

Nếu chỉ có một giá trị được lưu trữ, làm thế nào bạn có thể đọc một giá trị khác? Nó không có ở đó.


Tài liệu gcc liệt kê điều này trong phần Thực hiện hành vi được xác định

  • Một thành viên của một đối tượng hợp nhất được truy cập bằng cách sử dụng một thành viên của một loại khác (C90 6.3.2.3).

Các byte có liên quan của biểu diễn của đối tượng được coi là một đối tượng của loại được sử dụng để truy cập. Xem Type-pucky. Đây có thể là một đại diện bẫy.

chỉ ra rằng điều này là không bắt buộc theo tiêu chuẩn C.


2016-01-05: Thông qua các bình luận, tôi đã được liên kết với Báo cáo khiếm khuyết C99 # 283 có thêm một văn bản tương tự như một chú thích cho tài liệu tiêu chuẩn C:

78a) Nếu thành viên được sử dụng để truy cập nội dung của đối tượng hợp nhất không giống với thành viên cuối cùng được sử dụng để lưu trữ một giá trị trong đối tượng, phần thích hợp của biểu diễn đối tượng của giá trị được diễn giải lại dưới dạng đại diện đối tượng trong phần mới loại như được mô tả trong 6.2.6 (một quá trình đôi khi được gọi là "loại pucky"). Đây có thể là một đại diện bẫy.

Không chắc chắn nếu nó làm rõ nhiều mặc dù, xem xét rằng một chú thích không phải là quy tắc cho tiêu chuẩn.


10
@LuchianGrigore: UB không phải là tiêu chuẩn nói là UB, thay vào đó là những gì tiêu chuẩn không mô tả cách hoạt động của nó. Đây chính xác là trường hợp như vậy. Liệu các tiêu chuẩn mô tả những gì xảy ra? Liệu nó nói rằng nó được thực hiện xác định? Không và không. Vì vậy, đó là UB. Ngoài ra, liên quan đến đối số "các thành viên chia sẻ cùng một địa chỉ bộ nhớ", bạn sẽ phải tham khảo các quy tắc răng cưa, điều này sẽ đưa bạn trở lại UB.
Yakov Galka

5
@Luchian: Không rõ ý nghĩa hoạt động là gì, "nghĩa là, giá trị của nhiều nhất một trong số các thành viên dữ liệu không tĩnh có thể được lưu trữ trong một liên minh bất cứ lúc nào."
Benjamin Lindley

5
@LuchianGrigore: Có. Có vô số trường hợp mà tiêu chuẩn không (và không thể) giải quyết. (C ++ là máy ảo hoàn chỉnh Turing nên chưa hoàn thiện.) Vậy thì sao? Nó không giải thích "hoạt động" nghĩa là gì, hãy tham khảo trích dẫn ở trên, sau "đó là".
Yakov Galka

8
@LuchianGrigore: Bỏ sót định nghĩa rõ ràng về hành vi cũng là vô thức hành vi không xác định, theo phần định nghĩa.
jxh

5
@Claudiu Đó là UB vì một lý do khác - nó vi phạm bí danh nghiêm ngặt.
Bí ẩn

18

Tôi nghĩ rằng tiêu chuẩn gần nhất nói rằng hành vi không xác định của nó là nơi nó xác định hành vi cho một liên kết có chứa một chuỗi ban đầu chung (C99, §6.5.2.3 / 5):

Một bảo đảm đặc biệt được thực hiện để đơn giản hóa việc sử dụng các hiệp hội: nếu một liên minh có chứa một số cấu trúc có chung một chuỗi ban đầu (xem bên dưới) và nếu đối tượng kết hợp hiện có một trong các cấu trúc này, thì được phép kiểm tra chung phần ban đầu của bất kỳ ai trong số họ bất cứ nơi nào có thể nhìn thấy một tuyên bố về loại hoàn chỉnh của liên minh. Hai cấu trúc chia sẻ một chuỗi ban đầu chung nếu các thành viên tương ứng có các loại tương thích (và, đối với các trường bit, cùng độ rộng) cho một chuỗi gồm một hoặc nhiều thành viên ban đầu.

C ++ 11 đưa ra các yêu cầu / quyền tương tự tại §9.2 / 19:

Nếu một liên kết bố cục tiêu chuẩn chứa hai hoặc nhiều cấu trúc bố cục tiêu chuẩn có chung một chuỗi ban đầu chung và nếu đối tượng kết hợp bố cục tiêu chuẩn hiện có một trong các cấu trúc bố cục tiêu chuẩn này, thì được phép kiểm tra phần ban đầu chung của bất kỳ của họ. Hai cấu trúc bố cục tiêu chuẩn chia sẻ một chuỗi ban đầu chung nếu các thành viên tương ứng có các loại tương thích bố cục và không phải thành viên nào là trường bit hoặc cả hai đều là trường bit có cùng chiều rộng cho một hoặc nhiều thành viên ban đầu.

Mặc dù không phải quốc gia trực tiếp, những cả hai mang một ý nghĩa mạnh mẽ rằng "kiểm tra" (đọc) một thành viên là "được phép" chỉ nếu 1) nó là (một phần của) các thành viên gần đây nhất bằng văn bản, hoặc 2) là một phần của một ban đầu thường gặp sự nối tiếp.

Đó không phải là một tuyên bố trực tiếp rằng làm khác đi là hành vi không xác định, nhưng đó là điều gần nhất mà tôi biết.


Để thực hiện điều này, bạn cần biết "các loại tương thích bố cục" là gì đối với C ++ hoặc "các loại tương thích" dành cho C.
Michael Anderson

2
@MichaelAnderson: Có và không. Bạn cần phải đối phó với những điều đó khi / nếu bạn muốn chắc chắn liệu có thứ gì đó nằm trong ngoại lệ này hay không - nhưng câu hỏi thực sự ở đây là liệu thứ gì đó rõ ràng nằm ngoài ngoại lệ có thực sự mang lại cho UB hay không. Tôi nghĩ rằng điều đó đủ mạnh ngụ ý ở đây để làm cho ý định rõ ràng, nhưng tôi không nghĩ rằng nó từng được nêu trực tiếp.
Jerry Coffin

Điều "trình tự ban đầu chung" này có thể đã lưu 2 hoặc 3 dự án của tôi từ Thùng Viết lại. Tôi đã cáu kỉnh khi lần đầu tiên đọc về hầu hết các cách sử dụng tuyệt vời unioncủa việc không được xác định, vì tôi đã được ấn tượng bởi một blog cụ thể rằng điều này là ổn, và đã xây dựng một số cấu trúc và dự án lớn xung quanh nó. Bây giờ tôi nghĩ rằng tôi có thể ổn sau tất cả, vì tôi unioncó chứa các lớp có cùng loại ở phía trước
underscore_d

@JerryCoffin, tôi nghĩ rằng bạn đã gợi ý cho cùng một câu hỏi như tôi: nếu cái gì của chúng tôi unionví dụ a uint8_tvà a class Something { uint8_t myByte; [...] };- tôi sẽ cho rằng điều này cũng sẽ áp dụng ở đây, nhưng nó được cho là rất cố ý chỉ cho phép structs. May mắn thay, tôi đã sử dụng những thứ đó thay vì nguyên thủy: O
underscore_d

@underscore_d: Tiêu chuẩn C ít nhất là loại bao gồm câu hỏi đó: "Một con trỏ tới một đối tượng cấu trúc, được chuyển đổi phù hợp, trỏ đến thành viên ban đầu của nó (hoặc nếu thành viên đó là một trường bit, sau đó đến đơn vị mà nó cư trú) , và ngược lại."
Jerry Coffin

12

Một cái gì đó chưa được đề cập bởi các câu trả lời có sẵn là chú thích 37 trong đoạn 21 của mục 6.2.5:

Lưu ý rằng loại tổng hợp không bao gồm loại kết hợp vì một đối tượng có loại kết hợp chỉ có thể chứa một thành viên tại một thời điểm.

Yêu cầu này dường như ngụ ý rõ ràng rằng bạn không được viết thành viên và đọc trong một thành viên khác. Trong trường hợp này, nó có thể là hành vi không xác định do thiếu đặc tả.


Nhiều triển khai tài liệu định dạng lưu trữ và quy tắc bố trí của họ. Một đặc tả như vậy trong nhiều trường hợp ngụ ý tác động của việc đọc lưu trữ của một loại và viết như một loại khác sẽ không có quy tắc nói rằng trình biên dịch không thực sự sử dụng định dạng lưu trữ được xác định của chúng trừ khi mọi thứ được đọc và viết bằng con trỏ của một loại nhân vật.
supercat

-3

Tôi cũng giải thích điều này với một ví dụ.
giả sử chúng ta có liên minh sau:

union A{
   int x;
   short y[2];
};

Tôi cũng giả sử rằng sizeof(int)cho 4 và sizeof(short)cho 2.
khi bạn viết union A a = {10}cũng tạo ra một var mới của loại A, đặt vào đó giá trị 10.

bộ nhớ của bạn sẽ trông như thế: (hãy nhớ rằng tất cả các thành viên công đoàn có cùng một vị trí)

       | x |
       | y [0] | y [1] |
       -----------------------------------------
   a-> | 0000 0000 | 0000 0000 | 0000 0000 | 0000 1010 |
       -----------------------------------------

như bạn có thể thấy, giá trị của ax là 10, giá trị của ay 1 là 10 và giá trị của ay [0] là 0.

Bây giờ, điều gì xảy ra nếu tôi làm điều này?

a.y[0] = 37;

trí nhớ của chúng ta sẽ như thế này:

       | x |
       | y [0] | y [1] |
       -----------------------------------------
   a-> | 0000 0000 | 0010 0101 | 0000 0000 | 0000 1010 |
       -----------------------------------------

điều này sẽ biến giá trị của ax thành 2424842 (theo số thập phân).

bây giờ, nếu liên minh của bạn có số float, hoặc gấp đôi, bản đồ bộ nhớ của bạn sẽ trở nên lộn xộn hơn, do cách bạn lưu trữ các con số chính xác. thêm thông tin bạn có thể nhận được ở đây .


18
:) Đây không phải là những gì tôi yêu cầu. Tôi biết những gì xảy ra trong nội bộ. Tôi biết nó hoạt động. Tôi hỏi liệu nó có trong tiêu chuẩn không.
Luchian Grigore
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.