8

Tôi đang đối mặt với sự nhầm lẫn về quy tắc bí danh nghiêm ngặt C++ và các hàm ý có thể có của nó. Xét đoạn mã sau:
C/C++ bí danh nghiêm ngặt, đối tượng tồn tại và các trình biên dịch hiện đại

int main() { 
    int32_t a = 5; 
    float* f = (float*)(&a); 
    *f = 1.0f; 

    int32_t b = a; // Probably not well-defined? 
    float g = *f; // What about this? 
} 

Nhìn vào C++ thông số kỹ thuật, phần 3.10.10, về mặt kỹ thuật không ai trong số các mã được dường như vi phạm "răng cưa quy tắc" cho có:

Nếu một chương trình cố gắng truy cập vào các giá trị được lưu trữ của một đối tượng thông qua một vế trái của khác hơn là một trong các loại sau đây hành vi được unde fi định nghĩa:
... một danh sách các loại accessor đủ điều kiện ...

  • *f = 1.0f; không vi phạm quy tắc vì không có quyền truy cập vào giá trị được lưu trữ, tức là tôi chỉ ghi vào bộ nhớ thông qua con trỏ. Tôi không đọc từ bộ nhớ hoặc cố gắng diễn giải một giá trị ở đây.
  • Dòng int32_t b = a; không vi phạm các quy tắc vì tôi đang truy cập thông qua loại ban đầu.
  • Dòng float g = *f; không vi phạm các quy tắc cho cùng một lý do.

Trong another thread, thành viên CortAmmon thực sự làm cho cùng một điểm trong một phản ứng, và nói thêm rằng bất kỳ hành vi không xác định có thể phát sinh thông qua viết để đối tượng sống, như trong *f = 1.0f;, sẽ được hạch toán theo định nghĩa của tiêu chuẩn " đối tượng tồn tại "(có vẻ không quan trọng đối với các loại POD).

BAO GIỜ: Có nhiều bằng chứng bằng chứng trên internet ở trên mã sẽ tạo UB trên các trình biên dịch hiện đại. Xem ví dụ: herehere.
Luận điểm trong hầu hết các trường hợp là trình biên dịch được tự do cân nhắc &af không phải là răng cưa lẫn nhau và do đó miễn phí lên lịch lại hướng dẫn.

Câu hỏi lớn bây giờ là nếu hành vi trình biên dịch như vậy thực sự sẽ là "quá giải thích" của tiêu chuẩn.
Thời gian duy nhất mà tiêu chuẩn nói về "bí danh" cụ thể là trong chú thích ở 3.10.10, trong đó rõ ràng rằng đó là các quy tắc chi phối răng cưa.
Như tôi đã đề cập trước đó, tôi không thấy bất kỳ mã nào ở trên vi phạm tiêu chuẩn, nhưng nó sẽ được cho là bất hợp pháp bởi một số lượng lớn người (và có thể là người biên dịch).

Tôi thực sự sẽ đánh giá cao một số giải thích tại đây.

Cập nhật nhỏ:
Là thành viên BenVoigt chỉ ra một cách chính xác, int32_t có thể không phù hợp với float trên một số nền tảng để mã nhất định có thể là vi phạm của "lưu trữ đủ liên kết và kích thước" quy tắc. Tôi muốn nêu rõ rằng int32_t được chọn cố ý để căn chỉnh với float trên hầu hết các nền tảng và giả định cho câu hỏi này là các loại thực sự phù hợp.

Cập nhật nhỏ # 2:
Như một số thành viên đã chỉ ra, dòng int32_t b = a; có lẽ là vi phạm tiêu chuẩn, mặc dù không phải với sự chắc chắn tuyệt đối. Tôi đồng ý với quan điểm đó và, không thay đổi bất kỳ khía cạnh nào của câu hỏi, yêu cầu người đọc loại trừ dòng đó khỏi tuyên bố của tôi ở trên rằng không có mã nào vi phạm tiêu chuẩn.

+2

Viết là một dạng truy cập nhiều như đọc. –

+2

Nếu điều đó đúng thì tiêu chuẩn sẽ nói "truy cập bộ nhớ của một đối tượng thông qua ...". Nhưng những gì nó nói là "truy cập các giá trị được lưu trữ", mà không phải là những gì mã không. – rsp1984

