(Tại sao) đang sử dụng một hành vi không xác định của biến chưa được khởi tạo?


82

Nếu tôi có:

unsigned int x;
x -= x;

Rõ ràng là x phải bằng 0 sau biểu thức này, nhưng ở mọi nơi tôi nhìn, họ nói rằng hành vi của mã này là không xác định, không chỉ đơn thuần là giá trị của x(cho đến trước phép trừ).

Hai câu hỏi:

  • hành vi của mã này thực sự không xác định?
    (Ví dụ: Có thể xảy ra sự cố mã [hoặc tệ hơn] trên một hệ thống tuân thủ?)

  • Nếu vậy, tại sao C lại nói rằng hành vi là không xác định, khi nó hoàn toàn rõ ràng rằng xở đây phải bằng 0?

    tức là Lợi thế mang lại khi không xác định hành vi ở đây là gì?

Rõ ràng, trình biên dịch có thể đơn giản sử dụng bất kỳ giá trị rác nào mà nó cho là "tiện dụng" bên trong biến, và nó sẽ hoạt động như dự định ... có gì sai với cách tiếp cận đó?



3
Lợi thế của việc xác định một trường hợp đặc biệt cho hành vi ở đây là gì? Chắc chắn rồi, hãy để tất cả làm cho các chương trình và thư viện của chúng ta lớn hơn và chậm hơn vì @Mehrdad muốn tránh khởi tạo một biến trong một trường hợp cụ thể và hiếm gặp.
Paul Tomblin

9
@ W'rkncacnter Tôi không đồng ý với việc đó là một bản dupe. Bất kể giá trị của nó là bao nhiêu, OP dự kiến ​​nó sẽ bằng 0 sau đó x -= x. Câu hỏi đặt ra là tại sao việc truy cập các giá trị chưa được khởi tạo lại là UB.
Mysticial

6
Thật thú vị khi câu lệnh x = 0; thường được chuyển đổi thành xor x, x trong assembly. Nó gần giống như những gì bạn đang cố gắng làm ở đây, nhưng với xor thay vì phép trừ.
0xFE

1
'tức là Lợi thế mang lại khi không xác định hành vi ở đây là gì? '- Tôi đã nghĩ rằng lợi thế của tiêu chuẩn không liệt kê vô số biểu thức với các giá trị không phụ thuộc vào một hoặc nhiều biến là hiển nhiên. Đồng thời, @Paul, sự thay đổi như vậy đối với tiêu chuẩn sẽ không làm cho các chương trình và thư viện lớn hơn nữa.
Jim Balter

Câu trả lời:


90

Có, hành vi này không được xác định nhưng vì những lý do khác nhau mà hầu hết mọi người đều biết.

Đầu tiên, việc sử dụng một giá trị đơn nguyên không phải là hành vi không xác định, mà giá trị này chỉ đơn giản là không xác định. Khi đó, truy cập vào giá trị này là UB nếu giá trị là biểu diễn bẫy cho kiểu. Các loại không dấu hiếm khi có biểu diễn bẫy, vì vậy bạn sẽ tương đối an toàn khi ở bên đó.

Điều làm cho hành vi không được xác định là một thuộc tính bổ sung của biến của bạn, cụ thể là nó "có thể đã được khai báo với register" đó là địa chỉ của nó không bao giờ được sử dụng. Các biến như vậy được xử lý đặc biệt vì có những kiến ​​trúc có thanh ghi CPU thực có một loại trạng thái bổ sung là "chưa được khởi tạo" và không tương ứng với giá trị trong miền loại.

Chỉnh sửa: Cụm từ liên quan của tiêu chuẩn là 6.3.2.1p2:

Nếu lvalue chỉ định một đối tượng có thời lượng lưu trữ tự động có thể đã được khai báo với lớp lưu trữ đăng ký (chưa bao giờ được sử dụng địa chỉ của nó) và đối tượng đó chưa được khởi tạo (không được khai báo bằng bộ khởi tạo và không có phép gán cho nó trước khi sử dụng ), hành vi là không xác định.

Và để làm rõ hơn, mã sau đây hợp pháp trong mọi trường hợp:

unsigned char a, b;
memcpy(&a, &b, 1);
a -= a;
  • Ở đây địa chỉ của abđược lấy, vì vậy giá trị của chúng chỉ là không xác định.
  • unsigned charkhông bao giờ có các biểu diễn bẫy mà giá trị không xác định chỉ là không xác định, bất kỳ giá trị nào của đều unsigned charcó thể xảy ra.
  • Cuối cùng a phải giữ giá trị 0.

Edit2: abcó các giá trị không xác định:

3.19.3 giá trị không xác định
giá trị hợp lệ của các loại có liên quan mà tiêu chuẩn này không áp đặt các yêu cầu trên mà giá trị được chọn trong bất kỳ trường hợp


6
Có lẽ tôi đang thiếu một cái gì đó, nhưng với tôi, dường như unsignedchắc chắn có thể có các đại diện bẫy. Bạn có thể chỉ vào phần của tiêu chuẩn nói như vậy không? Tôi thấy trong §6.2.6.2 / 1 như sau: "Đối với các kiểu số nguyên không dấu khác với ký tự không dấu , các bit của biểu diễn đối tượng sẽ được chia thành hai nhóm: bit giá trị và bit đệm (không cần phải có bất kỳ loại nào sau này). ... điều này sẽ được biết đến như là biểu diễn giá trị. Giá trị của bất kỳ bit đệm nào là không xác định. ⁴⁴⁾ "với nhận xét nói rằng:" ⁴⁴⁾ Một số kết hợp giữa các bit đệm có thể tạo ra biểu diễn bẫy ".
conio

6
Tiếp tục nhận xét: "Một số kết hợp của các bit đệm có thể tạo ra các biểu diễn bẫy, ví dụ: nếu một bit đệm là bit chẵn lẻ. Bất kể, không có phép toán số học nào trên các giá trị hợp lệ có thể tạo ra biểu diễn bẫy ngoài một phần của điều kiện ngoại lệ, chẳng hạn như tràn và điều này không thể xảy ra với các loại không có dấu. " - Điều đó thật tuyệt khi chúng ta có một giá trị hợp lệ để làm việc, nhưng giá trị không xác định có thể là một biểu diễn bẫy trước khi được khởi tạo (ví dụ: đặt sai bit chẵn lẻ).
conio

4
@conio Bạn đúng cho tất cả các loại khác unsigned char, nhưng câu trả lời này đang sử dụng unsigned char. Mặc dù vậy, lưu ý rằng: một chương trình tuân thủ nghiêm ngặt có thể tính toán sizeof(unsigned) * CHAR_BITvà xác định dựa trên UINT_MAXviệc triển khai cụ thể rằng không thể có các biểu diễn bẫy unsigned. Sau khi chương trình đã xác định được điều đó, chương trình có thể tiến hành thực hiện chính xác những gì mà câu trả lời này thực hiện unsigned char.

4
@JensGustedt: Không phải là memcpymột sự phân tâm, tức là ví dụ của bạn sẽ không áp dụng nếu nó được thay thế bằng *&a = *&b;.
R .. GitHub DỪNG TRỢ GIÚP ICE

4
@R .. Tôi không chắc nữa. Có một cuộc thảo luận đang diễn ra về danh sách gửi thư của ủy ban C, và có vẻ như tất cả những điều này là một mớ hỗn độn lớn, cụ thể là một khoảng cách lớn giữa những gì đang (hoặc đã từng) và những gì thực sự được viết ra. Tuy nhiên, điều rõ ràng là truy cập bộ nhớ như unsigned charvà do đó memcpygiúp ích, bộ nhớ cho *&ít rõ ràng hơn. Tôi sẽ báo cáo khi điều này lắng xuống.
Jens Gustedt

24

Tiêu chuẩn C cung cấp cho trình biên dịch rất nhiều vĩ độ để thực hiện tối ưu hóa. Hậu quả của những tối ưu hóa này có thể gây ngạc nhiên nếu bạn giả sử một mô hình chương trình ngây thơ trong đó bộ nhớ chưa khởi tạo được đặt thành một số mẫu bit ngẫu nhiên và tất cả các hoạt động được thực hiện theo thứ tự chúng được viết.

Lưu ý: các ví dụ sau chỉ hợp lệ vì xkhông bao giờ được sử dụng địa chỉ của nó, vì vậy nó là "giống như đăng ký". Chúng cũng sẽ hợp lệ nếu loại xcó biểu diễn bẫy; điều này hiếm khi xảy ra đối với các loại không có dấu (nó yêu cầu "lãng phí" ít nhất một bit dung lượng lưu trữ và phải được ghi lại) và không thể xảy ra unsigned char. Nếu xcó kiểu có dấu, thì việc triển khai có thể xác định mẫu bit không phải là một số giữa - (2 n-1 -1) và 2 n-1 -1 như một biểu diễn bẫy. Hãy xem câu trả lời của Jens Gustedt .

