Trong thực tế, tại sao các trình biên dịch khác nhau sẽ tính các giá trị khác nhau của int x = ++ i + ++ i ;?


164

Hãy xem xét mã này:

int i = 1;
int x = ++i + ++i;

Chúng tôi có một số phỏng đoán cho những gì một trình biên dịch có thể làm cho mã này, giả sử nó biên dịch.

  1. cả hai đều ++itrở lại 2, dẫn đến x=4.
  2. một ++itrả lại 2và trả lại khác 3, dẫn đến x=5.
  3. cả hai đều ++itrở lại 3, dẫn đến x=6.

Đối với tôi, điều thứ hai có vẻ khả dĩ nhất. Một trong hai ++toán tử được thực thi với i = 1, iđược tăng dần và kết quả 2được trả về. Sau đó, ++toán tử thứ hai được thực thi với i = 2, iđược tăng dần và kết quả 3được trả về. Sau đó 23được thêm vào với nhau để cung cấp cho 5.

Tuy nhiên, tôi đã chạy mã này trong Visual Studio và kết quả là 6. Tôi đang cố gắng hiểu các trình biên dịch tốt hơn và tôi đang tự hỏi điều gì có thể dẫn đến kết quả 6. Dự đoán duy nhất của tôi là mã có thể được thực thi với một số đồng thời "tích hợp sẵn". Hai ++toán tử được gọi, mỗi toán tử tăng lên itrước khi toán tử kia trả về, và sau đó cả hai đều quay trở lại 3. Điều này sẽ mâu thuẫn với hiểu biết của tôi về ngăn xếp cuộc gọi và cần phải được giải thích.

Điều gì (hợp lý) mà C++trình biên dịch có thể làm dẫn đến kết quả 4hoặc kết quả hoặc 6?

Ghi chú

Ví dụ này xuất hiện như một ví dụ về hành vi không xác định trong Lập trình của Bjarne Stroustrup: Nguyên tắc và Thực hành sử dụng C ++ (C ++ 14).

Xem bình luận của quế .


5
Thông số C không thực sự bao gồm thứ tự của các hoạt động hoặc đánh giá ở bên phải của dấu = so với các hoạt động trước / sau, chỉ ở bên trái.
Cristobol Polychronopolis

2
Khuyên bạn nên trích dẫn trong câu hỏi nếu bạn lấy ví dụ này từ một cuốn sách của Stroustrup (như đã đề cập trong phần nhận xét cho một trong các câu trả lời).
Daniel R. Collins

4
@philipxy Bản sao được đề xuất của bạn không phải là bản sao của câu hỏi này. Các câu hỏi là khác nhau. Các câu trả lời trong bản sao đề xuất của bạn không trả lời câu hỏi này. Các câu trả lời trong bản sao đề xuất của bạn không phải là bản sao của (các) câu trả lời được chấp nhận (hoặc phiếu bầu cao) cho câu hỏi này. Tôi tin rằng bạn đã đọc sai câu hỏi của tôi. Tôi đề nghị bạn đọc lại nó và xem xét lại cuộc bỏ phiếu để đóng.
quế

3
@philipxy "Các câu trả lời nói rằng trình biên dịch có thể làm bất cứ điều gì ..." Điều đó không trả lời câu hỏi của tôi. "họ cho thấy rằng ngay cả khi bạn nghĩ rằng câu hỏi của bạn là khác, nó chỉ là một biến thể của câu hỏi đó" Cái gì? "mặc dù bạn không cung cấp phiên bản C ++ của mình" Phiên bản C ++ của tôi không liên quan đến câu hỏi của tôi. "do đó toàn bộ chương trình mà câu lệnh ở trong có thể làm bất cứ điều gì" Tôi biết, nhưng câu hỏi của tôi là về hành vi cụ thể. "Nhận xét của bạn không phản ánh nội dung của các câu trả lời ở đó." Nhận xét của tôi phản ánh nội dung câu hỏi của tôi, mà bạn nên đọc lại.
quế

2
Để trả lời tiêu đề; bởi vì UB có nghĩa là bahavioir là không xác định. Nhiều trình biên dịch được thực hiện vào các thời điểm khác nhau trong suốt lịch sử, bởi những người khác nhau cho các kiến ​​trúc khác nhau, khi được yêu cầu tô màu bên ngoài các đường nét và thực hiện trong thế giới thực, họ phải đặt một cái gì đó vào phần này bên ngoài đặc tả, vì vậy mọi người đã làm chính xác điều đó và mỗi trong số họ đã sử dụng bút chì màu khác nhau. Do đó, châm ngôn của hoary, đừng dựa vào UB
Toby

Câu trả lời:


199

Trình biên dịch lấy mã của bạn, chia nó thành các hướng dẫn rất đơn giản, sau đó kết hợp lại và sắp xếp chúng theo cách mà nó cho là tối ưu.

Mật mã

int i = 1;
int x = ++i + ++i;

bao gồm các hướng dẫn sau:

1. store 1 in i
2. read i as tmp1
3. add 1 to tmp1
4. store tmp1 in i
5. read i as tmp2
6. read i as tmp3
7. add 1 to tmp3
8. store tmp3 in i
9. read i as tmp4
10. add tmp2 and tmp4, as tmp5
11. store tmp5 in x

