Điều gì đã tạo ra i = i ++ + 1; hợp pháp trong C ++ 17?


186

Trước khi bạn bắt đầu la hét hành vi không xác định, điều này được liệt kê rõ ràng trong N4659 (C ++ 17)

  i = i++ + 1;        // the value of i is incremented

Tuy nhiên, trong N3337 (C ++ 11)

  i = i++ + 1;        // the behavior is undefined

Những gì đã thay đổi?

Từ những gì tôi có thể thu thập, từ [N4659 basic.exec]

Trừ khi có ghi chú, các đánh giá toán hạng của các toán tử riêng lẻ và các biểu thức con của các biểu thức riêng lẻ đều không được đưa ra. [...] Các tính toán giá trị của các toán hạng của toán tử được sắp xếp theo trình tự trước khi tính toán giá trị của kết quả của toán tử. Nếu một tác dụng phụ trên một vị trí bộ nhớ không bị ảnh hưởng so với tác dụng phụ khác trên cùng một vị trí bộ nhớ hoặc tính toán giá trị sử dụng giá trị của bất kỳ đối tượng nào trong cùng một vị trí bộ nhớ và chúng không có khả năng đồng thời, thì hành vi không được xác định.

Trong đó giá trị được xác định tại [N4659 basic.type]

Đối với các loại có thể sao chép tầm thường, biểu diễn giá trị là một tập hợp các bit trong biểu diễn đối tượng xác định một giá trị , là một phần tử riêng biệt của một tập hợp các giá trị được xác định theo thực hiện

Từ [N3337 basic.exec]

Trừ khi có ghi chú, các đánh giá toán hạng của các toán tử riêng lẻ và các biểu thức con của các biểu thức riêng lẻ đều không được đưa ra. [...] Các tính toán giá trị của các toán hạng của toán tử được sắp xếp theo trình tự trước khi tính toán giá trị của kết quả của toán tử. Nếu một tác dụng phụ trên một đối tượng vô hướng không có kết quả tương ứng với một tác dụng phụ khác trên cùng một đối tượng vô hướng hoặc một tính toán giá trị sử dụng giá trị của cùng một đối tượng vô hướng, thì hành vi không được xác định.

Tương tự, giá trị được xác định tại [N3337 basic.type]

Đối với các loại có thể sao chép tầm thường, biểu diễn giá trị là một tập hợp các bit trong biểu diễn đối tượng xác định một giá trị , là một phần tử riêng biệt của một tập hợp các giá trị được xác định theo thực hiện.

Chúng giống hệt nhau ngoại trừ đề cập đến đồng thời không quan trọng và với việc sử dụng vị trí bộ nhớ thay vì đối tượng vô hướng , trong đó

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

Mà không ảnh hưởng đến ví dụ.

Từ [N4659 expr.ass]

Toán tử gán (=) và toán tử gán gán tổng hợp tất cả các nhóm từ phải sang trái. Tất cả đều yêu cầu một giá trị có thể sửa đổi là toán hạng bên trái của chúng và trả về một giá trị tham chiếu đến toán hạng bên trái. Kết quả trong mọi trường hợp là trường bit nếu toán hạng bên trái là trường bit. Trong mọi trường hợp, phép gán được sắp xếp theo trình tự sau khi tính toán giá trị của toán hạng phải và trái và trước khi tính toán giá trị của biểu thức gán. Toán hạng bên phải được sắp xếp theo thứ tự trước toán hạng bên trái.

Từ [N3337 expr.ass]

Toán tử gán (=) và toán tử gán gán tổng hợp tất cả các nhóm từ phải sang trái. Tất cả đều yêu cầu một giá trị có thể sửa đổi là toán hạng bên trái của chúng và trả về một giá trị tham chiếu đến toán hạng bên trái. Kết quả trong mọi trường hợp là trường bit nếu toán hạng bên trái là trường bit. Trong mọi trường hợp, phép gán được sắp xếp theo trình tự sau khi tính toán giá trị của toán hạng phải và trái và trước khi tính toán giá trị của biểu thức gán.

Sự khác biệt duy nhất là câu cuối cùng vắng mặt trong N3337.

Tuy nhiên, câu cuối cùng không nên có bất kỳ tầm quan trọng nào vì toán hạng bên trái ikhông phải là "hiệu ứng phụ khác" hay "sử dụng giá trị của cùng một đối tượng vô hướng"biểu thức id là một giá trị.


