Tại sao f (i = -1, i = -1) hành vi không xác định?


267

Tôi đã đọc về thứ tự vi phạm đánh giá , và họ đưa ra một ví dụ đánh đố tôi.

1) Nếu một hiệu ứng phụ trên một đối tượng vô hướng không được giải trình tự so với hiệu ứng phụ khác trên cùng một đối tượng vô hướng, thì hành vi không được xác định.

// snip
f(i = -1, i = -1); // undefined behavior

Trong bối cảnh này, ilà một đối tượng vô hướng , có nghĩa là rõ ràng

Các loại số học (3.9.1), loại liệt kê, loại con trỏ, con trỏ đến loại thành viên (3.9.2), std :: nullptr_t và các phiên bản đủ điều kiện cv của các loại này (3.9.3) được gọi chung là các loại vô hướng.

Tôi không thấy cách tuyên bố mơ hồ trong trường hợp đó. Đối với tôi, dường như bất kể đối số thứ nhất hay thứ hai được đánh giá đầu tiên, ikết thúc là -1và cả hai đối số cũng vậy -1.

Ai đó có thể vui lòng làm rõ?


CẬP NHẬT

Tôi thực sự đánh giá cao tất cả các cuộc thảo luận. Cho đến nay, tôi thích câu trả lời của @ Harmic rất nhiều vì nó phơi bày những cạm bẫy và rắc rối của việc xác định tuyên bố này mặc dù nó nhìn thẳng về phía trước như thế nào. @ acheong87 chỉ ra một số vấn đề xuất hiện khi sử dụng tài liệu tham khảo, nhưng tôi nghĩ đó là trực giao với khía cạnh tác dụng phụ chưa được giải đáp của câu hỏi này.


TÓM LƯỢC

Vì câu hỏi này có rất nhiều sự chú ý, tôi sẽ tóm tắt những điểm / câu trả lời chính. Đầu tiên, cho phép tôi giải thích nhỏ để chỉ ra rằng "tại sao" có thể có ý nghĩa khác nhau nhưng có ý nghĩa khác nhau, cụ thể là "vì lý do gì ", "vì lý do gì " và "vì mục đích gì ". Tôi sẽ nhóm các câu trả lời theo nghĩa nào trong số những ý nghĩa của "tại sao" mà chúng giải quyết.

vì lý do gì

Câu trả lời chính ở đây đến từ Paul Draper , với Martin J đóng góp một câu trả lời tương tự nhưng không rộng rãi. Câu trả lời của Paul Draper sôi sùng sục

Đó là hành vi không xác định bởi vì nó không được xác định hành vi là gì.

Câu trả lời là rất tốt về mặt giải thích những gì tiêu chuẩn C ++ nói. Nó cũng giải quyết một số trường hợp liên quan của UB như f(++i, ++i);f(i=1, i=-1);. Trong trường hợp đầu tiên của các trường hợp liên quan, không rõ liệu đối số thứ nhất nên là i+1và đối số thứ hai i+2hay ngược lại; trong lần thứ hai, không rõ ilà 1 hay -1 sau khi gọi hàm. Cả hai trường hợp này đều là UB vì chúng thuộc quy tắc sau:

Nếu một tác dụng phụ trên một đối tượng vô hướng không bị ảnh hưởng so với tác dụng phụ khác trên cùng một đối tượng vô hướng, thì hành vi không được xác định.

Do đó, f(i=-1, i=-1)cũng là UB vì nó thuộc cùng một quy tắc, mặc dù ý định của lập trình viên là (IMHO) rõ ràng và không mơ hồ.

Paul Draper cũng làm cho nó rõ ràng trong kết luận của mình rằng

Nó có thể đã được xác định hành vi? Đúng. Nó đã được định nghĩa? Không.

điều này đưa chúng ta đến câu hỏi "vì lý do / mục đích nào còn f(i=-1, i=-1)lại là hành vi không xác định?"

vì lý do / mục đích gì

Mặc dù có một số lỗi quá mức (có thể bất cẩn) trong tiêu chuẩn C ++, nhiều thiếu sót được suy luận hợp lý và phục vụ một mục đích cụ thể. Mặc dù tôi biết rằng mục đích thường là "làm cho công việc của trình biên dịch dễ dàng hơn" hoặc "mã nhanh hơn", nhưng tôi chủ yếu quan tâm để biết liệu có lý do chính đáng nào để lại f(i=-1, i=-1) là UB không.

