Các nhánh có hành vi không xác định có thể được giả định là không thể truy cập và tối ưu hóa như mã chết không?


88

Hãy xem xét tuyên bố sau:

*((char*)NULL) = 0; //undefined behavior

Nó rõ ràng gọi hành vi không xác định. Sự tồn tại của một câu lệnh như vậy trong một chương trình nhất định có nghĩa là toàn bộ chương trình là không xác định hoặc hành vi đó chỉ trở thành không xác định khi luồng điều khiển chạm vào câu lệnh này?

Chương trình sau có được xác định rõ trong trường hợp người dùng không bao giờ nhập số 3không?

while (true) {
 int num = ReadNumberFromConsole();
 if (num == 3)
  *((char*)NULL) = 0; //undefined behavior
}

Hay đó là hành vi hoàn toàn không xác định cho dù người dùng nhập nội dung gì?

Ngoài ra, trình biên dịch có thể giả định rằng hành vi không xác định sẽ không bao giờ được thực thi trong thời gian chạy không? Điều đó sẽ cho phép suy luận ngược thời gian:

int num = ReadNumberFromConsole();

if (num == 3) {
 PrintToConsole(num);
 *((char*)NULL) = 0; //undefined behavior
}

Ở đây, trình biên dịch có thể giải thích rằng trong trường hợp num == 3chúng ta sẽ luôn gọi hành vi không xác định. Do đó, trường hợp này phải là bất khả thi và số không cần in. Toàn bộ ifcâu lệnh có thể được tối ưu hóa. Loại suy luận ngược này có được phép theo tiêu chuẩn không?


19
đôi khi tôi tự hỏi liệu người dùng có nhiều đại diện có nhận được nhiều ủng hộ hơn cho các câu hỏi không vì "ồ họ có rất nhiều đại diện, đây hẳn là một câu hỏi hay" ... nhưng trong trường hợp này, tôi đọc câu hỏi và nghĩ "wow, điều này thật tuyệt "trước khi tôi nhìn vào người hỏi.
turbulencetoo

4
Tôi nghĩ rằng thời điểm hành vi không xác định xuất hiện, là không xác định.
eerorika

6
Tiêu chuẩn C ++ nói rõ ràng rằng một đường dẫn thực thi với hành vi không xác định tại bất kỳ điểm nào là hoàn toàn không xác định. Tôi thậm chí sẽ giải thích nó như nói rằng bất kỳ chương trình nào có hành vi không xác định trên đường dẫn là hoàn toàn không xác định (bao gồm các kết quả hợp lý trên các phần khác, nhưng nó không được đảm bảo). Các trình biên dịch được tự do sử dụng hành vi không xác định để sửa đổi chương trình của bạn. blog.llvm.org/2011/05/what-every-c-programmer-should-know.html chứa một số ví dụ hay.
Jens

4
@Jens: Nó thực sự có nghĩa là chỉ đường dẫn thực thi. Nếu không, bạn sẽ gặp rắc rối const int i = 0; if (i) 5/i;.
MSalters

1
Nói chung, trình biên dịch không thể chứng minh điều PrintToConsoleđó không gọi std::exitvì vậy nó phải thực hiện cuộc gọi.
MSalters

Câu trả lời:


65

Sự tồn tại của một câu lệnh như vậy trong một chương trình nhất định có nghĩa là toàn bộ chương trình là không xác định hoặc hành vi đó chỉ trở thành không xác định khi luồng điều khiển chạm vào câu lệnh này?

Cũng không. Điều kiện đầu tiên là quá mạnh và điều kiện thứ hai là quá yếu.

Việc truy cập đối tượng đôi khi được sắp xếp theo trình tự, nhưng tiêu chuẩn mô tả hoạt động của chương trình bên ngoài thời gian. Danvil đã được trích dẫn:

nếu bất kỳ quá trình thực thi nào như vậy chứa một thao tác không xác định, thì tiêu chuẩn này không yêu cầu việc triển khai thực hiện chương trình đó với đầu vào đó (thậm chí không liên quan đến các hoạt động trước thao tác không xác định đầu tiên)

Điều này có thể được hiểu:

Nếu việc thực thi chương trình mang lại hành vi không xác định, thì toàn bộ chương trình có hành vi không xác định.