Nhưng mặc dù đây là một danh sách được đánh số theo cách tôi đã viết, chỉ có một số phụ thuộc theo thứ tự ở đây: 1-> 2-> 3-> 4-> 5-> 10-> 11 và 1-> 6-> 7- > 8-> 9-> 10-> 11 phải theo thứ tự tương đối của chúng. Ngoài ra, trình biên dịch có thể tự do sắp xếp lại và có lẽ loại bỏ sự dư thừa.

Ví dụ: bạn có thể sắp xếp danh sách như sau:

1. store 1 in i
2. read i as tmp1
6. read i as tmp3
3. add 1 to tmp1
7. add 1 to tmp3
4. store tmp1 in i
8. store tmp3 in i
5. read i as tmp2
9. read i as tmp4
10. add tmp2 and tmp4, as tmp5
11. store tmp5 in x

Tại sao trình biên dịch có thể làm được điều này? Bởi vì không có trình tự cho các tác dụng phụ của sự gia tăng. Nhưng bây giờ trình biên dịch có thể đơn giản hóa: ví dụ, có một cửa hàng chết trong 4: giá trị ngay lập tức bị ghi đè. Ngoài ra, tmp2 và tmp4 thực sự giống nhau.

1. store 1 in i
2. read i as tmp1
6. read i as tmp3
3. add 1 to tmp1
7. add 1 to tmp3
8. store tmp3 in i
5. read i as tmp2
10. add tmp2 and tmp2, as tmp5
11. store tmp5 in x

Và bây giờ mọi thứ cần làm với tmp1 là mã chết: nó không bao giờ được sử dụng. Và việc đọc lại tôi cũng có thể bị loại bỏ:

1. store 1 in i
6. read i as tmp3
7. add 1 to tmp3
8. store tmp3 in i
10. add tmp3 and tmp3, as tmp5
11. store tmp5 in x

Hãy nhìn xem, đoạn mã này ngắn hơn nhiều. Trình tối ưu hóa rất vui. Lập trình viên thì không, vì tôi chỉ được tăng một lần. Giáo sư.

Thay vào đó, hãy xem xét một thứ khác mà trình biên dịch có thể làm: hãy quay lại phiên bản gốc.

1. store 1 in i
2. read i as tmp1
3. add 1 to tmp1
4. store tmp1 in i
5. read i as tmp2
6. read i as tmp3
7. add 1 to tmp3
8. store tmp3 in i
9. read i as tmp4
10. add tmp2 and tmp4, as tmp5
11. store tmp5 in x

Trình biên dịch có thể sắp xếp lại nó như thế này:

1. store 1 in i
2. read i as tmp1
3. add 1 to tmp1
4. store tmp1 in i
6. read i as tmp3
7. add 1 to tmp3
8. store tmp3 in i
5. read i as tmp2
9. read i as tmp4
10. add tmp2 and tmp4, as tmp5
11. store tmp5 in x

và sau đó thông báo lại rằng tôi được đọc hai lần, vì vậy hãy loại bỏ một trong số chúng:

1. store 1 in i
2. read i as tmp1
3. add 1 to tmp1
4. store tmp1 in i
6. read i as tmp3
7. add 1 to tmp3
8. store tmp3 in i
5. read i as tmp2
10. add tmp2 and tmp2, as tmp5
11. store tmp5 in x

Điều đó thật tuyệt, nhưng nó có thể đi xa hơn: nó có thể sử dụng lại tmp1:

1. store 1 in i
2. read i as tmp1
3. add 1 to tmp1
4. store tmp1 in i
6. read i as tmp1
7. add 1 to tmp1
8. store tmp1 in i
5. read i as tmp2
10. add tmp2 and tmp2, as tmp5
11. store tmp5 in x

Sau đó, nó có thể loại bỏ việc đọc lại i trong 6:

1. store 1 in i
2. read i as tmp1
3. add 1 to tmp1
4. store tmp1 in i
7. add 1 to tmp1
8. store tmp1 in i
5. read i as tmp2
10. add tmp2 and tmp2, as tmp5
11. store tmp5 in x

Bây giờ 4 là một cửa hàng chết:

1. store 1 in i
2. read i as tmp1
3. add 1 to tmp1
7. add 1 to tmp1
8. store tmp1 in i
5. read i as tmp2
10. add tmp2 and tmp2, as tmp5
11. store tmp5 in x

và bây giờ 3 và 7 có thể được hợp nhất thành một lệnh:

1. store 1 in i
2. read i as tmp1
3+7. add 2 to tmp1
8. store tmp1 in i
5. read i as tmp2
10. add tmp2 and tmp2, as tmp5
11. store tmp5 in x

Loại bỏ tạm thời cuối cùng:

1. store 1 in i
2. read i as tmp1
3+7. add 2 to tmp1
8. store tmp1 in i
10. add tmp1 and tmp1, as tmp5
11. store tmp5 in x

Và bây giờ bạn sẽ nhận được kết quả mà Visual C ++ mang lại cho bạn.

Lưu ý rằng trong cả hai đường dẫn tối ưu hóa, các phụ thuộc thứ tự quan trọng được giữ nguyên, miễn là các hướng dẫn không bị xóa mà không làm gì cả.


36
Hiện tại, đây là câu trả lời duy nhất đề cập đến trình tự .
PM 2Ring

3
-1 Tôi không nghĩ rằng câu trả lời này đang làm rõ. Các kết quả quan sát được hoàn toàn không phụ thuộc vào bất kỳ tối ưu hóa trình biên dịch nào (xem câu trả lời của tôi).
Daniel R. Collins