harmicsupercat cung cấp câu trả lời chính mà cung cấp một lý do cho UB. Harmic chỉ ra rằng một trình biên dịch tối ưu hóa có thể phá vỡ các hoạt động gán nguyên tử rõ ràng thành nhiều lệnh máy và nó có thể xen kẽ các hướng dẫn đó để có tốc độ tối ưu. Điều này có thể dẫn đến một số kết quả rất đáng ngạc nhiên: ikết thúc là -2 trong kịch bản của anh ấy! Do đó, tác hại cho thấy cách gán cùng một giá trị cho một biến nhiều lần có thể có tác động xấu nếu các hoạt động không được thực hiện.

supercat cung cấp một giải trình có liên quan về những cạm bẫy của việc cố gắng f(i=-1, i=-1)làm những gì có vẻ như nó phải làm. Ông chỉ ra rằng trên một số kiến ​​trúc, có những hạn chế cứng đối với nhiều lần ghi đồng thời vào cùng một địa chỉ bộ nhớ. Một trình biên dịch có thể gặp khó khăn trong việc nắm bắt điều này nếu chúng ta đang xử lý một cái gì đó tầm thường hơn f(i=-1, i=-1).

davidf cũng cung cấp một ví dụ về các hướng dẫn xen kẽ rất giống với các tác hại.

Mặc dù mỗi ví dụ của các tác nhân gây hại, supercat và davidf 'có phần bị chiếm đoạt, chúng được kết hợp với nhau để cung cấp một lý do hữu hình tại sao f(i=-1, i=-1)nên không xác định hành vi.

Tôi chấp nhận câu trả lời có hại vì nó đã làm tốt nhất việc giải quyết tất cả các ý nghĩa tại sao, mặc dù câu trả lời của Paul Draper đã giải quyết phần "vì lý do gì" tốt hơn.

câu trả lời khác

JohnB chỉ ra rằng nếu chúng ta xem xét các toán tử gán quá tải (thay vì chỉ là vô hướng đơn giản), thì chúng ta cũng có thể gặp rắc rối.


1
Một đối tượng vô hướng là một đối tượng của kiểu vô hướng. Xem 3.9 / 9: "Các loại số học (3.9.1), các kiểu liệt kê, các loại con trỏ, các con trỏ đến các loại thành viên (3.9.2) std::nullptr_tvà các phiên bản đủ điều kiện cv của các loại này (3.9.3) được gọi chung là các loại vô hướng . "
Rob Kennedy

1
Có thể có một lỗi trên trang và chúng thực sự có nghĩa f(i-1, i = -1)hoặc một cái gì đó tương tự.
Ông Lister


@RobKennedy Cảm ơn. "Các loại số học" có bao gồm bool không?
Nicu Stiurca

1
SchighSchagh cập nhật của bạn nên ở phần trả lời.
Grijesh Chauhan

Câu trả lời:


343

Vì các hoạt động là không có kết quả, không có gì để nói rằng các hướng dẫn thực hiện nhiệm vụ không thể được xen kẽ. Nó có thể là tối ưu để làm như vậy, tùy thuộc vào kiến ​​trúc CPU. Trang được tham chiếu cho biết điều này:

Nếu A không được giải trình tự trước B và B không được giải trình tự trước A, thì có hai khả năng tồn tại:

  • các đánh giá của A và B không được thực hiện: chúng có thể được thực hiện theo bất kỳ thứ tự nào và có thể trùng lặp (trong một luồng thực thi, trình biên dịch có thể xen kẽ các lệnh CPU bao gồm A và B)

  • các đánh giá của A và B được sắp xếp theo trình tự không xác định: chúng có thể được thực hiện theo bất kỳ thứ tự nào nhưng có thể không trùng nhau: A sẽ hoàn thành trước B, hoặc B sẽ hoàn thành trước A. Biểu thức có thể ngược lại vào lần tiếp theo cùng biểu thức được đánh giá.