Các trình biên dịch cố gắng gán các thanh ghi cho các biến, vì các thanh ghi nhanh hơn bộ nhớ. Vì chương trình có thể sử dụng nhiều biến hơn bộ xử lý có thanh ghi, trình biên dịch thực hiện cấp phát thanh ghi, điều này dẫn đến các biến khác nhau sử dụng cùng một thanh ghi vào những thời điểm khác nhau. Xem xét đoạn chương trình

unsigned x, y, z;   /* 0 */
y = 0;              /* 1 */
z = 4;              /* 2 */
x = - x;            /* 3 */
y = y + z;          /* 4 */
x = y + 1;          /* 5 */

Khi dòng 3 được đánh giá, xchưa được khởi tạo, do đó (lý do là trình biên dịch) dòng 3 phải là một loại lỗi không thể xảy ra do các điều kiện khác mà trình biên dịch không đủ thông minh để tìm ra. Vì zkhông được sử dụng sau dòng 4 và xkhông được sử dụng trước dòng 5, nên cùng một thanh ghi có thể được sử dụng cho cả hai biến. Vì vậy, chương trình nhỏ này được biên dịch cho các hoạt động sau trên thanh ghi:

r1 = 0;
r0 = 4;
r0 = - r0;
r1 += r0;
r0 = r1;

Giá trị cuối cùng của xlà giá trị cuối cùng của r0và giá trị cuối cùng của ylà giá trị cuối cùng của r1. Các giá trị này là x = -3 và y = -4, không phải 5 và 4 như sẽ xảy ra nếu xđược khởi tạo đúng cách.

Để có một ví dụ phức tạp hơn, hãy xem xét đoạn mã sau:

unsigned i, x;
for (i = 0; i < 10; i++) {
    x = (condition() ? some_value() : -x);
}

Giả sử rằng trình biên dịch phát hiện ra rằng conditionkhông có tác dụng phụ. Vì conditionkhông sửa đổi x, trình biên dịch biết rằng lần đầu tiên chạy qua vòng lặp không thể được truy cập xvì nó chưa được khởi tạo. Do đó, lần thực thi đầu tiên của phần thân vòng lặp tương đương với x = some_value(), không cần phải kiểm tra điều kiện. Trình biên dịch có thể biên dịch mã này như thể bạn đã viết

unsigned i, x;
i = 0; /* if some_value() uses i */
x = some_value();
for (i = 1; i < 10; i++) {
    x = (condition() ? some_value() : -x);
}

Cách mà điều này có thể được mô hình hóa bên trong trình biên dịch là xem xét rằng bất kỳ giá trị nào tùy thuộc vào xđều có bất kỳ giá trị nào thuận tiện miễn xlà chưa được khởi tạo. Bởi vì hành vi khi một biến chưa được khởi tạo là không được xác định, thay vì biến chỉ có một giá trị không xác định, trình biên dịch không cần theo dõi bất kỳ mối quan hệ toán học đặc biệt nào giữa các giá trị bất kỳ là thuận tiện. Do đó, trình biên dịch có thể phân tích đoạn mã trên theo cách này:

  • trong lần lặp vòng lặp đầu tiên, xkhông được khởi tạo bởi thời gian -xđược đánh giá.
  • -x có hành vi không xác định, vì vậy giá trị của nó là bất cứ điều gì-thuận-tiện.
  • Quy tắc tối ưu hóa được áp dụng, vì vậy mã này có thể được đơn giản hóa thành .condition ? value : valuecondition; value

Khi đối mặt với mã trong câu hỏi của bạn, chính trình biên dịch này sẽ phân tích rằng khi nào x = - xđược đánh giá, giá trị của -xlà bất kỳ điều gì-thuận-tiện. Vì vậy, bài tập có thể được tối ưu hóa đi.

Tôi chưa tìm kiếm ví dụ về trình biên dịch hoạt động như được mô tả ở trên, nhưng đó là loại tối ưu hóa mà các trình biên dịch tốt cố gắng thực hiện. Tôi sẽ không ngạc nhiên khi gặp một người. Đây là một ví dụ ít hợp lý hơn về trình biên dịch mà chương trình của bạn gặp sự cố. (Có thể không quá đáng nếu bạn biên dịch chương trình của mình trong một số loại chế độ gỡ lỗi nâng cao.)

Trình biên dịch giả định này ánh xạ mọi biến trong một trang bộ nhớ khác nhau và thiết lập các thuộc tính của trang để việc đọc từ một biến chưa được khởi tạo gây ra một bẫy bộ xử lý gọi trình gỡ lỗi. Bất kỳ sự gán nào cho một biến trước tiên đảm bảo rằng trang bộ nhớ của nó được ánh xạ bình thường. Trình biên dịch này không cố gắng thực hiện bất kỳ tối ưu hóa nâng cao nào - nó đang ở chế độ gỡ lỗi, nhằm mục đích dễ dàng xác định các lỗi như các biến chưa được khởi tạo. Khi x = - xđược đánh giá, phía bên phải gây ra một cái bẫy và trình gỡ lỗi sẽ kích hoạt.