3
Điều này giả định các hoạt động đọc-sửa đổi-ghi. Một số CPU, chẳng hạn như x86 phổ biến, có hoạt động gia tăng nguyên tử, điều này càng làm tình hình thêm phức tạp.
Đánh dấu

6
@philipxy "Tiêu chuẩn không có gì để nói về mã đối tượng." Tiêu chuẩn cũng không có gì để nói về hành vi của đoạn mã này - đó là UB. Đó là một tiền đề của câu hỏi. OP muốn biết tại sao trong thực tế, các trình biên dịch có thể đưa ra các kết quả khác nhau và kỳ lạ. Ngoài ra, câu trả lời của tôi thậm chí không nói gì về mã đối tượng.
Sebastian Redl

5
@philipxy Tôi không hiểu ý kiến ​​phản đối của bạn. Như đã lưu ý, câu hỏi là về những gì một trình biên dịch có thể làm khi có UB, không phải về tiêu chuẩn C ++. Tại sao việc sử dụng mã đối tượng sẽ không phù hợp khi khám phá cách trình biên dịch giả định đang chuyển đổi mã? Trên thực tế, làm thế nào mà bất cứ thứ gì ngoại trừ mã đối tượng sẽ có liên quan?
Konrad Rudolph

58

Mặc dù đây là UB (như OP ngụ ý), sau đây là những cách giả định mà một trình biên dịch có thể nhận được 3 kết quả. Cả ba sẽ cho cùng một xkết quả đúng nếu được sử dụng với các int i = 1, j = 1;biến khác nhau thay vì một và giống nhau i.

  1. Cả hai ++ tôi trả về 2, dẫn đến x = 4.
int i = 1;
int i1 = i, i2 = i;   // i1 = i2 = 1
++i1;                 // i1 = 2
++i2;                 // i2 = 2
int x = i1 + i2;      // x = 4
  1. một ++ tôi trả về 2 và cái kia trả về 3, dẫn đến x = 5.
int i = 1;
int i1 = ++i;           // i1 = 2
int i2 = ++i;           // i2 = 3
int x = i1 + i2;        // x = 5
  1. cả hai ++ tôi trả về 3, kết quả là x = 6.
int i = 1;
int &i1 = i, &i2 = i;
++i1;                   // i = 2
++i2;                   // i = 3
int x = i1 + i2;        // x = 6

2
đây là một phản hồi tốt hơn những gì tôi hy vọng, cảm ơn bạn.
quế

1
Đối với tùy chọn 1, trình biên dịch có thể đã ghi chú trước i. Biết nó chỉ có thể xảy ra một lần, nó chỉ phát ra một lần. Đối với tùy chọn 2, mã được dịch sang mã máy theo nghĩa đen, giống như một dự án lớp trình biên dịch đại học có thể làm. Đối với tùy chọn 3, nó giống như tùy chọn 1, nhưng nó tạo ra hai bản sao của preincrement. Phải sử dụng một vectơ, không phải một tập hợp. :-)
Zan Lynx

@dxiv xin lỗi, tôi xấu quá, tôi đã trộn lẫn các bài đăng
muru

22

Đối với tôi, điều thứ hai có vẻ khả dĩ nhất.

Tôi đang đi cho lựa chọn số 4: Cả hai ++ixảy ra đồng thời.

Các bộ xử lý mới hơn tiến tới một số tối ưu hóa thú vị và đánh giá mã song song, nếu được phép như ở đây, là một cách khác mà trình biên dịch tiếp tục tạo mã nhanh hơn. Tôi xem như một triển khai thực tế , các trình biên dịch đang tiến tới song song.

Tôi có thể dễ dàng thấy điều kiện đua gây ra hành vi không xác định hoặc lỗi xe buýt do tranh chấp bộ nhớ giống nhau - tất cả đều được cho phép khi người lập trình vi phạm hợp đồng C ++ - do đó UB.

Câu hỏi của tôi là: những điều (hợp lý) nào mà một trình biên dịch C ++ có thể làm dẫn đến kết quả là 4 hoặc kết quả là 6?

có thể , nhưng không tính trong đó.

Không sử dụng ++i + ++ivà cũng không mong đợi kết quả hợp lý.


Nếu tôi có thể chấp nhận cả câu trả lời này và @ dxiv, tôi sẽ. Cảm ơn bạn đã phản hồi.
quế

4
@UriRaz: Bộ xử lý thậm chí có thể không nhận thấy có nguy cơ dữ liệu tùy thuộc vào lựa chọn của trình biên dịch. Ví dụ: trình biên dịch có thể gán icho hai thanh ghi, tăng cả hai thanh ghi và ghi lại cả hai. Bộ xử lý không có cách nào để giải quyết điều đó. Vấn đề cơ bản là cả C ++ và CPU hiện đại đều không tuần tự nghiêm ngặt. C ++ có trình tự xảy ra trước và xảy ra sau một cách rõ ràng, để cho phép xảy ra cùng lúc theo mặc định.
MSalters

1
Nhưng chúng tôi biết đó không phải là trường hợp của OP sử dụng Visual Studio; hầu hết các ISA chính thống bao gồm x86 và ARM được định nghĩa theo mô hình thực thi tuần tự hoàn toàn trong đó một lệnh máy kết thúc hoàn toàn trước khi lệnh tiếp theo bắt đầu. Superscalar ngoài trật tự phải duy trì ảo ảnh đó cho một luồng duy nhất. (Các luồng khác đọc bộ nhớ được chia sẻ không được đảm bảo để xem mọi thứ theo thứ tự chương trình, nhưng quy tắc cơ bản của OoO executive là không phá vỡ việc thực thi một luồng.)
Peter Cordes