Điều đó tự nó dường như không gây ra sự cố - giả sử rằng thao tác đang được thực hiện đang lưu trữ giá trị -1 vào một vị trí bộ nhớ. Nhưng cũng không có gì để nói rằng trình biên dịch không thể tối ưu hóa nó thành một tập lệnh riêng biệt có cùng tác dụng, nhưng có thể thất bại nếu hoạt động được xen kẽ với một hoạt động khác trên cùng một vị trí bộ nhớ.

Ví dụ, hãy tưởng tượng rằng nó hiệu quả hơn bằng 0 bộ nhớ, sau đó giảm dần nó, so với việc tải giá trị -1 in. Sau đó, điều này:

f(i=-1, i=-1)

có thể trở thành:

clear i
clear i
decr i
decr i

Bây giờ tôi là -2.

Nó có thể là một ví dụ không có thật, nhưng nó có thể.


59
Ví dụ rất hay về cách biểu thức thực sự có thể làm điều gì đó bất ngờ trong khi tuân thủ các quy tắc tuần tự. Vâng, một chút giả định, nhưng mã đầu tiên tôi đang hỏi về vấn đề này. :)
Nicu Stiurca

10
Và ngay cả khi việc chuyển nhượng được thực hiện như một hoạt động nguyên tử, có thể hình dung ra một kiến ​​trúc siêu khối trong đó cả hai nhiệm vụ được thực hiện đồng thời gây ra xung đột truy cập bộ nhớ dẫn đến thất bại. Ngôn ngữ được thiết kế sao cho người viết trình biên dịch có nhiều tự do nhất có thể trong việc sử dụng các lợi thế của máy mục tiêu.
ach

11
Tôi thực sự thích ví dụ của bạn về cách thậm chí gán cùng một giá trị cho cùng một biến trong cả hai tham số có thể dẫn đến kết quả không mong muốn vì hai phép gán không được xử lý
Martin J.

1
+ 1e + 6 (ok, +1) cho điểm mà mã được biên dịch không phải lúc nào cũng như bạn mong đợi. Trình tối ưu hóa thực sự rất tốt trong việc ném các loại đường cong này vào bạn khi bạn không tuân theo các quy tắc: P
Corey

3
Trên bộ xử lý Arm, tải 32 bit có thể mất tới 4 lệnh: Nó thực hiện load 8bit immediate and shifttối đa 4 lần. Thông thường trình biên dịch sẽ thực hiện địa chỉ gián tiếp để lấy một số tạo thành một bảng để tránh điều này. (-1 có thể được thực hiện trong 1 hướng dẫn, nhưng một ví dụ khác có thể được chọn).
ctrl-alt-delor

208

Thứ nhất, "đối tượng vô hướng" có nghĩa là một loại giống như một int, floathoặc một con trỏ (xem một đối tượng vô hướng trong C ++ như thế nào? ).


Thứ hai, có vẻ rõ ràng hơn rằng

f(++i, ++i);

sẽ có hành vi không xác định. Nhưng

f(i = -1, i = -1);

là ít rõ ràng

Một ví dụ hơi khác:

int i;
f(i = 1, i = -1);
std::cout << i << "\n";

Nhiệm vụ nào đã xảy ra "cuối cùng" i = 1, hay i = -1? Nó không được định nghĩa trong tiêu chuẩn. Thực sự, điều đó có nghĩa là icó thể 5(xem câu trả lời của tác giả để có lời giải thích hoàn toàn hợp lý cho trường hợp như thế này). Hoặc chương trình của bạn có thể segfault. Hoặc định dạng lại ổ cứng của bạn.

Nhưng bây giờ bạn hỏi: "Còn ví dụ của tôi thì sao? Tôi đã sử dụng cùng một giá trị ( -1) cho cả hai bài tập. Điều gì có thể không rõ ràng về điều đó?"

Bạn đúng ... ngoại trừ cách ủy ban tiêu chuẩn C ++ mô tả điều này.

Nếu một tác dụng phụ trên một đối tượng vô hướng không bị ảnh hưởng so với tác dụng phụ khác trên cùng một đối tượng vô hướng, thì hành vi không được xác định.