Vì vậy, một tuyên bố không liên lạc được với UB không mang lại cho UB chương trình. Một câu lệnh có thể truy cập được (vì các giá trị của đầu vào) không bao giờ đạt được, không cung cấp cho chương trình UB. Đó là lý do tại sao điều kiện đầu tiên của bạn quá mạnh.

Bây giờ, trình biên dịch nói chung không thể nói những gì có UB. Vì vậy, để cho phép trình tối ưu hóa sắp xếp lại các câu lệnh với UB tiềm năng có thể sắp xếp lại thứ tự nếu hành vi của chúng được xác định, cần cho phép UB "quay ngược thời gian" và sai trước điểm trình tự trước đó (hoặc trong C ++ 11 thuật ngữ, để UB ảnh hưởng đến những thứ được sắp xếp trước thứ UB). Do đó tình trạng thứ hai của bạn quá yếu.

Một ví dụ chính của điều này là khi trình tối ưu hóa dựa vào răng cưa nghiêm ngặt. Toàn bộ điểm của các quy tắc bí danh nghiêm ngặt là cho phép trình biên dịch sắp xếp lại các hoạt động không thể được sắp xếp lại một cách hợp lệ nếu các con trỏ được đề cập có bí danh trên cùng một bộ nhớ. Vì vậy, nếu bạn sử dụng con trỏ răng cưa bất hợp pháp và UB xảy ra, thì nó có thể dễ dàng ảnh hưởng đến một câu lệnh "trước" câu lệnh UB. Đối với máy trừu tượng có liên quan, câu lệnh UB vẫn chưa được thực thi. Theo như mã đối tượng thực tế có liên quan, nó đã được thực thi một phần hoặc toàn bộ. Nhưng tiêu chuẩn không cố gắng đi vào chi tiết về ý nghĩa của việc trình tối ưu hóa sắp xếp lại các câu lệnh hoặc ý nghĩa của điều đó đối với UB. Nó chỉ đưa ra giấy phép triển khai sai ngay khi nó vừa ý.

Bạn có thể nghĩ về điều này là, "UB có một cỗ máy thời gian".

Cụ thể để trả lời các ví dụ của bạn:

  • Hành vi chỉ là không xác định nếu 3 được đọc.
  • Các trình biên dịch có thể và thực hiện loại bỏ mã như đã chết nếu một khối cơ bản chứa một hoạt động nhất định chưa được xác định. Chúng được phép (và tôi đoán là có) trong những trường hợp không phải là một khối cơ bản nhưng ở đó tất cả các nhánh đều dẫn đến UB. Ví dụ này không phải là một ứng cử viên trừ khi PrintToConsole(3)bằng cách nào đó được biết là chắc chắn sẽ quay trở lại. Nó có thể ném ra một ngoại lệ hoặc bất cứ điều gì.

Một ví dụ tương tự như tùy chọn thứ hai của bạn là tùy chọn gcc -fdelete-null-pointer-checks, có thể lấy mã như thế này (Tôi chưa kiểm tra ví dụ cụ thể này, hãy coi nó là minh họa cho ý tưởng chung):

void foo(int *p) {
    if (p) *p = 3;
    std::cout << *p << '\n';
}

và thay đổi nó thành:

*p = 3;
std::cout << "3\n";

Tại sao? Bởi vì nếu plà null thì mã vẫn có UB, vì vậy trình biên dịch có thể cho rằng nó không phải là null và tối ưu hóa cho phù hợp. Kernel linux vấp này ( https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2009-1897 ) chủ yếu bởi vì nó hoạt động trong một chế độ mà dereferencing một con trỏ null không phải là UB, dự kiến ​​sẽ dẫn đến một ngoại lệ phần cứng được xác định mà hạt nhân có thể xử lý. Khi tối ưu hóa được bật, gcc yêu cầu sử dụng -fno-delete-null-pointer-checksđể đảm bảo vượt quá tiêu chuẩn đó.

Tái bút Câu trả lời thiết thực cho câu hỏi "khi nào thì hành vi không xác định xảy ra?" là "10 phút trước khi bạn dự định đi trong ngày".