+1

Trong số các vấn đề khác với mã này, bạn không bao giờ có một đối tượng kiểu 'float', bởi vì bạn không bao giờ" thu được dung lượng đủ kích thước và căn chỉnh chính xác ". 'a' có kích thước và căn chỉnh cho' int', không phải 'float', và một số nền tảng sẽ thực sự cho bạn biết (để đặt nó vui lòng). –

Trả lời

5

Bạn đang sai trong điểm bullet thứ ba của mình (và có thể là điểm đầu tiên).

Bạn tuyên bố "Đường kẻ float g = *f; không vi phạm quy tắc vì cùng lý do.", Trong đó "lý do tương tự" (hơi mơ hồ) có vẻ là "truy cập thông qua loại ban đầu". Nhưng đó không phải là những gì bạn đang làm. Bạn đang truy cập vào số int32_t (có tên a) thông qua một loại giá trị theo số float (được lấy từ biểu thức *f). Vì vậy, bạn đang vi phạm tiêu chuẩn.

Tôi cũng tin (nhưng không chắc chắn về điều này) lưu trữ giá trị là quyền truy cập vào (đó) giá trị được lưu trữ, vì vậy ngay cả *f = 1.0f; cũng vi phạm các quy tắc.

+0

Các trạng thái tiêu chuẩn cho biết tuổi thọ của vật thể kết thúc khi bộ nhớ của nó bị khử phân bổ hoặc tái sử dụng. Đó là những gì tôi đang làm bằng '* f = 1.0f;'. Do đó đối tượng đang được xem xét là kiểu float và dòng 'float g = * f;' có thể được coi là hợp pháp. – rsp1984

+1

Nhưng "đối tượng" trong trường hợp này là 'a', không phải là' 5' xảy ra là "trong" 'a'. Vì vậy, nó vẫn là UB. –

+1

Nhưng 'a' đã chết sau' * f = 1.0f; 'phải không? – rsp1984

2

Tôi nghĩ rằng tuyên bố này là không chính xác:

Dòng int32_t b = a; không vi phạm các quy tắc vì tôi đang truy cập thông qua loại ban đầu của nó.

Đối tượng được lưu trữ tại vị trí &a bây giờ là một phao, vì vậy bạn đang cố gắng truy cập giá trị được lưu trữ của phao thông qua một loại giá trị sai.

1

Có một số sự mơ hồ đáng kể trong đặc điểm kỹ thuật về tuổi thọ và quyền truy cập đối tượng, nhưng dưới đây là một số vấn đề với mã theo thông số của tôi.

float* f = (float*)(&a); 

này thực hiện một reinterpret_cast và chừng nào float không đòi hỏi sự liên kết chặt chẽ hơn int32_t sau đó bạn có thể đúc các giá trị kết quả trở lại một int32_t* và bạn sẽ nhận được con trỏ gốc. Sử dụng kết quả không được định nghĩa khác trong mọi trường hợp.

*f = 1.0f; 

Giả sử *f bí danh với a (và rằng lưu trữ cho một int32_t có sự liên kết và kích thước thích hợp cho một float) sau đó dòng trên kết thúc cuộc đời của đối tượng int32_t và đặt một đối tượng float vào chỗ của nó:

Thời gian tồn tại của đối tượng loại T bắt đầu khi: lưu trữ với căn chỉnh và kích cỡ phù hợp với loại T và nếu đối tượng có khởi tạo không nhỏ, khởi tạo của nó hoàn tất.

Thời gian tồn tại của đối tượng loại T kết thúc khi: [...] bộ nhớ mà đối tượng chiếm được sử dụng lại hoặc được giải phóng.

— 3.8 Tuổi thọ của đối tượng [cơ bản.cuộc sống]/1

Chúng tôi đang tái sử dụng lưu trữ, nhưng nếu int32_t có kích thước và sự liên kết yêu cầu tương tự sau đó nó có vẻ như một float luôn tồn tại trong cùng một vị trí (kể từ khi lưu trữ được 'lấy'). Có lẽ chúng ta có thể tránh sự mơ hồ này bằng cách thay đổi dòng này thành new (f) float {1.0f};, vì vậy chúng ta biết rằng đối tượng float có thời gian bắt đầu vào hoặc trước khi hoàn thành quá trình khởi tạo.

