Mã sẽ không bao giờ được thực thi có thể gọi hành vi không xác định không?


81

Mã gọi hành vi không xác định (trong ví dụ này là phép chia cho 0) sẽ không bao giờ được thực thi, chương trình có còn hành vi không xác định không?

int main(void)
{
    int i;
    if(0)
    {
        i = 1/0;
    }
    return 0;
}

Tôi nghĩ đó vẫn là hành vi chưa được xác định, nhưng tôi không thể tìm thấy bất kỳ bằng chứng nào trong tiêu chuẩn để hỗ trợ hoặc phủ nhận tôi.

Vì vậy, bất kỳ ý tưởng?


7
Tôi muốn nói nó không phải là "hành vi" nếu nó không bao giờ thực hiện
Kevin

1
Nếu UB là thời gian chạy (như thế này) - thì không. Nhưng tôi rất nghi ngờ tiêu chuẩn nói bất cứ điều gì về điều này.
keltar

13
Nghe giống như một câu hỏi về ngữ nghĩa, không phải lập trình.
Wooble

14
@Wooble Tôi không đồng ý. Cụm từ hành vi không xác định có một ý nghĩa đặc biệt trong C / C ++. Và câu hỏi này có liên quan đến một số tình huống khác xác định hành vi không xác định hay không. Đối với bản ghi, nếu bạn đã đọc tiêu chuẩn C / C ++, bạn sẽ tìm thấy cụm từ hành vi không xác định ở khắp mọi nơi.
Yu Hao

10
@Cornstalks: Tiêu chuẩn C không sử dụng cụm từ “gọi ra hành vi không xác định”, vì vậy bạn không thể suy luận về tiêu chuẩn C dựa trên ý nghĩa của cụm từ này. Sử dụng nó để mô tả C là không phù hợp vì nó gợi ý rằng "hành vi không xác định" là một thứ chẳng hạn như bức tường mà bạn gặp phải nếu bạn đi quá giới hạn. Trên thực tế, "hành vi không xác định" là một thiếu sót; nó là sự kết thúc của ranh giới. Khi bạn rời khỏi thị trấn được xác định rõ ràng là tiêu chuẩn C, bạn đang ở trong một cánh đồng rộng mở, nơi có thể xây dựng bất cứ thứ gì.
Eric Postpischil

Câu trả lời:


70

Hãy xem cách tiêu chuẩn C định nghĩa các thuật ngữ "hành vi" và "hành vi không xác định".

Tài liệu tham khảo là dự thảo N1570 của tiêu chuẩn ISO C 2011; Tôi không biết về bất kỳ sự khác biệt liên quan nào trong bất kỳ tiêu chuẩn nào trong ba tiêu chuẩn ISO C đã được công bố (1990, 1999 và 2011).

Phần 3.4:

hành vi
xuất hiện bên ngoài hoặc hành động

Ok, điều đó hơi mơ hồ, nhưng tôi tranh luận rằng một câu lệnh đã cho không có "diện mạo" và chắc chắn không có "hành động", trừ khi nó thực sự được thực thi.

Mục 3.4.3:

hành vi ứng xử không xác định
, khi sử dụng cấu trúc chương trình không thể chuyển động hoặc có sai sót hoặc dữ liệu sai, mà tiêu chuẩn này không áp đặt yêu cầu

Nó nói " khi sử dụng " một cấu trúc như vậy. Từ "sử dụng" không được định nghĩa bởi tiêu chuẩn, vì vậy chúng tôi rơi trở lại nghĩa tiếng Anh thông thường. Một cấu trúc không được "sử dụng" nếu nó không bao giờ được thực thi.

Có một lưu ý dưới định nghĩa đó:

CHÚ THÍCH: Hành vi không xác định có thể xảy ra bao gồm từ bỏ qua hoàn toàn tình huống với kết quả không thể đoán trước, đến hành vi trong quá trình dịch hoặc thực thi chương trình theo cách thức được tài liệu hóa đặc trưng của môi trường (có hoặc không đưa ra thông báo chẩn đoán), đến việc chấm dứt bản dịch hoặc thực thi (với phát hành một thông báo chẩn đoán).

Vì vậy, một trình biên dịch được phép từ chối chương trình của bạn tại thời điểm biên dịch nếu hành vi của nó là không xác định. Nhưng giải thích của tôi về điều đó là nó có thể làm như vậy chỉ khi nó có thể chứng minh rằng mọi thực thi của chương trình sẽ gặp phải hành vi không xác định. Tôi nghĩ điều này ngụ ý rằng:

if (rand() % 2 == 0) {
    i = i / 0;
}

mà chắc chắn có thể có hành vi không xác định, không thể bị từ chối tại thời điểm biên dịch.

Như một vấn đề thực tế, các chương trình phải có khả năng thực hiện các bài kiểm tra thời gian chạy để bảo vệ khỏi việc gọi ra hành vi không xác định và tiêu chuẩn phải cho phép chúng làm như vậy.

Ví dụ của bạn là:

if (0) {
    i = 1/0;
}

mà không bao giờ thực hiện phép chia cho 0. Một thành ngữ rất phổ biến là:

int x, y;
/* set values for x and y */
if (y != 0) {
    x = x / y;
}

Sự phân chia chắc chắn có hành vi không xác định nếu y == 0, nhưng nó không bao giờ được thực thi nếu y == 0. Hành vi được xác định rõ và vì lý do tương tự mà ví dụ của bạn cũng được xác định rõ: bởi vì hành vi không xác định tiềm ẩn có thể không bao giờ thực sự xảy ra.

(Trừ khi INT_MIN < -INT_MAX && x == INT_MIN && y == -1(có, phép chia số nguyên có thể bị tràn), nhưng đó là một vấn đề riêng biệt.)

Trong một nhận xét (kể từ khi bị xóa), ai đó đã chỉ ra rằng trình biên dịch có thể đánh giá các biểu thức không đổi tại thời điểm biên dịch. Điều đó đúng, nhưng không liên quan trong trường hợp này, vì trong bối cảnh

i = 1/0;

1/0 không phải là một biểu thức hằng .

Một liên tục thể hiện là một phạm trù cú pháp làm giảm tới có điều kiện thể hiện (trong đó không bao gồm bài tập và biểu thức dấu phẩy). Việc sản xuất liên tục thể hiện xuất hiện trong ngữ pháp chỉ trong những bối cảnh thật sự yêu cầu một biểu thức hằng số, chẳng hạn như trường hợp nhãn. Vì vậy, nếu bạn viết:

switch (...) {
    case 1/0:
    ...
}