4
Trên thực tế, đã có khá nhiều vấn đề bảo mật do điều này trong quá khứ. Đặc biệt, bất kỳ kiểm tra tràn sau thực tế nào đều có nguy cơ bị tối ưu hóa do điều này. Ví dụ void can_add(int x) { if (x + 100 < x) complain(); }có thể được tối ưu hóa hoàn toàn, bởi vì nếu x+100 không tràn thì không có gì xảy ra và nếu x+100 tràn, đó là UB theo tiêu chuẩn, vì vậy không có gì có thể xảy ra.
fgp

3
@fgp: đúng, đó là một sự tối ưu hóa mà mọi người phàn nàn một cách cay đắng nếu họ lướt qua nó, bởi vì nó bắt đầu có cảm giác như trình biên dịch đang cố tình phá mã của bạn để trừng phạt bạn. "Tại sao tôi lại viết nó theo cách đó nếu tôi muốn bạn xóa nó!" ;-) Nhưng tôi nghĩ rằng đôi khi nó hữu ích cho trình tối ưu hóa khi thao tác các biểu thức số học lớn hơn, để giả sử không có lỗi tràn và tránh bất kỳ thứ gì đắt tiền chỉ cần thiết trong những trường hợp đó.
Steve Jessop

2
Có đúng không khi nói rằng chương trình không phải là không xác định nếu người dùng không bao giờ nhập 3, nhưng nếu anh ta nhập 3 trong khi thực hiện thì toàn bộ quá trình thực thi trở thành không xác định? Ngay sau khi chắc chắn 100% rằng chương trình sẽ gọi hành vi không xác định (và không sớm hơn thế), hành vi sẽ được phép trở thành bất kỳ thứ gì. Những câu nói này của tôi có đúng 100% không?
usr

3
@usr: Tôi tin rằng điều đó chính xác, vâng. Với ví dụ cụ thể của bạn (và đưa ra một số giả định về tính chắc chắn của dữ liệu đang được xử lý), về nguyên tắc, một triển khai có thể xem xét trước trong STDIN được đệm 3nếu muốn và chuyển về nhà trong ngày ngay khi nhìn thấy mới đến.
Steve Jessop

3
Thêm +1 (nếu tôi có thể) cho PS của bạn
Fred Larson

10

Các trạng thái tiêu chuẩn ở 1,9 / 4

[Lưu ý: Tiêu chuẩn này không áp đặt yêu cầu đối với hoạt động của các chương trình có chứa hành vi không xác định. - ghi chú cuối]

Điểm thú vị có lẽ là "chứa" nghĩa là gì. Sau đó một chút ở 1,9 / 5 nó nói:

Tuy nhiên, nếu bất kỳ quá trình thực hiện nào như vậy chứa một thao tác không xác định, thì tiêu chuẩn này không đặt ra yêu cầu đối với việc triển khai thực hiện chương trình đó với đầu vào đó (ngay cả đối với các thao tác trước thao tác không xác định đầu tiên)

Ở đây nó đặc biệt đề cập đến "thực thi ... với đầu vào đó". Tôi sẽ giải thích rằng, hành vi không xác định trong một nhánh có thể không được thực thi ngay bây giờ không ảnh hưởng đến nhánh thực thi hiện tại.

Tuy nhiên, một vấn đề khác là các giả định dựa trên hành vi không xác định trong quá trình tạo mã. Hãy xem câu trả lời của Steve Jessop để biết thêm chi tiết về điều đó.


1
Nếu hiểu theo nghĩa đen, đó là bản án tử hình cho tất cả các chương trình thực đang tồn tại.
usr

6
Tôi không nghĩ câu hỏi là nếu UB có thể xuất hiện trước khi mã thực sự được sử dụng. Theo tôi hiểu, câu hỏi đặt ra là UB có thể xuất hiện nếu mã thậm chí không đạt được. Và tất nhiên câu trả lời cho điều đó là "không".
sepp2k

Tiêu chuẩn không quá rõ ràng về điều này trong 1,9 / 4, nhưng 1,9 / 5 có thể được hiểu như những gì bạn đã nói.
Danvil

1
Ghi chú không mang tính quy chuẩn. 1,9 / 5 vượt trội hơn nốt ở 1,9 / 4
MSalters

5

Một ví dụ hướng dẫn là