Họ có thể đã tạo ra một ngoại lệ đặc biệt cho trường hợp đặc biệt của bạn, nhưng họ đã không làm thế. (Và tại sao họ nên sử dụng? Điều gì sẽ có thể có?) Vì vậy, ivẫn có thể 5. Hoặc ổ cứng của bạn có thể trống. Do đó, câu trả lời cho câu hỏi của bạn là:

Đó là hành vi không xác định bởi vì nó không được xác định hành vi là gì.

(Điều này đáng được nhấn mạnh bởi vì nhiều lập trình viên nghĩ rằng "không xác định" có nghĩa là "ngẫu nhiên" hoặc "không thể đoán trước". Nó không có nghĩa là không được xác định bởi tiêu chuẩn. Hành vi có thể nhất quán 100% và vẫn không được xác định.)

Nó có thể đã được xác định hành vi? Đúng. Nó đã được định nghĩa? Không. Do đó, nó là "không xác định".

Điều đó nói rằng, "không xác định" không có nghĩa là trình biên dịch sẽ định dạng ổ cứng của bạn ... điều đó có nghĩa là nó có thể và nó vẫn sẽ là một trình biên dịch tuân thủ tiêu chuẩn. Trên thực tế, tôi chắc chắn g ++, Clang và MSVC sẽ làm những gì bạn mong đợi. Họ sẽ không "phải".


Một câu hỏi khác có thể là Tại sao ủy ban tiêu chuẩn C ++ lại chọn cách làm cho hiệu ứng phụ này không bị ảnh hưởng? . Câu trả lời đó sẽ liên quan đến lịch sử và ý kiến ​​của ủy ban. Hoặc Điều gì là tốt khi có tác dụng phụ này không có kết quả trong C ++? , cho phép mọi biện minh, cho dù đó có phải là lý do thực sự của ủy ban tiêu chuẩn hay không. Bạn có thể hỏi những câu hỏi ở đây, hoặc tại lập trình viên.stackexchange.com.


9
@hvd, vâng, thực tế tôi biết rằng nếu bạn kích hoạt -Wsequence-pointcho g ++, nó sẽ cảnh báo bạn.
Paul Draper

47
"Tôi chắc chắn g ++, Clang và MSVC sẽ làm những gì bạn mong đợi" Tôi không tin tưởng vào một trình biên dịch hiện đại. Họ là ác quỷ. Ví dụ, họ có thể nhận ra rằng đây là hành vi không xác định và cho rằng mã này không thể truy cập được. Nếu họ không làm như vậy hôm nay, họ có thể làm như vậy vào ngày mai. Bất kỳ UB là một quả bom hẹn giờ.
CodeInChaos

8
@BlacklightShining "câu trả lời của bạn rất tệ vì nó không tốt" không phải là phản hồi rất hữu ích, phải không?
Vincent van der Weele

13
@BobJarvis Các trình biên dịch hoàn toàn không có nghĩa vụ phải tạo mã chính xác từ xa khi đối mặt với hành vi không xác định. TIt thậm chí có thể giả định rằng mã này thậm chí không bao giờ được gọi và do đó thay thế toàn bộ bằng một nop (Lưu ý rằng trình biên dịch thực sự đưa ra các giả định như vậy khi đối mặt với UB). Do đó, tôi muốn nói rằng phản ứng chính xác đối với báo cáo lỗi như vậy chỉ có thể được "đóng, hoạt động như dự định"
Grizzly

7
@SchighSchagh Đôi khi việc viết lại các thuật ngữ (chỉ trên bề mặt dường như là một câu trả lời tautological) là những gì mọi người cần. Hầu hết mọi người mới sử dụng thông số kỹ thuật nghĩ rằng undefined behaviorcó nghĩa là something random will happen, đó là xa trường hợp hầu hết thời gian.
Izkata

27

Một lý do thực tế để không tạo ra ngoại lệ từ các quy tắc chỉ vì hai giá trị giống nhau:

// config.h
#define VALUEA  1

// defaults.h
#define VALUEB  1

// prog.cpp
f(i = VALUEA, i = VALUEB);

Hãy xem xét trường hợp này đã được cho phép.

Bây giờ, một vài tháng sau, nhu cầu thay đổi

 #define VALUEB 2

Dường như vô hại, phải không? Nhưng đột nhiên prog.cpp sẽ không biên dịch nữa. Tuy nhiên, chúng tôi cảm thấy rằng việc biên dịch không nên phụ thuộc vào giá trị của một nghĩa đen.