23
Bạn đã xác định lý do tại sao: Trong C ++ 17, toán hạng bên phải được sắp xếp theo thứ tự trước toán hạng bên trái. Trong C ++ 11 không có trình tự như vậy. Chính xác thì câu hỏi của bạn là gì?
Rob

4
@ Robᵩ Xem câu cuối cùng.
qua đường

7
Có ai có một liên kết đến động lực cho sự thay đổi này? Tôi muốn một bộ phân tích tĩnh có thể nói "bạn không muốn làm điều đó" khi phải đối mặt với mã như thế nào i = i++ + 1;.

7
@NeilButterworth, đó là từ bài báo p0145r3.pdf : "Tinh chỉnh thứ tự đánh giá biểu thức cho thành ngữ C ++".
xaizek

9
@NeilButterworth, phần số 2 nói rằng đây là phản biện trực quan và thậm chí các chuyên gia không làm đúng trong mọi trường hợp. Đó là khá nhiều động lực của họ.
xaizek

Câu trả lời:


144

Trong C ++ 11, hành động "gán", tức là tác dụng phụ của việc sửa đổi LHS, được giải trình tự sau khi tính toán giá trị của toán hạng bên phải. Lưu ý rằng đây là một bảo đảm tương đối "yếu": nó chỉ tạo ra trình tự liên quan đến tính toán giá trị của RHS. Nó không nói gì về các tác dụng phụ có thể có trong RHS, vì sự xuất hiện của các tác dụng phụ không phải là một phần của tính toán giá trị . Các yêu cầu của C ++ 11 không thiết lập trình tự tương đối giữa hành động chuyển nhượng và bất kỳ tác dụng phụ nào của RHS. Đây là những gì tạo ra tiềm năng cho UB.

Hy vọng duy nhất trong trường hợp này là bất kỳ đảm bảo bổ sung nào được thực hiện bởi các nhà khai thác cụ thể được sử dụng trong RHS. Nếu RHS sử dụng tiền tố ++, các thuộc tính tuần tự cụ thể cho dạng tiền tố ++sẽ lưu ngày trong ví dụ này. Nhưng postfix ++là một câu chuyện khác: nó không đảm bảo như vậy. Trong C ++ 11, các tác dụng phụ của =và hậu tố ++kết thúc không có kết quả với mối quan hệ với nhau trong ví dụ này. Và đó là UB.

Trong C ++ 17, một câu thêm được thêm vào đặc tả của toán tử gán:

Toán hạng bên phải được sắp xếp theo thứ tự trước toán hạng bên trái.

Kết hợp với ở trên nó làm cho một sự đảm bảo rất mạnh mẽ. Nó sắp xếp mọi thứ xảy ra trong RHS (bao gồm mọi tác dụng phụ) trước mọi thứ xảy ra trong LHS. Vì nhiệm vụ thực tế được sắp xếp theo trình tự sau LHS (và RHS), nên trình tự bổ sung đó hoàn toàn cô lập hành động chuyển nhượng khỏi mọi tác dụng phụ có trong RHS. Trình tự mạnh mẽ hơn này là những gì loại bỏ các UB trên.

(Được cập nhật để đưa vào tài khoản ý kiến ​​của @John Bollinger.)


3
Có thực sự đúng khi đưa "hành động chuyển nhượng thực tế" vào các hiệu ứng được bao phủ bởi "toán hạng tay trái" trong đoạn trích đó không? Tiêu chuẩn có ngôn ngữ riêng về trình tự của bài tập thực tế. Tôi lấy đoạn trích mà bạn đã trình bày bị giới hạn trong phạm vi sắp xếp thứ tự các biểu thức con bên trái và bên phải, dường như không đủ, kết hợp với phần còn lại của phần đó, để hỗ trợ tốt định nghĩa của tuyên bố của OP.
John Bollinger

11
Sửa lỗi: việc gán thực tế vẫn được giải trình tự sau khi tính toán giá trị của toán hạng bên trái và việc đánh giá toán hạng bên trái được giải trình tự sau khi đánh giá (hoàn thành) toán hạng bên phải, vì vậy, thay đổi đó là đủ để hỗ trợ cho việc xác định rõ OP hỏi về. Sau đó, tôi chỉ phân minh các chi tiết, nhưng những điều này có vấn đề, vì chúng có thể có ý nghĩa khác nhau cho các mã khác nhau.
John Bollinger