sau đó 1/0là một biểu thức hằng số - và một biểu thức vi phạm ràng buộc trong 6.6p4: "Mỗi biểu thức hằng số sẽ đánh giá một hằng số nằm trong phạm vi các giá trị có thể biểu diễn cho kiểu của nó.", vì vậy cần phải chẩn đoán. Nhưng phía bên phải của một phép gán không yêu cầu một biểu thức hằng , chỉ đơn thuần là một biểu thức điều kiện , vì vậy các ràng buộc đối với biểu thức hằng không áp dụng. Một trình biên dịch có thể đánh giá bất kỳ biểu thức nào mà nó có thể biên dịch tại thời điểm biên dịch, nhưng chỉ khi hành vi giống như khi nó được đánh giá trong khi thực thi (hoặc, trong ngữ cảnh if (0), không được đánh giá trong quá trình thực thi ().

(Một cái gì đó trông giống hệt như một biểu thức hằng không nhất thiết phải là một biểu thức hằng , cũng giống như, trong x + y * z, chuỗi x + ykhông phải là một biểu thức cộng do ngữ cảnh mà nó xuất hiện.)

Có nghĩa là chú thích trong N1570 phần 6.6 mà tôi sẽ trích dẫn:

Do đó, trong lần khởi tạo sau,
static int i = 2 || 1 / 0;
biểu thức là một biểu thức hằng số nguyên hợp lệ với giá trị là một.

không thực sự liên quan đến câu hỏi này.

Cuối cùng, có một số thứ được xác định là gây ra hành vi không xác định không liên quan đến những gì xảy ra trong quá trình thực thi. Phụ lục J, phần 2 của tiêu chuẩn C (xem lại bản dự thảo N1570 ) liệt kê những điều gây ra hành vi không xác định, được thu thập từ phần còn lại của tiêu chuẩn. Một số ví dụ (tôi không khẳng định đây là danh sách đầy đủ) là:

  • Tệp nguồn trống không kết thúc bằng ký tự dòng mới không được đặt ngay trước ký tự gạch chéo ngược hoặc kết thúc bằng mã thông báo hoặc nhận xét tiền xử lý một phần
  • Việc ghép mã thông báo tạo ra một chuỗi ký tự khớp với cú pháp của tên ký tự chung
  • Một ký tự không có trong bộ ký tự nguồn cơ bản được gặp trong tệp nguồn, ngoại trừ trong một mã định danh, một hằng số ký tự, một chuỗi ký tự, tên tiêu đề, nhận xét hoặc mã thông báo tiền xử lý không bao giờ được chuyển đổi thành mã thông báo
  • Một mã định danh, chú thích, ký tự chuỗi, hằng số ký tự hoặc tên tiêu đề chứa ký tự nhiều byte không hợp lệ hoặc không bắt đầu và kết thúc ở trạng thái thay đổi ban đầu
  • Cùng một mã định danh có cả liên kết bên trong và bên ngoài trong cùng một đơn vị dịch

Những trường hợp cụ thể này là những thứ mà một trình biên dịch có thể phát hiện ra. Tôi nghĩ rằng hành vi của họ là không xác định vì ủy ban không muốn hoặc không thể áp đặt cùng một hành vi trên tất cả các triển khai và việc xác định một loạt các hành vi được phép chỉ là không đáng để nỗ lực. Chúng không thực sự thuộc loại "mã sẽ không bao giờ được thực thi", nhưng tôi đề cập đến chúng ở đây cho đầy đủ.


2
@EricPostpischil 6.6 / 4 cho biết "Mỗi biểu thức hằng số sẽ đánh giá một hằng số nằm trong phạm vi giá trị có thể biểu diễn cho kiểu của nó." Điều đó sẽ không loại trừ 1/0việc trở thành một biểu thức hằng số?
Casey

2
@EricPostpischil: Tôi không nghĩ điều đó hoàn toàn đúng. Vi phạm một hạn chế nói chung có nghĩa là cần phải có chẩn đoán thời gian biên dịch, không chỉ đơn thuần là thứ gì đó có thể là foo thì không phải là foo . 1/0không phải là một biểu thức hằng trong ngữ cảnh của câu hỏi vì nó không được phân tích cú pháp như một biểu thức hằng , chỉ đơn thuần là một biểu thức điều kiện , một phần của biểu thức gán . case 1/0:sẽ vi phạm ràng buộc và yêu cầu chẩn đoán.
Keith Thompson

DR # 109 dường như chỉ ra rằng chương trình không phải là UB, hãy xem câu trả lời mới mà tôi vừa đăng.
ouah

1
Re " so we fall back to the common English Nghia ", Theo nghĩa tiếng Anh, chương trình sử dụng một cấu trúc nếu nó có trong chương trình. Vậy tại sao câu trả lời của bạn giả sử sử dụng một cấu trúc có nghĩa là thực thi một cấu trúc? Kết luận của bạn không tuân theo lời giải thích của bạn!
ikegami

31

Bài viết này thảo luận về câu hỏi này trong phần 2.6:

int main(void){
      guard();
      5 / 0;
}

Các tác giả cho rằng chương trình được xác định khi guard()nào không kết thúc. Họ cũng thấy mình phân biệt các khái niệm “tĩnh không xác định” và “không xác định động”, ví dụ:

Ý định đằng sau tiêu chuẩn 11 dường như là, nói chung, các tình huống được thực hiện một cách tĩnh không xác định nếu không dễ dàng tạo mã cho chúng. Chỉ khi mã có thể được tạo, thì tình huống mới có thể được xác định động.

11) Thư từ riêng với thành viên ủy ban.

Tôi khuyên bạn nên xem toàn bộ bài báo. Tổng hợp lại, nó vẽ nên một bức tranh nhất quán.

Thực tế là các tác giả của bài báo đã phải thảo luận câu hỏi với một thành viên ủy ban xác nhận rằng tiêu chuẩn hiện đang mờ nhạt về câu trả lời cho câu hỏi của bạn.


1
Đó là ví dụ trình bày những khó khăn chỉ trong cho dù bạn tĩnh có thể xác định xem hành vi của nó được xác định. Khi nó chạy (giả sử hành vi của guard()không phải là không xác định), hành vi không được xác định nếu và chỉ khi câu lệnh 5 / 0;thực sự được thực thi. (Lưu ý rằng một trình biên dịch có thể thay thế hợp pháp việc đánh giá 5 / 0bằng một lệnh gọi đến abort()hoặc một cái gì đó tương tự; sau đó chương trình sẽ hủy bỏ nếu và chỉ khi việc thực thi đạt đến điểm đó.) Một trình biên dịch có thể từ chối chương trình đó chỉ khi nó có thể xác định rằng guard()sẽ luôn kết thúc.
Keith Thompson