Dòng dưới cùng: không có ngoại lệ cho quy tắc bởi vì nó sẽ làm cho quá trình biên dịch thành công phụ thuộc vào giá trị (chứ không phải loại) của hằng số.

BIÊN TẬP

@HeartWare chỉ ra rằng các biểu thức không đổi của biểu mẫu A DIV Bkhông được phép trong một số ngôn ngữ, khi B0 và khiến quá trình biên dịch không thành công. Do đó thay đổi hằng số có thể gây ra lỗi biên dịch ở một số nơi khác. Đó là, IMHO, không may. Nhưng chắc chắn là tốt để hạn chế những điều như vậy là không thể tránh khỏi.


Chắc chắn, nhưng ví dụ không sử dụng chữ nguyên. Bạn f(i = VALUEA, i = VALUEB);chắc chắn có tiềm năng cho hành vi không xác định. Tôi hy vọng bạn không thực sự mã hóa các giá trị đằng sau số nhận dạng.
Sói

3
@Wold Nhưng trình biên dịch không thấy các macro tiền xử lý. Và ngay cả khi điều này không phải như vậy, thật khó để tìm thấy một ví dụ trong bất kỳ ngôn ngữ lập trình nào, nơi mã nguồn biên dịch cho đến khi một thay đổi một số int không đổi từ 1 đến 2. Điều này đơn giản là không thể chấp nhận và không thể giải thích được, trong khi bạn thấy các giải thích rất tốt ở đây tại sao mã đó bị hỏng ngay cả với cùng các giá trị.
Ingo

Có, các trình biên dịch không thấy các macro. Nhưng, đây có phải là câu hỏi?
Sói

1
Câu trả lời của bạn bị thiếu điểm, hãy đọc câu trả lời có hại và nhận xét của OP về nó.
Sói

1
Nó có thể làm được SomeProcedure(A, B, B DIV (2-A)). Dù sao, nếu ngôn ngữ nói rằng CONST phải được đánh giá đầy đủ tại thời điểm biên dịch, thì dĩ nhiên, yêu cầu của tôi không có giá trị trong trường hợp đó. Vì nó bằng cách nào đó làm mờ sự khác biệt của thời gian và thời gian chạy. Nó cũng sẽ thông báo nếu chúng ta viết CONST C = X(2-A); FUNCTION X:INTEGER(CONST Y:INTEGER) = B/Y; ?? Hoặc là các chức năng không được phép?
Ingo

12

Sự nhầm lẫn là việc lưu trữ một giá trị không đổi vào một biến cục bộ không phải là một lệnh nguyên tử trên mỗi kiến ​​trúc mà C được thiết kế để chạy. Bộ xử lý mã chạy trên các vấn đề nhiều hơn trình biên dịch trong trường hợp này. Ví dụ, trên ARM nơi mỗi lệnh không thể mang hằng số 32 bit hoàn chỉnh, việc lưu trữ một int trong một biến cần nhiều hơn một lệnh đó. Ví dụ với mã giả này, nơi bạn chỉ có thể lưu trữ 8 bit tại một thời điểm và phải làm việc trong thanh ghi 32 bit, i là int32:

reg = 0xFF; // first instruction
reg |= 0xFF00; // second
reg |= 0xFF0000; // third
reg |= 0xFF000000; // fourth
i = reg; // last

Bạn có thể tưởng tượng rằng nếu trình biên dịch muốn tối ưu hóa thì nó có thể xen kẽ cùng một chuỗi hai lần và bạn không biết giá trị nào sẽ được ghi vào i; và hãy nói rằng anh ta không thông minh lắm:

reg = 0xFF;
reg |= 0xFF00;
reg |= 0xFF0000;
reg = 0xFF;
reg |= 0xFF000000;
i = reg; // writes 0xFF0000FF == -16776961
reg |= 0xFF00;
reg |= 0xFF0000;
reg |= 0xFF000000;
i = reg; // writes 0xFFFFFFFF == -1