int foo(int x)
{
    int a;
    if (x)
        return a;
    return 0;
}

Cả GCC hiện tại và Clang hiện tại sẽ tối ưu hóa điều này (trên x86) để

xorl %eax,%eax
ret

bởi vì họ suy ra rằng xluôn luôn bằng không từ UB trong if (x)đường dẫn điều khiển. GCC thậm chí sẽ không đưa ra cảnh báo về việc sử dụng giá trị chưa được khởi tạo! (vì pass áp dụng logic trên chạy trước pass tạo ra cảnh báo giá trị chưa được khởi tạo)


1
Ví dụ thú vị. Thật là khó chịu khi bật tối ưu hóa lại ẩn cảnh báo. Điều này thậm chí còn không được ghi lại - tài liệu GCC chỉ nói rằng việc bật tối ưu hóa sẽ tạo ra nhiều cảnh báo hơn .
sleske

@sleske Thật là khó chịu, tôi đồng ý, nhưng các cảnh báo giá trị chưa được khởi tạo nổi tiếng là khó "đúng" - thực hiện chúng một cách hoàn hảo tương đương với Vấn đề tạm dừng và các lập trình viên cảm thấy phi lý một cách kỳ lạ khi thêm các khởi tạo biến "không cần thiết" để loại bỏ các kết quả dương tính giả, vì vậy các tác giả trình biên dịch kết thúc được quá một thùng. Tôi đã từng hack trên GCC và tôi nhớ lại rằng mọi người đều sợ hãi khi làm phiền với thẻ cảnh báo-giá trị chưa được khởi tạo.
zwol

@zwol: Tôi tự hỏi có bao nhiêu phần trăm "tối ưu hóa" do loại bỏ mã chết như vậy thực sự làm cho mã hữu ích nhỏ hơn và kết quả là bao nhiêu khiến các lập trình viên làm cho mã lớn hơn (ví dụ: bằng cách thêm mã để khởi tạo angay cả khi trong mọi trường hợp uninitialized asẽ được chuyển đến hàm mà hàm sẽ không bao giờ làm gì với nó)?
supercat

@supercat Tôi đã không tham gia sâu vào công việc biên dịch trong ~ 10 năm và hầu như không thể lý giải về việc tối ưu hóa từ các ví dụ đồ chơi. Đây loại của tối ưu hóa có xu hướng liên kết với 2-5% giảm tổng thể mã kích thước trên các ứng dụng thực tế, nếu tôi nhớ không lầm.
zwol

1
@supercat 2-5% là rất lớn khi những thứ này diễn ra. Tôi đã thấy mọi người đổ mồ hôi 0,1%.
zwol

4

Bản nháp làm việc C ++ hiện tại cho biết trong 1.9.4 rằng

Tiêu chuẩn này không đặt ra yêu cầu nào đối với hoạt động của các chương trình có chứa hành vi không đúng.

Dựa trên điều này, tôi sẽ nói rằng một chương trình chứa hành vi không xác định trên bất kỳ đường dẫn thực thi nào có thể làm bất cứ điều gì tại mỗi thời điểm thực thi của nó.

Có hai bài viết thực sự tốt về hành vi không xác định và những gì trình biên dịch thường làm:


1
Điều đó không có ý nghĩa. Hàm int f(int x) { if (x > 0) return 100/x; else return 100; }chắc chắn không bao giờ gọi hành vi không xác định, mặc dù 100/0tất nhiên là không xác định.
fgp

1
@fgp Tuy nhiên, tiêu chuẩn (đặc biệt là 1.9 / 5) nói rằng nếu thể đạt được hành vi không xác định , thì không quan trọng khi nào đạt được. Ví dụ, printf("Hello, World"); *((char*)NULL) = 0 không được đảm bảo để in bất kỳ thứ gì. Điều này hỗ trợ tối ưu hóa, bởi vì trình biên dịch có thể tự do sắp xếp lại các hoạt động (tất nhiên là tuân theo các ràng buộc phụ thuộc) mà nó biết rằng cuối cùng sẽ xảy ra, mà không cần phải tính đến hành vi không xác định.
fgp

Tôi muốn nói rằng một chương trình có chức năng của bạn không chứa hành vi không xác định, bởi vì không có đầu vào mà 100/0 sẽ được đánh giá.
Jens