+1 Giải thích tốt, tiêu chuẩn đang quan tâm đặc biệt đến tình huống đó. Để tiếp tục câu chuyện đó, hãy xem câu trả lời của tôi bên dưới. (quá lâu để có một bình luận).
Jens Gustedt

@JensGustedt Ồ, câu trả lời của bạn đưa ra một điểm rất quan trọng mà tôi (và những người khác) đã bỏ qua: trừ khi kiểu có giá trị bẫy, đối với kiểu không dấu yêu cầu "lãng phí" ít nhất một bit, xcó giá trị chưa được khởi tạo nhưng hành vi khi truy cập sẽ được định nghĩa nếu x không có hành vi giống như thanh ghi.
Gilles 'Somali dừng tà ác'

@Gilles: ít nhất clang tạo ra loại tối ưu hóa mà bạn đã đề cập: (1) , (2) , (3) .
Vlad

1
Có lợi ích thực tế nào khi có các quy trình chế biến theo kiểu đó? Nếu mã hạ lưu không bao giờ sử dụng giá trị của x, thì tất cả các hoạt động trên nó có thể bị bỏ qua cho dù giá trị của nó đã được xác định hay chưa. Nếu mã theo sau ví dụ if (volatile1) x=volatile2; ... x = (x+volatile3) & 255;sẽ hài lòng như nhau với bất kỳ giá trị 0-255 nào xcó thể chứa trong trường hợp giá trị volatile1không mang lại kết quả, tôi sẽ nghĩ rằng một triển khai sẽ cho phép lập trình viên bỏ qua một lần ghi không cần thiết xsẽ được coi là chất lượng cao hơn một sẽ cư xử ...
supercat

... trong trường hợp đó hoàn toàn không thể đoán trước được. Việc triển khai có thể làm dấy lên một cách đáng tin cậy một cái bẫy do triển khai xác định trong trường hợp đó, đối với một số mục đích nhất định, có thể được coi là có chất lượng cao hơn, nhưng hành vi hoàn toàn không thể đoán trước được đối với tôi dường như là dạng hành vi chất lượng thấp nhất cho bất kỳ mục đích nào.
supercat

16

Có, chương trình có thể bị lỗi. Ví dụ, có thể có các biểu diễn bẫy (các mẫu bit cụ thể không thể xử lý được) có thể gây ra ngắt CPU, điều này có thể làm hỏng chương trình.

(6.2.6.1 trên bản nháp C11 muộn cho biết) Một số biểu diễn đối tượng nhất định không cần đại diện cho một giá trị của loại đối tượng. Nếu giá trị được lưu trữ của một đối tượng có cách biểu diễn như vậy và được đọc bởi một biểu thức giá trị không có kiểu ký tự, thì hành vi đó là không xác định. Nếu một biểu diễn như vậy được tạo ra bởi một hiệu ứng phụ sửa đổi tất cả hoặc bất kỳ phần nào của đối tượng bằng một biểu thức giá trị không có kiểu ký tự, hành vi đó là không xác định. 50) Biểu diễn như vậy được gọi là biểu diễn bẫy.

(Giải thích này chỉ áp dụng trên các nền tảng unsigned intcó thể có các biểu diễn bẫy, điều này hiếm khi xảy ra trên các hệ thống trong thế giới thực; xem nhận xét để biết chi tiết và giới thiệu để biết các nguyên nhân thay thế và có lẽ phổ biến hơn dẫn đến từ ngữ hiện tại của tiêu chuẩn.)


3
@VladLazarenko: Đây là về C, không phải CPU cụ thể. Bất cứ ai cũng có thể thiết kế một cách tầm thường một CPU có các mẫu bit cho các số nguyên khiến nó phát điên. Hãy xem xét một CPU có một "bit điên rồ" trong các thanh ghi của nó.
David Schwartz

2
Vì vậy, tôi có thể nói rằng hành vi được xác định rõ trong trường hợp số nguyên và x86 không?

3
Về mặt lý thuyết, bạn có thể có một trình biên dịch quyết định chỉ sử dụng số nguyên 28 bit (trên x86) và thêm mã cụ thể để xử lý từng phép cộng, phép nhân (v.v.) và đảm bảo rằng 4 bit này không được sử dụng (hoặc phát ra một SIGSEGV nếu không ). Một giá trị chưa được sinh ra có thể gây ra điều này.
eq-