Tuy nhiên, trong các thử nghiệm của tôi, gcc đủ tốt để nhận ra rằng cùng một giá trị được sử dụng hai lần và tạo ra nó một lần và không có gì lạ. Tôi nhận được -1, -1 Nhưng ví dụ của tôi vẫn hợp lệ vì điều quan trọng là phải xem xét rằng ngay cả một hằng số có thể không rõ ràng như nó có vẻ.


Tôi cho rằng trên ARM trình biên dịch sẽ chỉ tải hằng số từ một bảng. Những gì bạn mô tả có vẻ giống MIPS hơn.
ach

1
@AndreyChernyakhovskiy Yep, nhưng trong trường hợp không chỉ đơn giản là -1(trình biên dịch đã được lưu trữ ở đâu đó), mà là 3^81 mod 2^32, nhưng không đổi, thì trình biên dịch có thể thực hiện chính xác những gì đã thực hiện ở đây và trong một số đòn bẩy của việc gọi tắt, tôi xen kẽ các chuỗi cuộc gọi để tránh chờ đợi
yo '

@tohecz, yeah, tôi đã kiểm tra rồi. Thật vậy, trình biên dịch quá thông minh để tải mọi hằng số từ một bảng. Dù sao, nó sẽ không bao giờ sử dụng cùng một thanh ghi để tính hai hằng số. Điều này chắc chắn sẽ 'xác định' hành vi được xác định.
ach

@AndreyChernyakhovskiy Nhưng có lẽ bạn không phải là "mọi lập trình viên biên dịch C ++ trên thế giới". Hãy nhớ rằng có những máy có 3 thanh ghi ngắn chỉ có sẵn để tính toán.
yo '

@tohecz, hãy xem xét ví dụ f(i = A, j = B)ở đâu ijlà hai đối tượng riêng biệt. Ví dụ này không có UB. Máy có 3 thanh ghi ngắn không phải là lý do để trình biên dịch trộn lẫn hai giá trị ABtrong cùng một thanh ghi (như trong câu trả lời của @ davidf), vì nó sẽ phá vỡ ngữ nghĩa chương trình.
ach

11

Hành vi thường được chỉ định là không xác định nếu có một số lý do có thể hiểu được tại sao trình biên dịch đang cố gắng "hữu ích" có thể làm điều gì đó gây ra hành vi hoàn toàn bất ngờ.

Trong trường hợp một biến được ghi nhiều lần mà không có gì để đảm bảo rằng việc ghi xảy ra ở những thời điểm khác nhau, một số loại phần cứng có thể cho phép nhiều hoạt động "lưu trữ" được thực hiện đồng thời đến các địa chỉ khác nhau bằng bộ nhớ cổng kép. Tuy nhiên, một số bộ nhớ cổng kép rõ ràng cấm kịch bản trong đó hai cửa hàng đồng thời đánh cùng một địa chỉ, bất kể các giá trị được viết có khớp hay không. Nếu một trình biên dịch cho một máy như vậy thông báo hai lần thử không có kết quả để ghi cùng một biến, thì nó có thể từ chối biên dịch hoặc đảm bảo rằng hai lần ghi không thể được lên lịch đồng thời. Nhưng nếu một hoặc cả hai truy cập thông qua một con trỏ hoặc tham chiếu, trình biên dịch có thể không phải lúc nào cũng có thể biết liệu cả hai lần ghi có thể chạm vào cùng một vị trí lưu trữ hay không. Trong trường hợp đó, nó có thể lên lịch ghi đồng thời, gây ra bẫy phần cứng trong nỗ lực truy cập.

Tất nhiên, việc ai đó có thể triển khai trình biên dịch C trên nền tảng như vậy không cho thấy hành vi đó không nên được xác định trên nền tảng phần cứng khi sử dụng các cửa hàng loại đủ nhỏ để được xử lý nguyên tử. Cố gắng lưu trữ hai giá trị khác nhau theo kiểu không có kết quả có thể gây ra sự kỳ lạ nếu trình biên dịch không biết về nó; ví dụ: đã cho:

uint8_t v;  // Global

void hey(uint8_t *p)
{
  moo(v=5, (*p)=6);
  zoo(v);
  zoo(v);
}