3
@JohnBollinger: Tôi thấy tò mò rằng các tác giả của Tiêu chuẩn sẽ tạo ra một sự thay đổi làm suy yếu hiệu quả của việc tạo mã đơn giản và về mặt lịch sử là không cần thiết, và chần chừ trong việc xác định các hành vi khác mà sự vắng mặt là vấn đề lớn hơn nhiều, và sẽ hiếm khi gây ra bất kỳ trở ngại có ý nghĩa cho hiệu quả.
supercat

1
@Kaz: Đối với các bài tập ghép, thực hiện đánh giá giá trị bên trái sau bên phải cho phép một cái gì đó muốn x -= y;được xử lý mov eax,[y] / sub [x],eaxthay vì mov eax,[x] / neg eax / add eax,[y] / mov [x],eax. Tôi không thấy bất cứ điều gì bình thường về điều đó. Nếu người ta phải chỉ định một thứ tự, thứ tự hiệu quả nhất có thể là thực hiện tất cả các tính toán cần thiết để xác định đối tượng bên trái trước, sau đó đánh giá toán hạng bên phải, sau đó là giá trị của đối tượng bên trái, nhưng điều đó đòi hỏi phải có một thuật ngữ cho hành động giải quyết id'ty của đối tượng bên trái.
supercat

1
@Kaz: Nếu xyđược volatile, điều đó sẽ có tác dụng phụ. Hơn nữa, những cân nhắc tương tự sẽ được áp dụng cho x += f();, nơi f()sửa đổi x.
supercat

33

Bạn đã xác định câu mới

Toán hạng bên phải được sắp xếp theo thứ tự trước toán hạng bên trái.

và bạn xác định chính xác rằng việc đánh giá toán hạng bên trái như một giá trị là không liên quan. Tuy nhiên, trình tự trước đó được chỉ định là một mối quan hệ bắc cầu. Do đó, toán hạng bên phải hoàn chỉnh (bao gồm cả phần tăng sau) cũng được sắp xếp theo thứ tự trước khi gán. Trong C ++ 11, chỉ tính toán giá trị của toán hạng bên phải được sắp xếp theo trình tự trước khi gán.


7

Trong các tiêu chuẩn C ++ cũ hơn và trong C11, định nghĩa của văn bản toán tử gán kết thúc bằng văn bản:

Các đánh giá của toán hạng là không có kết quả.

Có nghĩa là các tác dụng phụ trong toán hạng là không có kết quả và do đó chắc chắn không xác định hành vi nếu chúng sử dụng cùng một biến.

Văn bản này đã bị xóa đơn giản trong C ++ 11, khiến nó hơi mơ hồ. Có phải là UB hay không? Điều này đã được làm rõ trong C ++ 17 nơi họ đã thêm:

Toán hạng bên phải được sắp xếp theo thứ tự trước toán hạng bên trái.


Một lưu ý phụ, trong các tiêu chuẩn thậm chí cũ hơn, tất cả đã được làm rất rõ ràng, ví dụ từ C99:

Thứ tự đánh giá các toán hạng là không xác định. Nếu một nỗ lực được thực hiện để sửa đổi kết quả của toán tử gán hoặc để truy cập nó sau điểm chuỗi tiếp theo, hành vi không được xác định.

Về cơ bản, trong C11 / C ++ 11, họ đã nhầm lẫn khi xóa văn bản này.


1

Đây là thông tin thêm cho các câu trả lời khác và tôi đang đăng nó vì đoạn mã dưới đây cũng thường được hỏi về .

Giải thích trong các câu trả lời khác là chính xác và cũng áp dụng cho mã sau đây hiện được xác định rõ (và không thay đổi giá trị được lưu trữ của i):

i = i++;

Đây + 1là một cá trích đỏ và không thực sự rõ ràng tại sao Tiêu chuẩn lại sử dụng nó trong các ví dụ của họ, mặc dù tôi nhớ lại những người tranh luận về danh sách gửi thư trước C ++ 11 có thể + 1đã tạo ra sự khác biệt do buộc phải chuyển đổi giá trị sớm ở bên phải- bên tay. Chắc chắn không có cái nào áp dụng trong C ++ 17 (và có lẽ không bao giờ được áp dụng trong bất kỳ phiên bản nào của C ++).

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.