4
Tôi ghét khi ai đó xúc phạm người khác vì ai đó không hiểu vấn đề. Liệu hành vi đó có được xác định hay không hoàn toàn là vấn đề của tiêu chuẩn. Ồ, và chẳng có gì thực tế về kịch bản của eq cả ... nó hoàn toàn do giả thiết.
Jim Balter,

7
@Vlad Lazarenko: CPU Itanium có cờ NaT (Not a Thing) cho mỗi thanh ghi số nguyên. Cờ NaT được sử dụng để kiểm soát việc thực thi suy đoán và có thể tồn tại trong các thanh ghi không được khởi tạo đúng cách trước khi sử dụng. Việc đọc từ một thanh ghi như vậy với tập hợp bit NaT tạo ra một ngoại lệ. Xem blog.msdn.com/b/oldnewthing/archive/2004/01/19/60162.aspx
Nordic Mainframe,

13

(Câu trả lời này đề cập đến C 1999. Đối với C 2011, hãy xem câu trả lời của Jens Gustedt.)

Tiêu chuẩn C không nói rằng việc sử dụng giá trị của một đối tượng có thời lượng lưu trữ tự động không được khởi tạo là hành vi không xác định. Tiêu chuẩn C 1999 cho biết, trong 6.7.8 10, "Nếu một đối tượng có thời lượng lưu trữ tự động không được khởi tạo rõ ràng, giá trị của nó là không xác định." (Đoạn này tiếp tục xác định cách các đối tượng tĩnh được khởi tạo, vì vậy các đối tượng chưa được khởi tạo duy nhất mà chúng ta quan tâm là các đối tượng tự động.)

3.17.2 định nghĩa “giá trị không xác định” là “giá trị không xác định hoặc biểu diễn bẫy”. 3.17.3 định nghĩa “giá trị không xác định” là “giá trị hợp lệ của kiểu liên quan mà tiêu chuẩn này không đặt ra yêu cầu về giá trị nào được chọn trong bất kỳ trường hợp nào”.

Vì vậy, nếu chưa khởi tạo unsigned int xcó giá trị không xác định, thì x -= xphải tạo ra số không. Điều đó đặt ra câu hỏi liệu nó có thể là một biểu diễn bẫy hay không. Việc truy cập giá trị bẫy gây ra hành vi không xác định, theo 6.2.6.1 5.

Một số loại đối tượng có thể có các biểu diễn bẫy, chẳng hạn như các NaN báo hiệu của số dấu phẩy động. Nhưng số nguyên không dấu là đặc biệt. Theo 6.2.6.2, mỗi trong số N bit giá trị của một int không dấu đại diện cho lũy thừa của 2 và mỗi tổ hợp của các bit giá trị đại diện cho một trong các giá trị từ 0 đến 2 N -1. Vì vậy, các số nguyên không dấu chỉ có thể có các biểu diễn bẫy do một số giá trị trong các bit đệm của chúng (chẳng hạn như bit chẵn lẻ).

Nếu, trên nền tảng đích của bạn, một int chưa được ký không có các bit đệm, thì một int chưa được khởi tạo không thể có biểu diễn bẫy và việc sử dụng giá trị của nó không thể gây ra hành vi không xác định.


Nếu xcó một biểu diễn bẫy, thì x -= xcó thể bẫy, phải không? Tuy nhiên, +1 để chỉ ra các số nguyên không dấu không có bit thừa phải có hành vi được xác định - nó rõ ràng là ngược lại với các câu trả lời khác và (theo trích dẫn) nó có vẻ là những gì tiêu chuẩn ngụ ý.
user541686

Có, nếu loại xcó biểu diễn bẫy, thì x -= xcó thể bẫy. Ngay cả khi xđược sử dụng đơn giản như một giá trị cũng có thể mắc bẫy. (Nó là an toàn để sử dụng xnhư một giá trị trái; viết thành một đối tượng sẽ không bị ảnh hưởng bởi một đại diện cảnh khó khăn mà là ở trong đó.)
Eric Postpischil

loại unsigned hiếm khi có một đại diện bẫy
Jens Gustedt

Trích dẫn Raymond Chen , "Trên ia64, mỗi thanh ghi 64 bit thực sự là 65 bit. Bit phụ được gọi là" NaT ", viết tắt của" not a thing ". Bit được đặt khi thanh ghi không chứa giá trị hợp lệ. Hãy nghĩ về nó như là phiên bản số nguyên của dấu phẩy động NaN. ... nếu bạn có một thanh ghi có giá trị là NaT và bạn rất khó thở vào nó sai cách (ví dụ: cố gắng lưu giá trị của nó vào bộ nhớ), bộ xử lý sẽ đưa ra một ngoại lệ STATUS_REG_NAT_CONSUMPTION ". Tức là, một bit bẫy có thể hoàn toàn nằm ngoài giá trị.
Chúc mừng và hth. - Alf