1
Đây là câu trả lời yêu thích của tôi, vì nó là câu duy nhất đề cập đến việc thực thi lệnh song song ở mức CPU. Btw, rất vui khi được đề cập trong câu trả lời rằng do điều kiện chủng tộc, luồng cpu sẽ bị dừng chờ mở khóa mutex trên cùng một vị trí bộ nhớ, vì vậy điều này là rất tối ưu trong mô hình đồng thời. Thứ hai - do cùng một điều kiện chủng tộc, câu trả lời thực có thể là 4hoặc 5, - tùy thuộc vào mô hình / tốc độ thực thi luồng cpu, vì vậy đây chính là UB.
Agnius Vasiliauskas

1
@AgniusVasiliauskas Có lẽ, "Trong thực tế, tại sao các trình biên dịch khác nhau lại tính toán các giá trị khác nhau" đang tìm kiếm nhiều hơn để dễ hiểu hơn với một cái nhìn đơn giản về các bộ xử lý ngày nay. Tuy nhiên, phạm vi của các kịch bản trình biên dịch / bộ xử lý nếu lớn hơn nhiều so với một vài câu trả lời khác nhau được đề cập. Cái nhìn sâu sắc hữu ích của bạn lại là một cái khác. IMO, song song là tương lai và vì vậy câu trả lời này tập trung vào những điều đó, mặc dù theo một cách trừu tượng - vì tương lai vẫn đang mở ra. IAC, bài đăng đã trở nên phổ biến và các câu trả lời dễ nắm bắt sẽ được thưởng tốt nhất.
chux - Phục hồi Monica

17

Tôi nghĩ rằng một cách diễn giải đơn giản và dễ hiểu (không có bất kỳ giá thầu nào để tối ưu hóa trình biên dịch hoặc đa luồng) sẽ chỉ là:

  1. Tăng i
  2. Tăng i
  3. Thêm i+i

Với số ităng hai lần, giá trị của nó là 3 và khi cộng lại với nhau, tổng là 6.

Để kiểm tra, hãy coi đây là một hàm C ++:

int dblInc ()
{
    int i = 1;
    int x = ++i + ++i;
    return x;   
}

Bây giờ, đây là mã lắp ráp mà tôi nhận được khi biên dịch hàm đó, sử dụng phiên bản cũ của trình biên dịch GNU C ++ (win32, gcc phiên bản 3.4.2 (mingw-special)). Không có tối ưu hóa ưa thích hoặc đa luồng xảy ra ở đây:

__Z6dblIncv:
    push    ebp
    mov ebp, esp
    sub esp, 8
    mov DWORD PTR [ebp-4], 1
    lea eax, [ebp-4]
    inc DWORD PTR [eax]
    lea eax, [ebp-4]
    inc DWORD PTR [eax]
    mov eax, DWORD PTR [ebp-4]
    add eax, DWORD PTR [ebp-4]
    mov DWORD PTR [ebp-8], eax
    mov eax, DWORD PTR [ebp-8]
    leave
    ret

Lưu ý rằng biến cục bộ ichỉ nằm trên ngăn xếp ở một nơi duy nhất: địa chỉ [ebp-4]. Vị trí đó được tăng lên hai lần (trong dòng thứ 5-8 của hàm assembly; bao gồm các tải dường như dư thừa của địa chỉ đó vào eax). Sau đó, trên các dòng 9-10, giá trị đó được tải vào eaxvà sau đó được thêm vào eax(nghĩa là tính dòng điện i + i). Sau đó, nó được sao chép dư thừa vào ngăn xếp và trở lại eaxdưới dạng giá trị trả về (rõ ràng sẽ là 6).

Có thể cần quan tâm đến tiêu chuẩn C ++ (ở đây, tiêu chuẩn cũ: ISO / IEC 14882: 1998 (E)) nói về Biểu thức, phần 5.4:

Trừ khi được lưu ý, thứ tự đánh giá các toán hạng của các toán tử riêng lẻ và biểu thức con của các biểu thức riêng lẻ, và thứ tự diễn ra các phản ứng phụ, là không xác định.

Với chú thích cuối trang:

Mức độ ưu tiên của các toán tử không được chỉ định trực tiếp, nhưng nó có thể được suy ra từ cú pháp.

Hai ví dụ về hành vi không xác định được đưa ra tại thời điểm đó, cả hai đều liên quan đến toán tử tăng dần (một trong số đó là i = ++i + 1:).

Bây giờ, nếu muốn, người ta có thể: Tạo một lớp bao bọc số nguyên (như Java Integer); quá tải các hàm operator+operator++sao cho chúng trả về các đối tượng giá trị trung gian; và do đó viết ++iObj + ++iObjvà lấy nó để trả về một đối tượng đang giữ 5. (Tôi chưa đưa mã đầy đủ vào đây vì mục đích ngắn gọn.)

Cá nhân tôi rất thích thú nếu có một ví dụ về một trình biên dịch nổi tiếng đã thực hiện công việc theo bất kỳ cách nào khác với trình tự đã thấy ở trên. Đối với tôi, có vẻ như cách triển khai đơn giản nhất sẽ là chỉ thực hiện hai mã hợp ngữ inctrên kiểu nguyên thủy trước khi thao tác bổ sung được thực hiện.