@KeithThompson Để làm rõ sự phân biệt tĩnh / động trong bài viết, 5/0 được coi là động vì trình biên dịch có thể tạo mã chia cho 0: chỉ cần tạo mã thông thường chia cho z sau khi đã đặt z thành 0. Do đó, một trình biên dịch đơn giản có thể tạo ra một lệnh chia. Một trình biên dịch phức tạp xác định rằng guard()không kết thúc không phải tạo bất kỳ mã nào cho 5/0. Ngược lại, không có cách nào để tạo mã cho (int)(void)5, người ta không thể chỉ tạo mã cho (int)(void)z, vì điều đó cũng không đúng. Vì vậy, các tác giả cho rằng ...
Pascal Cuoq

@KeithThompson… một trình biên dịch được phép từ chối chương trình if (0) (int)(void)5;vì câu hỏi hóc búa mà nó đưa ra cho trình biên dịch ngây thơ, trong khi UB động không thể truy cập chẳng hạn if (0) 5 / 0;là vô hại. Đây là những gì diễn ra từ cuộc thảo luận của họ với một thành viên ủy ban và tôi đã thấy một lập luận tương tự được đưa ra ở nơi khác (nhưng có lẽ từ cùng một nguồn, đặc biệt là vì tôi không nhớ nó ở đâu). Tôi đang xem xét cơ sở lý luận của C99 vào lúc này, nếu tôi thấy bất kỳ đề cập nào về vấn đề này, tôi sẽ quay lại và chỉ ra.
Pascal Cuoq

2
(int)(void)5là một vi phạm ràng buộc. N1570 6.5.4, mô tả toán tử ép kiểu: "Ràng buộc: Trừ khi tên kiểu chỉ định kiểu void, tên kiểu sẽ chỉ định kiểu vô hướng nguyên tử, đủ điều kiện hoặc không đủ điều kiện và toán hạng phải có kiểu vô hướng.". (void)5không có kiểu vô hướng, do đó (int)(void)5vi phạm ràng buộc đó, bất kể mã chứa nó có được thực thi hay không.
Keith Thompson

@KeithThompson Vâng, họ dường như đã chọn sai ví dụ, nhưng bên trong danh sách dài trong J.2, có một ví dụ không phải là vi phạm ràng buộc và đó là "tĩnh", chắc chắn? Còn cổ điển cũ thì sao, “Một tệp nguồn trống không kết thúc bằng ký tự dòng mới…”? Không có khái niệm về khả năng tiếp cận áp dụng cho điều này, nhưng nó không phải là một vi phạm ràng buộc, phải không?
Pascal Cuoq

5

Trong trường hợp này, hành vi không xác định là kết quả của việc thực thi mã. Vì vậy, nếu mã không được thực thi, không có hành vi không xác định.

Mã không được thực thi có thể gọi hành vi không xác định nếu hành vi không xác định chỉ là kết quả của việc khai báo mã (ví dụ: nếu một số trường hợp tạo bóng biến là không xác định).


Đối với ví dụ về trường hợp số 2, hãy xem xét #include "//e"cái nào gọi UB.
Michael Foukarakis

2

Tôi sẽ đi đến đoạn cuối cùng của câu trả lời này: https://stackoverflow.com/a/18384176/694576

... UB là vấn đề thời gian chạy, không phải là vấn đề thời gian biên dịch ...

Vì vậy, không, không có UB nào được viện dẫn.


2
Bạn không nên tin mọi thứ bạn đọc trên internet, đặc biệt là không phải từ câu trả lời StackOverflow đó.
Pascal Cuoq

1
@PascalCuoq Điều đó làm mất niềm tin của một số tín đồ SO như tôi. Đi đâu bây giờ?
0decimal0

2

Chỉ khi tiêu chuẩn thực hiện các thay đổi đột ngột và mã của bạn đột nhiên không còn là "không bao giờ được thực thi". Nhưng tôi không thấy bất kỳ cách hợp lý nào mà điều này có thể gây ra 'hành vi không xác định'. Nó không gây ra bất cứ điều gì .


2

Về chủ đề của hành vi không xác định, thường khó có thể tách các khía cạnh chính thức khỏi các khía cạnh thực tế. Đây là định nghĩa của hành vi không xác định trong tiêu chuẩn năm 1989 (tôi không có phiên bản mới hơn trong tay, nhưng tôi không mong đợi điều này đã thay đổi đáng kể):