−1 Câu lệnh "Nếu, trên nền tảng đích của bạn, một int không dấu không có bit đệm, thì một int không dấu chưa được khởi tạo không thể có biểu diễn bẫy và việc sử dụng giá trị của nó không thể gây ra hành vi không xác định." không thể xem xét các sơ đồ như các bit x64 NaT.
Chúc mừng và hth. - Alf

11

Vâng, nó không xác định. Mã có thể bị lỗi. C nói rằng hành vi là không xác định vì không có lý do cụ thể nào để đưa ra một ngoại lệ cho quy tắc chung. Ưu điểm là ưu điểm giống như tất cả các trường hợp khác của hành vi không xác định - trình biên dịch không phải xuất mã đặc biệt để làm cho điều này hoạt động.

Rõ ràng, trình biên dịch có thể đơn giản sử dụng bất kỳ giá trị rác nào mà nó cho là "tiện dụng" bên trong biến, và nó sẽ hoạt động như dự định ... có gì sai với cách tiếp cận đó?

Bạn nghĩ tại sao điều đó không xảy ra? Đó chính xác là cách tiếp cận được thực hiện. Trình biên dịch không cần thiết để làm cho nó hoạt động, nhưng không bắt buộc phải làm cho nó không thành công.


1
Tuy nhiên, trình biên dịch cũng không cần phải có mã đặc biệt cho việc này. Chỉ cần phân bổ không gian (như mọi khi) và không phức tạp hóa biến sẽ mang lại cho nó hành vi chính xác. Tôi không nghĩ rằng điều đó cần logic đặc biệt.
user541686

7
1) Chắc chắn, họ có thể có. Nhưng tôi không thể nghĩ ra bất kỳ lập luận nào có thể làm cho điều đó tốt hơn. 2) Nền tảng biết rằng không thể dựa vào giá trị của bộ nhớ chưa khởi tạo, vì vậy bạn có thể tự do thay đổi nó. Ví dụ, nó có thể không có bộ nhớ chưa khởi tạo trong nền để có các trang được xóa sẵn sàng để sử dụng khi cần thiết. (Hãy xem xét nếu điều này xảy ra: 1) Chúng tôi đọc giá trị để trừ, giả sử chúng tôi nhận được 3. 2) Trang nhận về 0 vì nó chưa được khởi tạo, thay đổi giá trị thành 0. 3) Chúng tôi thực hiện một phép trừ nguyên tử, phân bổ trang và tạo giá trị -3. Rất tiếc.)
David Schwartz

2
-1 bởi vì bạn không đưa ra lời biện minh nào cho yêu cầu của mình. Có những tình huống sẽ hợp lệ để mong đợi rằng trình biên dịch chỉ lấy giá trị được ghi trong vị trí bộ nhớ.
Jens Gustedt

1
@JensGustedt: Tôi không hiểu nhận xét của bạn. Bạn có thể vui lòng làm rõ?
David Schwartz

3
Bởi vì bạn chỉ tuyên bố rằng có một quy tắc chung, mà không đề cập đến nó. Như vậy, nó chỉ là một nỗ lực "bằng chứng của cơ quan có thẩm quyền" mà không phải là những gì tôi mong đợi ở SO. Và vì không tranh luận một cách hiệu quả tại sao đây không thể là một giá trị không cụ thể. Lý do duy nhất mà đây là UB trong trường hợp chung xcó thể được tuyên bố là register, đó là địa chỉ của nó không bao giờ được lấy. Tôi không biết liệu bạn có biết về điều đó không (nếu, bạn đã che giấu nó một cách hiệu quả) nhưng một câu trả lời chính xác phải đề cập đến nó.
Jens Gustedt

6

Đối với bất kỳ biến nào thuộc loại bất kỳ, không được khởi tạo hoặc vì các lý do khác giữ một giá trị không xác định, điều sau áp dụng cho việc đọc mã giá trị đó:

  • Trong trường hợp biến có thời lượng lưu trữ tự động không có địa chỉ của nó, thì mã luôn gọi hành vi không xác định [1].
  • Ngược lại, trong trường hợp hệ thống hỗ trợ các biểu diễn bẫy cho kiểu biến đã cho, mã luôn gọi hành vi không xác định [2].
  • Ngược lại, nếu không có biểu diễn bẫy, biến sẽ nhận một giá trị không xác định. Không có gì đảm bảo rằng giá trị không xác định này là nhất quán mỗi khi biến được đọc. Tuy nhiên, nó được đảm bảo không phải là một biểu diễn bẫy và do đó nó được đảm bảo không gọi ra hành vi không xác định [3].

    Sau đó, giá trị có thể được sử dụng một cách an toàn mà không gây ra sự cố chương trình, mặc dù mã đó không thể di động đối với các hệ thống có biểu diễn bẫy.