1
Chính xác - vì vậy vấn đề là liệu UB có thể thực sự được kích hoạt hay không, chứ không phải liệu nó có thể được kích hoạt về mặt lý thuyết hay không. Hoặc bạn đã sẵn sàng tranh luận rằng int x,y; std::cin >> x >> y; std::cout << (x+y);được phép nói rằng "1 + 1 = 17", chỉ vì có một số đầu vào mà x+ytràn (là UB vì intlà kiểu có dấu).
fgp

Về mặt hình thức, tôi muốn nói rằng chương trình có hành vi không xác định vì tồn tại các đầu vào kích hoạt nó. Nhưng bạn nói đúng rằng điều này không có ý nghĩa trong ngữ cảnh của C ++, vì sẽ không thể viết một chương trình mà không có hành vi không xác định. Tôi thích nó khi có ít hành vi không xác định hơn trong C ++, nhưng đó không phải là cách ngôn ngữ hoạt động (và có một số lý do chính đáng cho điều này, nhưng chúng không liên quan đến việc sử dụng hàng ngày của tôi ...).
Jens

3

Từ "hành vi" có nghĩa là một cái gì đó đang được thực hiện . Statemenr không bao giờ được thực thi không phải là "hành vi".

Sự minh họa:

*ptr = 0;

Đó có phải là hành vi không xác định? Giả sử chúng ta chắc chắn 100% ptr == nullptrít nhất một lần trong quá trình thực hiện chương trình. Câu trả lời phải là có.

Cái này thì sao?

 if (ptr) *ptr = 0;

Đó là không xác định? (Nhớ ptr == nullptrít nhất một lần?) Tôi chắc chắn hy vọng là không, nếu không bạn sẽ không thể viết bất kỳ chương trình hữu ích nào cả.

Không có srandardese nào bị hại khi đưa ra câu trả lời này.


3

Hành vi không xác định xảy ra khi chương trình sẽ gây ra hành vi không xác định cho dù điều gì xảy ra tiếp theo. Tuy nhiên, bạn đã đưa ra ví dụ sau.

int num = ReadNumberFromConsole();

if (num == 3) {
 PrintToConsole(num);
 *((char*)NULL) = 0; //undefined behavior
}

Trừ khi trình biên dịch biết định nghĩa của PrintToConsole, nó không thể loại bỏ if (num == 3)điều kiện. Giả sử rằng bạn có LongAndCamelCaseStdio.htiêu đề hệ thống với khai báo sau là PrintToConsole.

void PrintToConsole(int);

Không có gì quá hữu ích, được rồi. Bây giờ, chúng ta hãy xem nhà cung cấp có thể tệ hơn như thế nào (hoặc có lẽ không quá ác, hành vi không xác định có thể tồi tệ hơn) bằng cách kiểm tra định nghĩa thực tế của hàm này.

int printf(const char *, ...);
void exit(int);

void PrintToConsole(int num) {
    printf("%d\n", num);
    exit(0);
}

Trình biên dịch thực sự phải giả định rằng bất kỳ chức năng tùy ý nào mà trình biên dịch không biết nó làm gì có thể thoát hoặc ném một ngoại lệ (trong trường hợp của C ++). Bạn có thể nhận thấy rằng điều đó *((char*)NULL) = 0;sẽ không được thực thi, vì quá trình thực thi sẽ không tiếp tục sau khi PrintToConsolegọi.

Hành vi không xác định sẽ xảy ra khi PrintToConsolethực sự trả về. Trình biên dịch hy vọng điều này sẽ không xảy ra (vì điều này sẽ khiến chương trình thực thi hành vi không xác định bất kể điều gì), do đó bất cứ điều gì có thể xảy ra.

Tuy nhiên, chúng ta hãy xem xét một cái gì đó khác. Giả sử chúng ta đang thực hiện kiểm tra null và sử dụng biến sau khi kiểm tra null.

int putchar(int);

const char *warning;

void lol_null_check(const char *pointer) {
    if (!pointer) {
        warning = "pointer is null";
    }
    putchar(*pointer);
}

