Câu trả lời đúng cho cout << a ++ << a;?


98

Gần đây trong một cuộc phỏng vấn có một câu hỏi dạng khách quan sau đây.

int a = 0;
cout << a++ << a;

Câu trả lời:

a. 10
b. 01
c. hành vi không xác định

Tôi đã trả lời lựa chọn b, tức là đầu ra sẽ là "01".

Nhưng thật ngạc nhiên sau đó, tôi được một người phỏng vấn cho biết rằng câu trả lời đúng là phương án c: undefined.

Bây giờ, tôi đã biết khái niệm về điểm trình tự trong C ++. Hành vi không được xác định cho câu lệnh sau:

int i = 0;
i += i++ + i++;

nhưng theo sự hiểu biết của tôi đối với câu lệnh cout << a++ << a, ostream.operator<<()sẽ được gọi hai lần, trước với ostream.operator<<(a++)và sau ostream.operator<<(a).

Tôi cũng đã kiểm tra kết quả trên trình biên dịch VS2010 và đầu ra của nó cũng là '01'.


30
Bạn đã yêu cầu một lời giải thích? Tôi thường phỏng vấn các ứng viên tiềm năng và khá thích thú khi nhận được câu hỏi, điều đó thể hiện sự quan tâm.
Brady

3
@jrok Đó là hành vi không xác định. Bất cứ điều gì mà việc triển khai thực hiện (bao gồm cả việc gửi một email xúc phạm nhân danh bạn đến sếp của bạn) đều tuân thủ.
James Kanze

2
Câu hỏi này đang kêu lên một câu trả lời C ++ 11 ( phiên bản hiện tại của C ++) không đề cập đến điểm trình tự. Rất tiếc, tôi không đủ hiểu biết về việc thay thế các điểm trình tự trong C ++ 11.
CB Bailey

3
Nếu nó không được xác định thì chắc chắn không thể có 10, nó sẽ là 01hoặc 00. ( c++sẽ luôn đánh giá đến giá trị ctrước khi được tăng lên). Và ngay cả khi nó không được xác định, nó vẫn sẽ rất khó hiểu.
bùng binh

2
Ya biết, khi tôi đọc tiêu đề “cout << c ++ << c”, tôi đã thoáng nghĩ nó như một tuyên bố về mối quan hệ giữa ngôn ngữ C và C ++, và một số khác có tên là “cout”. Bạn biết đấy, giống như ai đó đã nói rằng họ nghĩ rằng “cout” kém hơn nhiều so với C ++, và C ++ kém hơn nhiều so với C - và có lẽ bởi độ nhạy cảm mà “cout” kém hơn rất nhiều so với C. :)
tchrist

Câu trả lời:


145

Bạn có thể nghĩ đến:

cout << a++ << a;

Như:

std::operator<<(std::operator<<(std::cout, a++), a);

C ++ đảm bảo rằng tất cả các tác dụng phụ của các đánh giá trước đó sẽ được thực hiện tại các điểm trình tự . Không có điểm trình tự nào giữa đánh giá đối số hàm có nghĩa là đối số đó acó thể được đánh giá trước std::operator<<(std::cout, a++)hoặc sau đối số . Vì vậy, kết quả của trên là không xác định.


Cập nhật C ++ 17

Trong C ++ 17, các quy tắc đã được cập nhật. Đặc biệt:

Trong một biểu thức toán tử shift E1<<E2E1>>E2, mọi phép tính giá trị và tác dụng phụ của E1đều được sắp xếp theo trình tự trước mọi phép tính giá trị và hiệu ứng phụ của E2.

Có nghĩa là nó yêu cầu mã để tạo ra kết quả b, mã nào sẽ xuất ra 01.

Xem P0145R3 Trình tự đánh giá biểu thức tinh chỉnh cho Idiomatic C ++ để biết thêm chi tiết.


@Maxim: Cảm ơn vì đã giải thích. Với các cuộc gọi bạn giải thích, nó sẽ là hành vi không xác định. Nhưng bây giờ, tôi có một câu hỏi nữa (có thể là câu hỏi khó hơn, và tôi thiếu một số thứ cơ bản và đang suy nghĩ lung tung) Làm thế nào bạn suy luận rằng phiên bản toàn cầu của std :: operator << () sẽ được gọi thay vì ostream :: operator < <() phiên bản thành viên. Khi gỡ lỗi, tôi đang cập nhật phiên bản thành viên của cuộc gọi ostream :: operator << () chứ không phải phiên bản toàn cầu và đó là lý do mà ban đầu tôi nghĩ rằng câu trả lời sẽ là 01
pravs

@Maxim Không phải là nó tạo ra sự khác biệt, nhưng vì ccó kiểu intnên operator<<đây là các hàm thành viên.
James Kanze

2
@pravs: dù operator<<là chức năng thành viên hay chức năng tự do đều không ảnh hưởng đến điểm trình tự.
Maxim Egorushkin

11
'Điểm trình tự' không còn được sử dụng trong tiêu chuẩn C ++. Nó không chính xác và đã được thay thế bằng quan hệ 'được sắp xếp trước / được sắp xếp sau'.
Rafał Dowgird