[1]: C11 6.3.2.1:

Nếu lvalue chỉ định một đối tượng có thời lượng lưu trữ tự động có thể đã được khai báo với lớp lưu trữ đăng ký (chưa bao giờ được sử dụng địa chỉ của nó) và đối tượng đó chưa được khởi tạo (không được khai báo bằng bộ khởi tạo và không có phép gán cho nó trước khi sử dụng ), hành vi là không xác định.

[2]: C11 6.2.6.1:

Một số biểu diễn đối tượng nhất định không cần đại diện cho một giá trị của kiểu đối tượng. Nếu giá trị được lưu trữ của một đối tượng có cách biểu diễn như vậy và được đọc bởi một biểu thức giá trị không có kiểu ký tự, thì hành vi đó là không xác định. Nếu một biểu diễn như vậy được tạo ra bởi một hiệu ứng phụ sửa đổi tất cả hoặc bất kỳ phần nào của đối tượng bằng một biểu thức giá trị không có kiểu ký tự, hành vi đó là không xác định. 50) Biểu diễn như vậy được gọi là biểu diễn bẫy.

[3] C11:

3.19.2
giá trị
không xác định hoặc giá trị không xác định hoặc biểu diễn bẫy

3.19.3
Giá trị không xác định giá trị
hợp lệ của kiểu liên quan trong đó tiêu chuẩn này không đặt ra yêu cầu về giá trị nào được chọn trong bất kỳ trường hợp nào
CHÚ THÍCH: Giá trị không xác định không thể là biểu diễn bẫy.

3.19.4
biểu diễn bẫy
một biểu diễn đối tượng không cần đại diện cho một giá trị của loại đối tượng


Tôi lập luận rằng điều này giải quyết thành "Nó luôn luôn là hành vi không xác định" vì máy trừu tượng C -can- có các biểu diễn bẫy. Chỉ vì việc triển khai của bạn không sử dụng chúng không làm cho mã được xác định. Trên thực tế, việc đọc chặt chẽ thậm chí sẽ không đòi hỏi các biểu diễn bẫy phải có trong phần cứng từ những gì tôi không thể nói Tôi không hiểu tại sao trình biên dịch không thể quyết định một mẫu bit cụ thể là một cái bẫy, hãy kiểm tra nó mỗi khi biến được đọc và gọi UB.
Vality

2
@Vality Trong thế giới thực, 99,9999% tất cả các máy tính là CPU bổ sung của hai loại CPU mà không có biểu diễn bẫy. Do đó, không có biểu diễn bẫy là tiêu chuẩn và thảo luận về hành vi trên các máy tính trong thế giới thực như vậy là rất phù hợp. Giả sử rằng các máy tính cực kỳ kỳ lạ là tiêu chuẩn thì không hữu ích. Các biểu diễn bẫy trong thế giới thực rất hiếm nên sự hiện diện của thuật ngữ biểu diễn bẫy trong tiêu chuẩn hầu hết được coi là một khiếm khuyết tiêu chuẩn kế thừa từ những năm 1980. Cũng như hỗ trợ cho máy tính bổ sung và ký hiệu & độ lớn.
Lundin

2
Nhân tiện, đây là một lý do tuyệt vời tại sao stdint.hnên luôn được sử dụng thay vì các kiểu gốc của C. Bởi vì stdint.hthực thi phần bổ sung của 2 và không có bit đệm. Nói cách khác, các stdint.hloại không được phép chứa đầy những thứ tào lao.
Lundin

2
Một lần nữa, phản hồi của ủy ban đối với báo cáo khiếm khuyết nói rằng: "Câu trả lời cho câu hỏi 2 là bất kỳ hoạt động nào được thực hiện trên các giá trị không xác định sẽ có kết quả là giá trị không xác định." và "Câu trả lời cho câu hỏi 3 là các hàm thư viện sẽ thể hiện hành vi không xác định khi được sử dụng trên các giá trị không xác định."
Antti Haapala

2
DRs 451 và 260
Antti Haapala

2