Trong trường hợp này, thật dễ dàng nhận thấy lol_null_checkyêu cầu con trỏ không phải NULL. Việc gán cho warningbiến không biến động toàn cục không phải là thứ có thể thoát khỏi chương trình hoặc ném ra bất kỳ ngoại lệ nào. Cácpointer này cũng không thay đổi, vì vậy nó không thể thay đổi giá trị một cách kỳ diệu ở giữa chức năng (nếu có, đó là hành vi không xác định). Việc gọi lol_null_check(NULL)sẽ gây ra hành vi không xác định có thể khiến biến không được gán (vì tại thời điểm này, thực tế là chương trình thực hiện hành vi không xác định đã biết).

Tuy nhiên, hành vi không xác định có nghĩa là chương trình có thể làm bất cứ điều gì. Do đó, không có gì ngăn hành vi không xác định quay ngược thời gian và làm hỏng chương trình của bạn trước khi int main()thực thi dòng đầu tiên . Đó là hành vi không xác định, không nhất thiết phải có ý nghĩa. Nó cũng có thể gặp sự cố sau khi nhập 3, nhưng hành vi không xác định sẽ quay ngược thời gian và gặp sự cố trước khi bạn nhập 3. Và ai biết được, có lẽ hành vi không xác định sẽ ghi đè lên RAM hệ thống của bạn và khiến hệ thống của bạn gặp sự cố 2 tuần sau đó, trong khi chương trình không xác định của bạn không chạy.


Tất cả các điểm hợp lệ. PrintToConsolelà nỗ lực của tôi trong việc chèn một hiệu ứng phụ bên ngoài chương trình có thể nhìn thấy ngay cả sau khi gặp sự cố và được sắp xếp theo trình tự mạnh mẽ. Tôi muốn tạo ra một tình huống để chúng ta có thể biết chắc chắn liệu tuyên bố này có được tối ưu hóa hay không. Nhưng bạn đúng ở chỗ nó có thể không bao giờ quay trở lại; Ví dụ của bạn về việc viết thư cho toàn cầu có thể phải tuân theo các tối ưu hóa khác không liên quan đến UB. Ví dụ, một toàn cầu không sử dụng có thể bị xóa. Bạn có ý tưởng tạo ra một tác dụng phụ bên ngoài theo cách được đảm bảo để kiểm soát trở lại không?
usr

Có thể tạo ra bất kỳ tác dụng phụ nào có thể quan sát được bên ngoài bằng mã mà trình biên dịch có thể tự do giả định lợi nhuận không? Theo sự hiểu biết của tôi, ngay cả một phương thức chỉ đọc một volatilebiến cũng có thể kích hoạt một cách hợp pháp hoạt động I / O mà có thể ngay lập tức làm gián đoạn luồng hiện tại; trình xử lý ngắt sau đó có thể giết luồng trước khi nó có cơ hội thực hiện bất kỳ điều gì khác. Tôi không thấy lý do nào mà trình biên dịch có thể đẩy hành vi không xác định trước thời điểm đó.
supercat

Từ quan điểm của tiêu chuẩn C, sẽ không có gì là bất hợp pháp khi có Hành vi không xác định khiến máy tính gửi thông báo đến một số người sẽ theo dõi và phá hủy tất cả bằng chứng về các hành động trước đó của chương trình, nhưng nếu một hành động có thể chấm dứt một chuỗi, thì mọi thứ được trình tự trước khi hành động đó sẽ phải xảy ra trước bất kỳ Hành vi không xác định nào xảy ra sau hành động đó.
supercat

1

Nếu chương trình đạt đến một câu lệnh gọi hành vi không xác định, không có yêu cầu nào được đặt lên bất kỳ đầu ra / hành vi nào của chương trình; không quan trọng là chúng sẽ diễn ra "trước" hay "sau" hành vi không xác định được gọi ra.

Suy luận của bạn về cả ba đoạn mã đều đúng. Cụ thể, một trình biên dịch có thể xử lý bất kỳ câu lệnh nào gọi vô điều kiện hành vi không xác định theo cách GCC xử lý __builtin_unreachable(): như một gợi ý tối ưu hóa rằng câu lệnh không thể truy cập được (và do đó, tất cả các đường dẫn mã dẫn đến nó một cách vô điều kiện cũng không thể truy cập được). Tất nhiên là có thể thực hiện các tối ưu hóa tương tự khác.