2
So the result of the above is undefined.Lời giải thích của bạn chỉ tốt cho những trường hợp không xác định , không phải cho không xác định . JamesKanze đã giải thích nó như thế nào càng không được xác định trong câu trả lời của anh ấy .
Deduplicator

68

Về mặt kỹ thuật, nhìn chung đây là Hành vi không xác định .

Nhưng, có hai khía cạnh quan trọng cho câu trả lời.

Câu lệnh mã:

std::cout << a++ << a;

được đánh giá là:

std::operator<<(std::operator<<(std::cout, a++), a);

Tiêu chuẩn không xác định thứ tự đánh giá các đối số cho một hàm.
Vì vậy:

  • std::operator<<(std::cout, a++) được đánh giá đầu tiên hoặc
  • ađược đánh giá đầu tiên hoặc
  • nó có thể là bất kỳ thứ tự được xác định triển khai nào.

Đơn đặt hàng này không được chỉ định [Tham khảo 1] theo tiêu chuẩn.

[Tham khảo 1] C ++ 03 5.2.2 Lệnh gọi hàm
Đoạn 8

Thứ tự đánh giá các lập luận là không xác định . Tất cả các hiệu ứng phụ của đánh giá biểu thức đối số có hiệu lực trước khi hàm được nhập. Thứ tự đánh giá của biểu thức hậu tố và danh sách biểu thức đối số là không xác định.

Hơn nữa, không có điểm trình tự giữa việc đánh giá các đối số cho một hàm nhưng một điểm trình tự chỉ tồn tại sau khi đánh giá tất cả các đối số [Tham khảo 2] .

[Tham khảo 2] C ++ 03 1.9 Thực thi chương trình [intro.execution]:
Đoạn 17:

Khi gọi một hàm (cho dù hàm có nội tuyến hay không), có một điểm trình tự sau khi đánh giá tất cả các đối số của hàm (nếu có) diễn ra trước khi thực hiện bất kỳ biểu thức hoặc câu lệnh nào trong thân hàm.

Lưu ý rằng, ở đây giá trị của cđược truy cập nhiều lần mà không có điểm trình tự can thiệp, về điều này, tiêu chuẩn cho biết:

[Tham khảo 3] C ++ 03 5 Biểu thức [expr]:
Đoạn 4:

....
Giữa điểm trình tự trước và điểm tiếp theo, một đối tượng vô hướng sẽ 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. Hơn nữa, giá trị trước sẽ chỉ được truy cập để xác định giá trị được lưu trữ . Các yêu cầu của đoạn này phải được đáp ứng đối với từng thứ tự cho phép của các biểu thức phụ của một biểu thức đầy đủ; nếu không thì hành vi là không xác định .

Mã sửa đổi cnhiều lần mà không có điểm trình tự can thiệp và nó không được truy cập để xác định giá trị của đối tượng được lưu trữ. Điều này rõ ràng là vi phạm điều khoản trên và do đó kết quả theo quy định của tiêu chuẩn là Hành vi không xác định [Hướng dẫn 3] .


1
Về mặt kỹ thuật, hành vi là không xác định, bởi vì có sự sửa đổi đối tượng và truy cập nó ở nơi khác mà không có điểm trình tự can thiệp. Không xác định không phảikhông xác định; nó khiến việc triển khai thậm chí còn nhiều thời gian hơn.
James Kanze

1
@Als Có. Tôi đã không thấy các chỉnh sửa của bạn (mặc dù tôi đã phản ứng với tuyên bố của jrok rằng chương trình không thể làm điều gì đó kỳ lạ --- nó có thể). Phiên bản đã chỉnh sửa của bạn là tốt khi nó đi, nhưng trong tâm trí của tôi, từ khóa là sắp xếp một phần ; các điểm trình tự chỉ giới thiệu một thứ tự một phần.
James Kanze

1
@ Xin cảm ơn vì một mô tả chi tiết, thực sự rất hữu ích !!
pravs

4
Tiêu chuẩn C ++ 0x mới nói về cơ bản là giống nhau nhưng ở các phần khác nhau và theo cách diễn đạt khác nhau :) Trích dẫn: (1.9 Program Execution [intro.execution], par 15): "Nếu một tác dụng phụ trên một đối tượng vô hướng là không có hàng rào so với hoặc là 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 phép tính giá trị sử dụng giá trị của cùng một đối tượng vô hướng, thì hành vi là hoàn tác. "
Rafał Dowgird

2
Tôi tin rằng có một lỗi trong câu trả lời này. "std :: cout << c ++ << c;" không thể dịch thành "std :: operator << (std :: operator << (std :: cout, c ++), c)", bởi vì std :: operator << (std :: ostream &, int) không tồn tại. Thay vào đó, nó dịch thành "std :: cout.operator << (c ++). Operator (c);", thực sự có một điểm trình tự giữa đánh giá của "c ++" và "c" (một toán tử được nạp chồng được coi là một lệnh gọi hàm và do đó có một điểm trình tự khi lệnh gọi hàm trả về). Do đó, hành vi và thứ tự thực hiện được chỉ định.
Christopher Smith