Trong khi nhiều câu trả lời tập trung vào các bộ xử lý bẫy truy cập đăng ký chưa được khởi tạo, các hành vi kỳ quặc có thể phát sinh ngay cả trên các nền tảng không có bẫy như vậy, bằng cách sử dụng các trình biên dịch không nỗ lực cụ thể để khai thác UB. Hãy xem xét mã:

volatile uint32_t a,b;
uin16_t moo(uint32_t x, uint16_t y, uint32_t z)
{
  uint16_t temp;
  if (a)
    temp = y;
  else if (b)
    temp = z;
  return temp;  
}

một trình biên dịch cho một nền tảng như ARM, nơi tất cả các hướng dẫn không phải tải và lưu trữ hoạt động trên thanh ghi 32-bit có thể xử lý hợp lý mã theo cách tương đương với:

volatile uint32_t a,b;
// Note: y is known to be 0..65535
// x, y, and z are received in 32-bit registers r0, r1, r2
uin32_t moo(uint32_t x, uint32_t y, uint32_t z)
{
  // Since x is never used past this point, and since the return value
  // will need to be in r0, a compiler could map temp to r0
  uint32_t temp;
  if (a)
    temp = y;
  else if (b)
    temp = z & 0xFFFF;
  return temp;  
}

Nếu một trong hai lần đọc biến động mang lại giá trị khác 0, r0 sẽ được tải với giá trị trong phạm vi 0 ... 65535. Nếu không, nó sẽ mang lại bất kỳ giá trị nào mà nó nắm giữ khi hàm được gọi (tức là giá trị được truyền vào x), có thể không phải là giá trị trong phạm vi 0..65535. Tiêu chuẩn thiếu bất kỳ thuật ngữ nào để mô tả hành vi của giá trị có kiểu là uint16_t nhưng có giá trị nằm ngoài phạm vi 0..65535, ngoại trừ việc nói rằng bất kỳ hành động nào có thể tạo ra hành vi đó đều gọi UB.


Hấp dẫn. Vì vậy, bạn nói câu trả lời được chấp nhận là sai? Hay bạn đang nói nó đúng trên lý thuyết nhưng trong thực tế, các trình biên dịch có thể làm những điều kỳ lạ hơn?
dùng541686

@Mehrdad: Việc triển khai thường có hành vi vượt ra ngoài giới hạn của những gì có thể xảy ra nếu không có UB. Tôi nghĩ sẽ hữu ích nếu Tiêu chuẩn công nhận khái niệm về giá trị không xác định một phần mà các bit "được phân bổ" sẽ hoạt động theo kiểu, tệ nhất là không xác định, nhưng với các bit phía trên bổ sung hoạt động không xác định (ví dụ: nếu kết quả của hàm trên được lưu trữ vào một biến kiểu uint16_t, biến đó đôi khi có thể được đọc là 123 và đôi khi là 6553623). Nếu kết quả cuối cùng bị bỏ qua ...
supercat

... hoặc được sử dụng theo cách mà bất kỳ cách nào có thể đọc được nó đều sẽ mang lại kết quả cuối cùng đáp ứng các yêu cầu, sự tồn tại của giá trị không xác định một phần sẽ không phải là vấn đề. Mặt khác, không có điều gì trong Tiêu chuẩn cho phép tồn tại các giá trị không xác định một phần trong bất kỳ trường hợp nào mà Tiêu chuẩn sẽ áp đặt bất kỳ yêu cầu hành vi nào.
supercat

Đối với tôi, có vẻ như những gì bạn đang mô tả chính xác là những gì trong câu trả lời được chấp nhận - rằng nếu một biến có thể đã được khai báo với register, thì nó có thể có thêm các bit làm cho hành vi có khả năng không được xác định. Đó chính xác là những gì bạn đang nói, phải không?
user541686

@Mehrdad: Câu trả lời được chấp nhận tập trung vào các kiến ​​trúc có các thanh ghi có thêm trạng thái "chưa khởi tạo" và mắc bẫy nếu một thanh ghi chưa khởi tạo được tải. Những kiến ​​trúc như vậy tồn tại, nhưng không phải là phổ biến. Tôi mô tả một tình huống trong đó phần cứng phổ biến có thể thể hiện hành vi nằm ngoài phạm vi của bất kỳ thứ gì được đề cập trong Tiêu chuẩn C, nhưng sẽ bị hạn chế một cách hữu ích nếu một trình biên dịch không bổ sung thêm tính độc đáo của riêng nó vào hỗn hợp. Ví dụ: nếu một hàm có một tham số chọn một hoạt động để thực hiện và một số hoạt động trả về dữ liệu hữu ích nhưng những hoạt động khác thì không, ...
supercat
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.