2
Toán tử tăng dần thực sự có giá trị "trả về" được xác định rất rõ
edc65 Ngày

@philipxy: Tôi đã chỉnh sửa câu trả lời để loại bỏ những đoạn mà bạn phản đối. Bạn có thể đồng ý hoặc không đồng ý với câu trả lời nhiều hơn ở điểm này.
Daniel R. Collins

2
Đó không phải là "Hai ví dụ về hành vi không xác định", đó là hai ví dụ về hành vi không xác định , một con thú rất khác nhau, phát sinh từ một đoạn khác trong tiêu chuẩn. Tôi thấy C ++ 98 được sử dụng để nói "không xác định" trong văn bản của ví dụ của chú thích cuối trang, mâu thuẫn với văn bản quy chuẩn, nhưng điều đó đã được khắc phục sau đó.
Cubbi

@Cubbi: Cả văn bản và chú thích trong tiêu chuẩn được trích dẫn ở đây đều sử dụng cụm từ "không xác định", "không được chỉ định trực tiếp", và nó có vẻ khớp với cụm từ định nghĩa trong phần 1.3.13.
Daniel R. Collins

1
@philipxy: Tôi thấy rằng bạn đã lặp lại cùng một nhận xét cho nhiều câu trả lời ở đây. Có vẻ như bài phê bình chính của bạn thực sự thiên về câu hỏi của OP, phạm vi của nó không chỉ về tiêu chuẩn trừu tượng.
Daniel R. Collins

7

Điều hợp lý mà một trình biên dịch có thể làm là Loại bỏ Biểu thức Khối thông dụng. Đây đã là một cách tối ưu hóa phổ biến trong các trình biên dịch: nếu một biểu thức con giống như (x+1)xuất hiện nhiều lần trong một biểu thức lớn hơn, thì nó chỉ cần được tính toán một lần. Ví dụ như trong a/(x+1) + b*(x+1)các x+1tiểu biểu thức có thể được tính một lần.

Tất nhiên, trình biên dịch phải biết những biểu thức con nào có thể được tối ưu hóa theo cách đó. Gọi rand()hai lần nên cho hai số ngẫu nhiên. Do đó, các lệnh gọi hàm không nội tuyến phải được miễn trừ khỏi CSE. Như bạn lưu ý, không có quy tắc nào nói cách i++xử lý hai lần xuất hiện , vì vậy không có lý do gì để miễn chúng khỏi CSE.

Kết quả thực sự có thể int x = ++i + ++i;được tối ưu hóa cho int __cse = i++; int x = __cse << 1. (CSE, tiếp theo là giảm cường độ lặp lại)


Tiêu chuẩn không có gì để nói về mã đối tượng. Điều này không được chứng minh bởi hoặc liên quan đến định nghĩa ngôn ngữ.
philipxy

1
@philipxy: Tiêu chuẩn không có gì để nói về bất kỳ dạng Hành vi không xác định nào. Đó là tiền đề của câu hỏi.
MSalters

7

Trong thực tế, bạn đang gọi hành vi không xác định. Điều gì cũng có thể xảy ra, không chỉ những điều bạn cho là "hợp lý", và thường xảy ra mà bạn không cân nhắc hợp lý. Mọi thứ theo định nghĩa là "hợp lý".

Một biên dịch rất hợp lý là trình biên dịch quan sát rằng việc thực thi một câu lệnh sẽ gọi ra hành vi không xác định, do đó câu lệnh không thể được thực thi, do đó nó được dịch thành một lệnh cố ý làm hỏng ứng dụng của bạn. Điều đó rất hợp lý.

Người phản đối: GCC hoàn toàn không đồng ý với bạn.


Khi Tiêu chuẩn mô tả một điều gì đó là "hành vi không xác định", điều đó có nghĩa là hành vi đó nằm ngoài phạm vi quyền hạn của Tiêu chuẩn . Bởi vì Tiêu chuẩn không cố gắng đánh giá tính hợp lý của những điều nằm ngoài quyền tài phán của mình và không cố gắng ngăn cấm mọi cách mà việc triển khai tuân thủ có thể vô ích một cách vô lý, việc Tiêu chuẩn không áp đặt các yêu cầu trong một tình huống cụ thể không ngụ ý bất kỳ phán đoán nào rằng tất cả các hành động khả thi đều “hợp lý” như nhau.
supercat

6

Không có hợp lý điều gì mà một trình biên dịch có thể làm để nhận được kết quả là 6, nhưng nó có thể và hợp pháp. Kết quả của 4 là hoàn toàn hợp lý và tôi coi kết quả của 5 đường biên là hợp lý. Tất cả chúng đều hoàn toàn hợp pháp.

Này đợi đã! Không rõ điều gì phải xảy ra? Việc bổ sung cần kết quả của hai lần tăng, vì vậy rõ ràng những điều này phải xảy ra trước. Và chúng tôi đi từ trái sang phải, vì vậy ... argh! Giá như nó thật đơn giản. Thật không may, đó không phải là trường hợp. Chúng tôi không đi từ trái sang phải, và đó là vấn đề.