Ngoài ra, 'truy cập' không nhất thiết có nghĩa là 'đã đọc'. Nó có thể có nghĩa là cả đọc và viết. Vì vậy, việc ghi được thực hiện bởi *f = 1.0f; có thể được coi là 'truy cập giá trị được lưu trữ' bằng cách ghi đè lên nó, trong trường hợp này cũng là một vi phạm bí danh.

Bây giờ giả định rằng một đối tượng phao tồn tại và tuổi thọ các int32_t đối tượng đã kết thúc:

int32_t b = a; 

Mã này truy cập giá trị được lưu trữ của một đối tượng float qua một glvalue với loại int32_t, và rõ ràng là một sự vi phạm răng cưa . Chương trình có hành vi không xác định theo 3.10/10.

float g = *f; 

Giả sử rằng int32_t có sự liên kết và kích thước yêu cầu đúng, và rằng con trỏ f đã thu được trong một cách mà cho phép sử dụng của nó được xác định rõ ràng, thì đây nên truy cập về mặt pháp lý đối tượng float đã được khởi tạo với 1.0f.

+0

Cảm ơn bạn, nhận được ý kiến ​​về vấn đề này thực sự hữu ích. Tôi đồng ý rằng 'int32_t b = a;' có thể là một vi phạm bí danh mặc dù tôi có thể đã nêu khác trong câu hỏi. Đối với những sự mơ hồ còn lại, có cách nào để liên lạc với những người đã viết tiêu chuẩn và yêu cầu làm rõ không? – rsp1984

+0

@RafaelSpring http://isocpp.org/std/submit-a-library-issue – bames53

0

Tôi đã học được cách khó mà trích dẫn 6.5.7 từ tiêu chuẩn C99 là vô ích mà không cần nhìn vào 6.5.6. Xem this answer để biết các báo giá có liên quan.

6.5.6 làm rõ rằng loại đối tượng có thể, trong một số trường hợp nhất định, thay đổi nhiều lần trong suốt cuộc đời của nó. Giá trị này có thể là loại giá trị gần đây nhất là được viết. Điều này thực sự hữu ích.

Chúng tôi cần phân biệt giữa "loại đã khai báo" và "loại hiệu quả". Biến cục bộ hoặc toàn cục tĩnh, có kiểu khai báo. Bạn đang bị mắc kẹt với kiểu đó, tôi nghĩ, suốt đời của đối tượng đó. Bạn có thể đọc từ đối tượng bằng cách sử dụng char *, nhưng "loại hiệu quả" không thay đổi không may.

Nhưng bộ nhớ được trả lại bởi malloc có "không có loại khai báo". Điều này sẽ vẫn đúng cho đến khi nó là free d. Nó sẽ không bao giờ có một kiểu khai báo, nhưng kiểu hiệu quả của nó có thể thay đổi theo 6.5.6, luôn lấy kiểu ghi gần đây nhất.

Vì vậy, đây là hợp pháp:

int main() { 
    void * vp = malloc(sizeof(int)+sizeof(float)); // it's big enough, 
        // and malloc will look after alignment for us. 
    int32_t *ap = vp; 
    *ap = 5;  // make int32_t the 'effective type' 
    float* f = vp; 
    *f = 1.0f; // this (legally) changes the effective type. 

    // int32_t b = *ap; // Not defined, because the 
          // effective type is wrong 
    float g = *f; // OK, because the effective type is (currently) correct. 
} 

Vì vậy, về cơ bản, văn bản cho một không gian -ed malloc là một cách hợp lệ để thay đổi loại của nó. Nhưng tôi đoán rằng điều đó không cho chúng ta một cách để nhìn vào sự tồn tại từ trước thông qua "thấu kính" của một loại mới, điều này có thể thú vị; nó là không thể trừ khi, tôi nghĩ rằng, chúng tôi sử dụng các trường hợp ngoại lệ char* khác nhau để xem dữ liệu của loại "sai".

Các vấn đề liên quan