1
Vì tò mò, từ khi nào __builtin_unreachable()bắt đầu có các hiệu ứng tiến hành cả lùi và tiến trong thời gian? Đưa ra một cái gì đó như extern volatile uint32_t RESET_TRIGGER; void RESET(void) { RESET_TRIGGER = 0xAA55; __memorybarrier(); __builtin_unreachable(); }tôi có thể thấy builtin_unreachable()là tốt khi cho trình biên dịch biết rằng nó có thể bỏ qua returnhướng dẫn, nhưng điều đó sẽ khác với việc nói rằng mã trước đó có thể bị bỏ qua.
supercat

@supercat vì RESET_TRIGGER dễ bay hơi nên việc ghi vào vị trí đó có thể có các tác dụng phụ tùy ý. Đối với trình biên dịch, nó giống như một cuộc gọi phương thức không rõ ràng. Do đó, nó không thể được chứng minh (và không phải trường hợp) __builtin_unreachablelà đạt được. Chương trình này được xác định.
usr

@usr: Tôi nghĩ rằng các trình biên dịch cấp thấp nên coi các truy cập dễ bay hơi là các lệnh gọi phương thức không rõ ràng, nhưng cả clang và gcc đều không làm như vậy. Trong số những thứ khác, một lệnh gọi phương thức không rõ ràng có thể khiến tất cả các byte của bất kỳ đối tượng nào có địa chỉ đã được tiếp xúc với thế giới bên ngoài và những đối tượng chưa được hoặc sẽ được truy cập bởi restrictcon trỏ trực tiếp , được viết bằng cách sử dụng một unsigned char*.
supercat

@usr: Nếu một trình biên dịch không coi một truy cập dễ bay hơi là một lệnh gọi phương thức không rõ ràng liên quan đến các truy cập vào các đối tượng được tiếp xúc, tôi không thấy lý do cụ thể nào để mong đợi rằng nó sẽ làm như vậy cho các mục đích khác. Tiêu chuẩn không yêu cầu việc triển khai phải làm như vậy, bởi vì có một số nền tảng phần cứng mà trình biên dịch có thể biết tất cả các tác động có thể có từ một truy cập dễ bay hơi. Tuy nhiên, một trình biên dịch thích hợp để sử dụng nhúng phải nhận ra rằng các truy cập dễ bay hơi có thể kích hoạt phần cứng chưa được phát minh khi trình biên dịch được viết.
supercat

@supercat Tôi nghĩ bạn đúng. Có vẻ như các hoạt động dễ bay hơi "không ảnh hưởng đến máy trừu tượng" và do đó không thể chấm dứt chương trình hoặc gây ra các tác dụng phụ.
usr

1

Nhiều tiêu chuẩn cho nhiều loại thứ tốn rất nhiều công sức vào việc mô tả những thứ mà việc triển khai NÊN hoặc KHÔNG NÊN làm, sử dụng danh pháp tương tự như được định nghĩa trong IETF RFC 2119 (mặc dù không nhất thiết phải trích dẫn các định nghĩa trong tài liệu đó). Trong nhiều trường hợp, mô tả về những thứ mà việc triển khai phải làm ngoại trừ những trường hợp chúng vô dụng hoặc không thực tế, quan trọng hơn các yêu cầu mà tất cả triển khai phù hợp phải tuân theo.

Thật không may, Tiêu chuẩn C và C ++ có xu hướng tránh mô tả về những thứ, mặc dù không bắt buộc 100%, nhưng vẫn nên được mong đợi về việc triển khai chất lượng không ghi lại hành vi trái ngược. Một gợi ý rằng việc triển khai nên làm điều gì đó có thể được coi là ngụ ý rằng những hành vi không kém hơn và trong những trường hợp nhìn chung sẽ rõ ràng những hành vi nào sẽ hữu ích hoặc thiết thực, so với không thực tế và vô ích, trên một triển khai nhất định, có ít nhận thấy rằng Tiêu chuẩn can thiệp vào các phán đoán như vậy.