Đọc vị trí bộ nhớ thành hai thanh ghi (hoặc khởi tạo cả hai từ cùng một nghĩa đen, tối ưu hóa quá trình chuyển đổi vòng quanh bộ nhớ) là một việc rất hợp lý đối với trình biên dịch. Điều này thực sự sẽ có tác động là có hai biến khác nhau , mỗi biến có giá trị là 2, cuối cùng sẽ được cộng vào kết quả là 4. Điều này là "hợp lý" vì nó nhanh và hiệu quả, và nó phù hợp với cả hai tiêu chuẩn và với mã.

Tương tự, vị trí bộ nhớ có thể được đọc một lần (hoặc biến được khởi tạo từ ký tự) và tăng lên một lần, và một bản sao bóng trong một thanh ghi khác có thể được tăng lên sau đó, điều này sẽ dẫn đến 2 và 3 được thêm vào cùng nhau. Tôi có thể nói đây là đường biên giới hợp lý, mặc dù hoàn toàn hợp pháp. Tôi cho rằng nó là hợp lý bởi vì nó không phải là cái này hay cái khác. Đó không phải là cách được tối ưu hóa "hợp lý", cũng không phải là cách chính xác "hợp lý". Nó hơi ở giữa.

Tăng vị trí bộ nhớ hai lần (dẫn đến giá trị là 3) và sau đó thêm giá trị đó vào chính nó để có kết quả cuối cùng là 6 là hợp pháp, nhưng không hoàn toàn hợp lý vì thực hiện các chuyến đi vòng quanh bộ nhớ không hiệu quả chính xác. Mặc dù trên một bộ xử lý có chuyển tiếp cửa hàng tốt, nó cũng có thể là "hợp lý" để làm điều đó, vì cửa hàng chủ yếu là ẩn ...
Vì trình biên dịch "biết" rằng đó là cùng một vị trí, nó cũng có thể chọn tăng giá trị hai lần trong một thanh ghi, và sau đó thêm nó vào chính nó. Một trong hai cách tiếp cận sẽ cho bạn kết quả là 6.

Trình biên dịch, theo cách diễn đạt của tiêu chuẩn, được phép cung cấp cho bạn bất kỳ kết quả nào như vậy, mặc dù cá nhân tôi coi 6 khá nhiều là một bản ghi nhớ "fuck you" từ Bộ phận đáng ghét, vì nó là một điều khá bất ngờ (hợp pháp hay không, cố gắng luôn tạo ra ít bất ngờ nhất là điều nên làm!). Mặc dù vậy, nhìn thấy Hành vi không xác định có liên quan như thế nào, thật đáng buồn là người ta không thể thực sự tranh luận về "bất ngờ", eh.

Vì vậy, trên thực tế, mã mà bạn có ở đó, cho trình biên dịch là gì? Hãy hỏi clang, điều này sẽ cho chúng ta thấy nếu chúng ta hỏi độc đáo (gọi bằng -ast-dump -fsyntax-only):

ast.cpp:4:9: warning: multiple unsequenced modifications to 'i' [-Wunsequenced]
int x = ++i + ++i;
        ^     ~~