1 hành vi không xác định
  hành vi, khi sử dụng cấu trúc chương trình không thể chuyển động hoặc có sai sót hoặc của
  dữ liệu sai, mà tiêu chuẩn này không áp dụng yêu cầu
2 LƯU Ý: Hành vi không xác định có thể xảy ra từ việc bỏ qua hoàn toàn tình huống
  với các kết quả không thể đoán trước, để hoạt động trong quá trình dịch hoặc thực thi chương trình
  theo cách được lập thành văn bản đặc trưng của môi trường (có hoặc không có
  phát hành một thông báo chẩn đoán), để kết thúc một bản dịch hoặc
  thực thi (với việc phát hành một thông báo chẩn đoán).

Từ một quan điểm chính thức, tôi muốn nói rằng chương trình của bạn thực hiện hành vi không xác định, có nghĩa là tiêu chuẩn không đặt ra bất kỳ yêu cầu nào về những gì nó sẽ làm khi chạy, chỉ vì nó chứa phép chia cho 0.

Mặt khác, từ quan điểm thực tế, tôi sẽ ngạc nhiên khi thấy một trình biên dịch không hoạt động như bạn mong đợi.


2

Tiêu chuẩn nói, như tôi nhớ đúng, nó được phép làm bất cứ điều gì ngay từ lúc này, một quy tắc đã bị phá vỡ. Có thể có một số sự kiện đặc biệt mang hương vị toàn cầu (nhưng tôi chưa bao giờ nghe hoặc đọc về điều gì đó tương tự) ... Vì vậy, tôi sẽ nói: Không, đây không thể là UB, bởi vì hành vi được xác định rõ ràng là 0 luôn luôn false, vì vậy quy tắc không thể bị phá vỡ trong thời gian chạy.


0 luôn đúng? hoặc thậm chí luôn đúng? bạn có phải là một số loại rubyist ?!
Grady Player

@Grady PlayerKhông, tôi là một loại Brain afk. Tôi sẽ sửa nó, xin lỗi
dhein

2

Tôi nghĩ đó vẫn là hành vi chưa được xác định, nhưng tôi không thể tìm thấy bất kỳ bằng chứng nào trong tiêu chuẩn để hỗ trợ hoặc phủ nhận tôi.

Tôi nghĩ rằng chương trình không gọi hành vi không xác định.

Báo cáo Lỗi # 109 giải quyết một câu hỏi tương tự và cho biết:

Hơn nữa, nếu mọi thực thi có thể có của một chương trình nhất định sẽ dẫn đến hành vi không xác định, thì chương trình đã cho không tuân thủ nghiêm ngặt. Việc triển khai tuân thủ không được không dịch một chương trình tuân thủ nghiêm ngặt đơn giản vì một số khả năng thực thi chương trình đó sẽ dẫn đến hành vi không xác định. Bởi vì foo có thể không bao giờ được gọi, ví dụ được đưa ra phải được dịch thành công bằng một triển khai tuân thủ.


-1

Nó phụ thuộc vào cách định nghĩa biểu thức "hành vi không xác định" và liệu "hành vi không xác định" của một câu lệnh có giống với "hành vi không xác định" cho một chương trình hay không.

Chương trình này trông giống như C, vì vậy phân tích sâu hơn về những gì tiêu chuẩn C được trình biên dịch sử dụng (như một số câu trả lời đã làm) là phù hợp.

Trong trường hợp không có tiêu chuẩn cụ thể, câu trả lời đúng là "nó phụ thuộc". Trong một số ngôn ngữ, các trình biên dịch sau lỗi đầu tiên cố gắng đoán ý của lập trình viên và vẫn tạo ra một số mã, theo suy đoán của trình biên dịch. Trong các ngôn ngữ khác, thuần túy hơn, một khi cái gì đó là không xác định, thì cái không xác định sẽ lan truyền đến toàn bộ chương trình.

Các ngôn ngữ khác có một khái niệm về "lỗi giới hạn". Đối với một số loại lỗi hạn chế, các ngôn ngữ này xác định mức độ thiệt hại mà một lỗi có thể gây ra. Trong các ngôn ngữ cụ thể với việc thu gom rác ngụ ý thường tạo ra sự khác biệt cho dù lỗi có làm mất hiệu lực hệ thống nhập hay khô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.