20

Các điểm trình tự chỉ xác định thứ tự từng phần . Trong trường hợp của bạn, bạn có (sau khi giải quyết quá tải được thực hiện):

std::cout.operator<<( a++ ).operator<<( a );

Có một điểm trình tự giữa a++lệnh gọi đến và đầu tiên std::ostream::operator<<, và có một điểm trình tự giữa lệnh gọi thứ hai avà thứ hai std::ostream::operator<<, nhưng không có điểm trình tự nào giữa a++a; các ràng buộc về thứ tự duy nhất a++được đánh giá đầy đủ (bao gồm cả các tác dụng phụ) trước lần gọi đầu tiên operator<<và rằng điều thứ hai ađược đánh giá đầy đủ trước lần gọi thứ hai tới operator<<. (Ngoài ra còn có các ràng buộc về thứ tự nhân quả: lệnh gọi thứ hai operator<<không thể đứng trước lệnh gọi đầu tiên, vì nó yêu cầu kết quả của lệnh đầu tiên làm đối số.) §5 / 4 (C ++ 03) nêu rõ:

Ngoại 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 tác dụng phụ, không được xác định. 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. Hơn nữa, giá trị trước sẽ chỉ được truy cập để xác định giá trị được lưu trữ. Các yêu cầu của đoạn này phải được đáp ứng đối với từng thứ tự cho phép của các biểu thức phụ của một biểu thức đầy đủ; nếu không thì hành vi là không xác định.

Một trong những câu lệnh cho phép của biểu thức của bạn là a++, acuộc gọi đầu tiên tới operator<<, cuộc gọi thứ hai tới operator<<; điều này sửa đổi giá trị được lưu trữ của a( a++), và truy cập nó ngoài việc xác định giá trị mới (thứ hai a), hành vi là không xác định.


Một trong những câu trích dẫn của bạn về tiêu chuẩn. "Ngoại trừ nơi lưu ý", IIRC, bao gồm một ngoại lệ khi xử lý một toán tử được nạp chồng, xử lý toán tử như một hàm và do đó tạo ra một điểm trình tự giữa lần gọi đầu tiên và thứ hai tới std :: ostream :: operator << (int ). Xin vui lòng sửa cho tôi nếu tôi sai.
Christopher Smith

@ChristopherSmith Một toán tử quá tải hoạt động giống như một cuộc gọi hàm. Thay vào đó, nếu clà kiểu người dùng với người dùng được xác định ++, intkết quả sẽ không xác định, nhưng sẽ không có hành vi không xác định.
James Kanze

1
@ChristopherSmith Bạn thấy điểm trình tự giữa hai điểm cfoo(foo(bar(c)), c)đâu? Có một điểm trình tự khi các hàm được gọi và khi chúng trả về, nhưng không có lệnh gọi hàm nào được yêu cầu giữa các đánh giá của cả hai c.
James Kanze

1
@ChristopherSmith Nếu clà một UDT, các toán tử được nạp chồng sẽ là các lệnh gọi hàm và sẽ giới thiệu một điểm trình tự, vì vậy hành vi sẽ không được xác định. Nhưng vẫn chưa xác định được liệu biểu thức phụ cđược đánh giá trước hay sau c++, vì vậy việc bạn có phiên bản tăng dần hay không sẽ không được chỉ định (và về lý thuyết, mỗi lần sẽ không giống nhau).
James Kanze

1
@ChristopherSmith Mọi thứ trước điểm trình tự sẽ xảy ra trước mọi thứ sau điểm trình tự. Nhưng các điểm trình tự chỉ xác định thứ tự một phần. Ví dụ, trong biểu thức được đề cập, không có điểm trình tự nào giữa các biểu thức con cc++, vì vậy cả hai có thể xảy ra theo bất kỳ thứ tự nào. Đối với dấu chấm phẩy ... Chúng chỉ gây ra một điểm trình tự khi chúng là biểu thức đầy đủ. Điểm chuỗi quan trọng khác là gọi hàm: f(c++)sẽ thấy tăng lên ctrong f, và các nhà điều hành dấu phẩy, &&, ||?:cũng gây ra các điểm theo thứ tự.
James Kanze

4

Câu trả lời chính xác là đặt câu hỏi. Tuyên bố là không thể chấp nhận được bởi vì người đọc không thể thấy câu trả lời rõ ràng. Một cách khác để xem xét nó là chúng tôi đã đưa ra các hiệu ứng phụ (c ++) khiến câu lệnh khó diễn giải hơn nhiều. Mã ngắn gọn là tuyệt vời, miễn là ý nghĩa của nó rõ ràng.


4
Câu hỏi có thể cho thấy một thực hành lập trình kém (và thậm chí C ++ không hợp lệ). Nhưng một câu trả lời được cho là trả lời cho câu hỏi chỉ ra điều gì sai và tại sao nó sai. Một bình luận về câu hỏi không phải là câu trả lời ngay cả khi chúng hoàn toàn hợp lệ. Tốt nhất, đây có thể là một bình luận, không phải là một câu trả lời.
PP
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.