Một trình biên dịch thông minh có thể tuân theo Tiêu chuẩn trong khi loại bỏ bất kỳ mã nào sẽ không có tác dụng ngoại trừ khi mã nhận được đầu vào chắc chắn sẽ gây ra Hành vi không xác định, nhưng "thông minh" và "ngu ngốc" không phải là từ trái nghĩa. Việc các tác giả của Tiêu chuẩn quyết định rằng có thể có một số kiểu triển khai mà hành vi hữu ích trong một tình huống nhất định sẽ là vô ích và không thực tế không ngụ ý bất kỳ đánh giá nào về việc liệu những hành vi đó có được coi là thực tế và hữu ích đối với những người khác hay không. Nếu việc triển khai có thể duy trì đảm bảo hành vi mà không mất chi phí nào ngoài việc mất cơ hội cắt tỉa "nhánh chết", thì hầu như mọi mã giá trị mà người dùng có thể nhận được từ đảm bảo đó sẽ vượt quá chi phí cung cấp. Loại bỏ nhánh chết có thể tốt trong trường hợp không, Nhưng nếu trong một mã số sử dụng tình huống cụ thể có thể xử lý hầu hết các hành vi có thể khác so với loại bỏ chết ngành, bất kỳ mã dùng nỗ lực sẽ phải rộng để tránh UB có khả năng sẽ vượt quá giá trị đạt từ DBE.


Đó là một điểm tốt là tránh UB có thể áp đặt chi phí cho mã người dùng.
usr

@usr: Đó là một điểm mà những người theo chủ nghĩa hiện đại hoàn toàn bỏ sót. Tôi có nên thêm một ví dụ? Ví dụ: nếu mã cần đánh giá x*y < zkhi x*ynào không bị tràn và trong trường hợp tràn có kết quả là 0 hoặc 1 theo kiểu tùy ý nhưng không có tác dụng phụ, thì không có lý do gì trên hầu hết các nền tảng tại sao việc đáp ứng các yêu cầu thứ hai và thứ ba lại đắt hơn đáp ứng cách đầu tiên, nhưng bất kỳ cách viết biểu thức nào để đảm bảo hành vi được xác định theo Tiêu chuẩn trong mọi trường hợp sẽ làm tăng thêm chi phí đáng kể. Viết biểu thức (int64_t)x*y < zcó thể nhiều hơn gấp bốn lần chi phí tính toán ...
supercat

... trên một số nền tảng và việc viết nó như (int)((unsigned)x*y) < zvậy sẽ ngăn trình biên dịch sử dụng những gì có thể là những thay thế đại số hữu ích (ví dụ: nếu nó biết điều đó xzbằng nhau và dương, nó có thể đơn giản hóa biểu thức ban đầu thành y<0, nhưng phiên bản sử dụng không dấu sẽ buộc trình biên dịch thực hiện phép nhân). Nếu trình biên dịch có thể đảm bảo mặc dù Tiêu chuẩn không bắt buộc, nó sẽ duy trì yêu cầu "năng suất 0 hoặc 1 mà không có tác dụng phụ", mã người dùng có thể cung cấp cho trình biên dịch các cơ hội tối ưu hóa mà nó không thể có được.
supercat

Vâng, có vẻ như một số dạng hành vi không xác định nhẹ hơn sẽ hữu ích ở đây. Lập trình viên có thể bật một chế độ khiến x*yphát ra giá trị bình thường trong trường hợp tràn nhưng bất kỳ giá trị nào. UB có thể định cấu hình trong C / C ++ có vẻ quan trọng đối với tôi.
usr

@usr: Nếu các tác giả của Tiêu chuẩn C89 thành thật khi nói rằng việc quảng bá các giá trị không dấu ngắn để đăng nhập là thay đổi vi phạm nghiêm trọng nhất và không phải là những kẻ ngu dốt, thì điều đó có nghĩa là họ mong đợi điều đó trong trường hợp các nền tảng có đã xác định các đảm bảo hành vi hữu ích, việc triển khai cho các nền tảng đó đã và đang cung cấp các đảm bảo đó cho các lập trình viên và các lập trình viên đã khai thác chúng, các trình biên dịch cho các nền tảng đó sẽ tiếp tục cung cấp các đảm bảo hành vi đó cho dù Tiêu chuẩn có ra lệnh cho họ hay 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.