nếu trình biên dịch nối tiếp cuộc gọi đến "moo" và có thể nói nó không sửa đổi "v", thì nó có thể lưu trữ 5 đến v, sau đó lưu trữ 6 đến * p, sau đó chuyển 5 đến "zoo", sau đó chuyển chuyển nội dung của v đến "sở thú". Nếu "sở thú" không sửa đổi "v", sẽ không có cách nào để hai cuộc gọi được chuyển qua các giá trị khác nhau, nhưng dù sao điều đó cũng có thể dễ dàng xảy ra. Mặt khác, trong trường hợp cả hai cửa hàng sẽ viết cùng một giá trị, thì sự kỳ lạ đó không thể xảy ra và trên hầu hết các nền tảng sẽ không có lý do hợp lý nào để việc triển khai thực hiện bất kỳ điều gì kỳ lạ. Thật không may, một số tác giả biên dịch không cần bất kỳ lý do nào cho các hành vi ngớ ngẩn ngoài "bởi vì Tiêu chuẩn cho phép điều đó", vì vậy ngay cả những trường hợp đó cũng không an toàn.


9

Thực tế là kết quả sẽ là như nhau trong hầu hết các trường trong này trường hợp là ngẫu nhiên; thứ tự đánh giá vẫn chưa được xác định. Xem xét f(i = -1, i = -2): ở đây, vấn đề đặt hàng. Lý do duy nhất nó không quan trọng trong ví dụ của bạn là tai nạn mà cả hai giá trị là -1.

Cho rằng biểu thức được chỉ định là một biểu hiện có hành vi không xác định, trình biên dịch tuân thủ độc hại có thể hiển thị hình ảnh không phù hợp khi bạn đánh giá f(i = -1, i = -1)và hủy bỏ việc thực thi - và vẫn được coi là hoàn toàn chính xác. May mắn thay, không có trình biên dịch tôi nhận thức được làm như vậy.


8

Theo tôi thì có vẻ như quy tắc duy nhất liên quan đến giải trình tự biểu thức đối số hàm là ở đây:

3) Khi gọi một hàm (cho dù hàm đó có phải là dòng hay không và có sử dụng cú pháp gọi hàm rõ ràng hay không), mọi tính toán giá trị và hiệu ứng phụ liên quan đến bất kỳ biểu thức đối số nào hoặc với biểu thức postfix chỉ định hàm được gọi là tuần tự trước khi thực hiện mọi biểu thức hoặc câu lệnh trong phần thân của hàm được gọi.

Điều này không xác định trình tự giữa các biểu thức đối số, vì vậy chúng tôi kết thúc trong trường hợp này:

1) Nếu một tác dụng phụ trên một đối tượng vô hướng không bị ảnh hưởng so với tác dụng phụ khác trên cùng một đối tượng vô hướng, thì hành vi không được xác định.

Trong thực tế, trên hầu hết các trình biên dịch, ví dụ bạn trích dẫn sẽ chạy tốt (trái ngược với "xóa đĩa cứng của bạn" và các hậu quả hành vi không xác định theo lý thuyết khác).
Tuy nhiên, đó là một trách nhiệm pháp lý, vì nó phụ thuộc vào hành vi trình biên dịch cụ thể, ngay cả khi hai giá trị được gán là như nhau. Ngoài ra, rõ ràng, nếu bạn cố gán các giá trị khác nhau, kết quả sẽ là "thực sự" không xác định:

void f(int l, int r) {
    return l < -1;
}
auto b = f(i = -1, i = -2);
if (b) {
    formatDisk();
}

8

C ++ 17 định nghĩa các quy tắc đánh giá chặt chẽ hơn. Cụ thể, nó sắp xếp các đối số chức năng (mặc dù theo thứ tự không xác định).

N5659 §4.6:15
Các đánh giá AB không được xác định theo trình tự khi A được giải trình tự trước khi B hoặc B được sắp xếp theo trình tự trước A , nhưng không xác định được. [ Lưu ý : Các đánh giá theo trình tự không xác định có thể trùng lặp, nhưng có thể được thực hiện trước. - lưu ý cuối ]

N5659 § 8.2.2:5
Việc khởi tạo một tham số, bao gồm mọi tính toán giá trị liên quan và tác dụng phụ, được sắp xếp không xác định theo trình tự đối với bất kỳ tham số nào khác.

Nó cho phép một số trường hợp sẽ là UB trước:

f(i = -1, i = -1); // value of i is -1
f(i = -1, i = -2); // value of i is either -1 or -2, but not specified which one

2
Cảm ơn bạn đã thêm bản cập nhật này cho c ++ 17 , vì vậy tôi không phải làm vậy. ;)
Yakk - Adam Nevraumont

Tuyệt vời, cảm ơn rất nhiều cho câu trả lời này. Theo dõi nhẹ: nếu fchữ ký là f(int a, int b), C ++ 17 có đảm bảo điều đó a == -1b == -2nếu được gọi như trong trường hợp thứ hai không?
Nicu Stiurca

Đúng. Nếu chúng ta có các tham số ab, thì i-then- ađược khởi tạo thành -1, sau đó i-then- bđược khởi tạo thành -2 hoặc theo cách xung quanh. Trong cả hai trường hợp, chúng tôi kết thúc với a == -1b == -2. Ít nhất đây là cách tôi đọc " Việc khởi tạo một tham số, bao gồm mọi tính toán giá trị liên quan và tác dụng phụ, được sắp xếp không xác định theo trình tự liên quan đến bất kỳ tham số nào khác ".
AlexD

Tôi nghĩ rằng nó đã giống nhau trong C mãi mãi.
fuz

5

Toán tử gán có thể bị quá tải, trong trường hợp đó thứ tự có thể có vấn đề:

struct A {
    bool first;
    A () : first (false) {
    }
    const A & operator = (int i) {
        first = !first;
        return * this;
    }
};

void f (A a1, A a2) {
    // ...
}


// ...
A i;
f (i = -1, i = -1);   // the argument evaluated first has ax.first == true

1
Đúng như vậy, nhưng câu hỏi là về các loại vô hướng , mà những người khác đã chỉ ra có nghĩa là về cơ bản là gia đình int, gia đình nổi và con trỏ.
Nicu Stiurca

Vấn đề thực sự trong trường hợp này là toán tử gán là trạng thái, do đó, ngay cả thao tác thường xuyên của biến cũng dễ xảy ra các vấn đề như thế này.
AJMansfield

2

Đây chỉ là trả lời "Tôi không chắc" đối tượng vô hướng "có thể có nghĩa gì ngoài một thứ như int hay float".

Tôi sẽ giải thích "đối tượng vô hướng" là tên viết tắt của "đối tượng kiểu vô hướng", hoặc chỉ là "biến kiểu vô hướng". Sau đó, pointer, enum(không đổi) là kiểu vô hướng.

Đây là một bài viết MSDN về các loại vô hướng .


Điều này đọc một chút giống như một "câu trả lời chỉ liên kết". Bạn có thể sao chép các bit có liên quan từ liên kết đó vào câu trả lời này (trong một blockquote) không?
Cole Johnson

1
@ColeJohnson Đây không phải là một câu trả lời duy nhất. Các liên kết chỉ để giải thích thêm. Câu trả lời của tôi là "con trỏ", "enum".
Bành Trương

Tôi không nói câu trả lời của bạn một câu trả lời duy nhất. Tôi nói nó "đọc như [một]" . Tôi đề nghị bạn đọc lý do tại sao chúng tôi không muốn chỉ trả lời liên kết trong phần trợ giúp. Lý do là, nếu Microsoft cập nhật URL của họ trong trang web của họ, liên kết đó sẽ bị hỏng.
Cole Johnson

1

Trên thực tế, có một lý do để không phụ thuộc vào thực tế là trình biên dịch sẽ kiểm tra iđược gán với cùng một giá trị hai lần, để có thể thay thế nó bằng một phép gán duy nhất. Nếu chúng ta có một số biểu thức thì sao?

void g(int a, int b, int c, int n) {
    int i;
    // hey, compiler has to prove Fermat's theorem now!
    f(i = 1, i = (ipow(a, n) + ipow(b, n) == ipow(c, n)));
}

1
Không cần phải chứng minh định lý của Fermat: chỉ cần gán 1cho i. Cả hai đối số đều gán 1và điều này thực hiện điều "đúng" hoặc đối số gán các giá trị khác nhau và đó là hành vi không xác định để lựa chọn của chúng tôi vẫn được cho phép.
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.