(some lines omitted)
`-CompoundStmt 0x2b3e628 <line:2:1, line:5:1>
  |-DeclStmt 0x2b3e4b8 <line:3:1, col:10>
  | `-VarDecl 0x2b3e430 <col:1, col:9> col:5 used i 'int' cinit
  |   `-IntegerLiteral 0x2b3e498 <col:9> 'int' 1
  `-DeclStmt 0x2b3e610 <line:4:1, col:18>
    `-VarDecl 0x2b3e4e8 <col:1, col:17> col:5 x 'int' cinit
      `-BinaryOperator 0x2b3e5f0 <col:9, col:17> 'int' '+'
        |-ImplicitCastExpr 0x2b3e5c0 <col:9, col:11> 'int' <LValueToRValue>
        | `-UnaryOperator 0x2b3e570 <col:9, col:11> 'int' lvalue prefix '++'
        |   `-DeclRefExpr 0x2b3e550 <col:11> 'int' lvalue Var 0x2b3e430 'i' 'int'
        `-ImplicitCastExpr 0x2b3e5d8 <col:15, col:17> 'int' <LValueToRValue>
          `-UnaryOperator 0x2b3e5a8 <col:15, col:17> 'int' lvalue prefix '++'
            `-DeclRefExpr 0x2b3e588 <col:17> 'int' lvalue Var 0x2b3e430 'i' 'int'

Như bạn có thể thấy, lvalue Var 0x2b3e430tiền tố giống nhau ++được áp dụng tại hai vị trí và hai vị trí này nằm dưới cùng một nút trong cây, điều này xảy ra là một toán tử rất không đặc biệt (+) không có gì đặc biệt về trình tự hoặc tương tự. Tại sao nó lại quan trọng? Vâng, hãy đọc tiếp.

Lưu ý cảnh báo: "nhiều sửa đổi không có hàng rào đối với 'i'" . Ồ ồ, nghe không ổn. Nó có nghĩa là gì? [basic.exec] cho chúng ta biết về các hiệu ứng phụ và trình tự, và nó cho chúng ta biết (đoạn 10) rằng theo mặc định, trừ khi có quy định rõ ràng khác, các đánh giá về toán hạng của các toán tử riêng lẻ và biểu thức con của các biểu thức riêng lẻ là không có giải trình tự . Chà, anh yêu, đó là trường hợp operator+- không có gì được nói khác, vì vậy ...

Nhưng chúng ta có quan tâm đến trình tự có trước, không xác định, hay không có trình tự? Ai muốn biết, dù sao?

Cũng đoạn văn đó cũng cho chúng ta biết rằng các đánh giá không có kết quả có thể trùng lặp và khi chúng đề cập đến cùng một vị trí bộ nhớ (trường hợp đó!) Và một đánh giá không có khả năng xảy ra đồng thời, thì hành vi là không xác định. Đây là nơi mà nó thực sự trở nên xấu xí bởi vì điều đó có nghĩa là bạn không biết gì, và bạn không có gì đảm bảo về việc "hợp lý". Điều phi lý thực ra hoàn toàn có thể cho phép và “hợp lý”.


Việc sử dụng "hợp lý" chỉ để ngăn bất cứ ai nói rằng "trình biên dịch có thể làm BẤT CỨ ĐIỀU GÌ, thậm chí phát ra lệnh duy nhất 'set x to 7.'" có lẽ tôi nên làm rõ.
quế

@cinnamon Nhiều năm trước, khi tôi còn trẻ và chưa có kinh nghiệm, các kỹ sư biên dịch tại Sun đã nói với tôi rằng trình biên dịch của họ hoạt động hoàn toàn hợp lý khi tạo ra mã cho hành vi không xác định mà tôi thấy không hợp lý vào thời điểm đó. Bài học kinh nghiệm.
gnasher729

Tiêu chuẩn không có gì để nói về mã đối tượng. Điều này là rời rạc và không rõ ràng về cách triển khai đề xuất của bạn được chứng minh bởi hoặc liên quan đến định nghĩa ngôn ngữ.
philipxy

@philipxy: Tiêu chuẩn xác định những gì được hình thành và xác định rõ và những gì không. Trong trường hợp của Q này, nó xác định hành vi là không xác định. Ngoài tính hợp pháp, còn có giả định hợp lý về các trình biên dịch tạo ra mã hiệu quả. Vâng, bạn đúng, tiêu chuẩn không yêu cầu như vậy. Tuy nhiên, đó là một giả định hợp lý.
Damon

1

Có một quy tắc :

Giữa điểm trình tự trước và điểm tiếp theo, một đối tượng vô hướng phải có giá trị được lưu trữ của nó được sửa đổi nhiều nhất một lần bằng cách đánh giá một biểu thức, nếu không thì hành vi là không xác định.

Do đó, ngay cả x = 100 là một kết quả hợp lệ có thể có.

Đối với tôi, kết quả hợp lý nhất trong ví dụ này là 6, bởi vì chúng ta đang tăng giá trị của i lên hai lần và chúng tự cộng nó vào. Rất khó để thực hiện phép cộng trước các giá trị tính toán từ cả hai phía của "+".

Nhưng các nhà phát triển trình biên dịch có thể thực hiện bất kỳ logic nào khác.


0

Có vẻ như ++ tôi trả về giá trị nhưng i ++ trả về giá trị.
Vì vậy, mã này là ok:

int i = 1;
++i = 10;
cout << i << endl;

Cái này không phải là:

int i = 1;
i++ = 10;
cout << i << endl;

Hai câu lệnh trên phù hợp với VisualC ++, GCC7.1.1, CLang và Embarcadero.
Đó là lý do tại sao mã của bạn trong VisualC ++ và GCC7.1.1 tương tự như sau

int i = 1;
... do something there for instance: ++i; ++i; ...
int x = i + i;

Khi nhìn vào quá trình tháo rời, đầu tiên nó tăng i, viết lại i. Khi cố gắng thêm nó cũng làm điều tương tự, hãy tăng i và viết lại nó. Sau đó thêm i vào i. Tôi nhận thấy CLang và Embarcadero hành động khác nhau. Vì vậy, nó không nhất quán với câu lệnh đầu tiên, sau ++ i đầu tiên, nó lưu trữ kết quả trong một giá trị và sau đó thêm nó vào i ++ thứ hai.
nhập mô tả hình ảnh ở đây


Vấn đề với "look line an lvalue" là bạn đang nói từ quan điểm của tiêu chuẩn C ++, không phải trình biên dịch.
MSalters

@MSalters Tuyên bố phù hợp với VisualStudio 2019, GCC7.1.1, clang và Embarcadero, và với đoạn mã đầu tiên. Vì vậy, đặc điểm kỹ thuật là nhất quán. Nhưng nó hoạt động khác đối với đoạn mã thứ hai. Đoạn mã thứ hai phù hợp với VisualStudio 2019 và GCC7.1.1 nhưng không nhất quán với clang và Embarcadero.
armagedescu

3
Chà, đoạn mã đầu tiên trong câu trả lời của bạn là C ++ hợp pháp, vì vậy rõ ràng việc triển khai phù hợp với đặc tả. So với câu hỏi, "làm gì đó" của bạn kết thúc bằng dấu chấm phẩy, khiến nó trở thành một câu lệnh đầy đủ. Điều đó tạo ra một trình tự theo yêu cầu của tiêu chuẩn C ++, nhưng không có trong câu hỏi.
MSalters

@MSalters Tôi muốn làm điều đó như một mã giả tương đương. Tuy nhiên, tôi không chắc làm thế nào để định dạng lại nó
armagedescu

0

Cá nhân tôi sẽ không bao giờ mong đợi một trình biên dịch xuất ra 6 trong ví dụ của bạn. Đã có câu trả lời tốt và chi tiết cho câu hỏi của bạn. Tôi sẽ thử một phiên bản ngắn.

Về cơ bản, ++ilà một quy trình gồm 2 bước trong bối cảnh này:

  1. Tăng giá trị của i
  2. Đọc giá trị của i

Trong bối cảnh của ++i + ++ihai bên, việc bổ sung có thể được đánh giá theo bất kỳ thứ tự nào theo tiêu chuẩn. Điều này có nghĩa là hai gia số được coi là độc lập. Ngoài ra, không có sự phụ thuộc giữa hai thuật ngữ. Do đó, số gia và số đọc icó thể được xen kẽ. Điều này mang lại thứ tự tiềm năng:

  1. Gia số icho toán hạng bên trái
  2. Tăng icho toán hạng bên phải
  3. Đọc lại itoán hạng bên trái
  4. Đọc lại iđể biết toán hạng phù hợp
  5. Tính tổng hai: cho kết quả 6

Bây giờ, tôi nghĩ về điều này, 6 có ý nghĩa nhất theo tiêu chuẩn. Đối với kết quả là 4, chúng ta cần một CPU đầu tiên đọc iđộc lập, sau đó tăng dần và ghi giá trị trở lại cùng một vị trí; về cơ bản là một điều kiện chủng tộc. Đối với giá trị 5, chúng tôi cần một trình biên dịch giới thiệu các thời gian tạm thời.

Tuy nhiên, tiêu chuẩn nói rằng ++ităng biến trước khi trả về, tức là trước khi thực sự thực thi dòng mã hiện tại. Toán tử sum +cần tính tổng i + isau khi áp dụng số gia. Tôi muốn nói rằng C ++ cần hoạt động trên các biến chứ không phải trên ngữ nghĩa giá trị. Do đó, đối với tôi 6 bây giờ có ý nghĩa nhất vì nó dựa trên ngữ nghĩa của ngôn ngữ chứ không phải mô hình thực thi của CPU.


0
#include <stdio.h>


void a1(void)
{
    int i = 1;
    int x = ++i;
    printf("i=%d\n",i);
    printf("x=%d\n",x);
    x = x + ++i;    // Here
    printf("i=%d\n",i);
    printf("x=%d\n",x);
}


void b2(void)
{
    int i = 1;
    int x = ++i;
    printf("i=%d\n",i);
    printf("x=%d\n",x);
    x = i + ++i;    // Here
    printf("i=%d\n",i);
    printf("x=%d\n",x);
}


void main(void)
{
    a1();
    // b2();
}

Chào mừng bạn đến với stackoverflow! Bạn có thể cung cấp bất kỳ hạn chế, giả định hoặc đơn giản hóa nào trong câu trả lời của mình không. Xem thêm chi tiết về cách trả lời tại liên kết này: stackoverflow.com/help/how-to-answer
Usama Abdulrehman

0

Nó phụ thuộc vào thiết kế của trình biên dịch, do đó câu trả lời sẽ phụ thuộc vào cách trình biên dịch giải mã các câu lệnh. Sử dụng hai biến khác nhau ++ x và ++ y để tạo logic sẽ là lựa chọn tốt hơn. lưu ý: sự thay đổi tùy thuộc vào phiên bản của phiên bản ngôn ngữ mới nhất trong ms visual studio nếu nó được cập nhật. Vì vậy, nếu các quy tắc đã thay đổi thì đầu ra cũng sẽ


0

Thử đi

int i = 1;
int i1 = i, i2 = i;   // i1 = i2 = 1
++i1;                 // i1 = 2
++i2;                 // i2 = 2
int x = i1 + i2;      // x = 4

-4

Trong thực tế, bạn đang gọi hành vi không xác định. Bất cứ điều gì có thể xảy ra, không chỉ là những điều mà bạn cho là "hợp lý", và thường điều xảy ra mà bạn không cân nhắc hợp lý. Mọi thứ theo định nghĩa là "hợp lý".

Một cách biên dịch rất hợp lý là trình biên dịch quan sát rằng việc thực thi một câu lệnh sẽ gọi ra hành vi không xác định, do đó câu lệnh không thể được thực thi, do đó nó được dịch thành một lệnh cố ý làm hỏng ứng dụng của bạn. Điều đó rất hợp lý. Rốt cuộc, trình biên dịch biết rằng sự cố này không bao giờ có thể xảy ra.


1
Tôi tin rằng bạn đã hiểu sai câu hỏi. Câu hỏi là về hành vi chung hoặc hành vi cụ thể có thể dẫn đến kết quả cụ thể (kết quả của x = 4, 5 hoặc 6). Nếu bạn không thích cách tôi sử dụng từ "hợp lý", tôi hướng dẫn bạn đến nhận xét của tôi ở trên, mà bạn đã trả lời: "Việc sử dụng từ 'hợp lý' chỉ để ngăn bất kỳ ai nói rằng 'trình biên dịch có thể làm BẤT CỨ ĐIỀU GÌ, thậm chí còn phát ra lệnh duy nhất '"đặt x thành 7." "" Nếu bạn có cách diễn đạt tốt hơn cho câu hỏi mà vẫn giữ được ý tưởng chung, tôi rất sẵn lòng. Ngoài ra, có vẻ như bạn đã đăng lại câu trả lời của mình.
quế

2
đề nghị xóa một trong hai câu trả lời của bạn vì cả hai đều rất giống nhau
MM
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.