Mã thể hiện hành vi không xác định do thứ tự đánh giá không xác định của các biểu thức con mặc dù nó không gọi ra hành vi không xác định vì tất cả các tác dụng phụ được thực hiện trong các hàm tạo ra mối quan hệ trình tự giữa các tác dụng phụ trong trường hợp này.
Ví dụ này được đề cập trong đề xuất N4228: Tinh chỉnh Thứ tự Đánh giá Biểu thức cho Idiomatic C ++ cho biết những điều sau về mã trong câu hỏi:
[...] Mã này đã được các chuyên gia C ++ trên toàn thế giới xem xét và xuất bản (Ngôn ngữ lập trình C ++, ấn bản lần thứ 4. ) Tuy nhiên, lỗ hổng đối với thứ tự đánh giá không xác định của nó chỉ được phát hiện gần đây bởi một công cụ [.. .]
Chi tiết
Đối với nhiều người, có thể rõ ràng rằng các đối số cho các hàm có một thứ tự đánh giá không xác định nhưng có lẽ không rõ ràng về cách hành vi này tương tác với các lệnh gọi hàm chuỗi. Tôi không rõ lắm khi lần đầu tiên tôi phân tích trường hợp này và dường như không phải đối với tất cả các chuyên gia đánh giá .
Thoạt nhìn, có thể thấy rằng vì mỗi nhóm replace
phải được đánh giá từ trái sang phải nên các nhóm đối số chức năng tương ứng cũng phải được đánh giá như các nhóm từ trái sang phải.
Điều này không chính xác, các đối số hàm có thứ tự đánh giá không xác định, mặc dù các lệnh gọi hàm chuỗi không giới thiệu thứ tự đánh giá từ trái sang phải cho mỗi lệnh gọi hàm, các đối số của mỗi lệnh gọi hàm chỉ được sắp xếp theo thứ tự trước đó đối với lệnh gọi hàm thành viên mà chúng là một phần của. Đặc biệt, điều này ảnh hưởng đến các lệnh gọi sau:
s.find( "even" )
và:
s.find( " don't" )
được sắp xếp theo trình tự không xác định đối với:
s.replace(0, 4, "" )
hai find
cuộc gọi có thể được đánh giá trước hoặc sau replace
, điều này quan trọng vì nó có tác dụng phụ s
theo cách có thể thay đổi kết quả của find
, nó thay đổi độ dài của s
. Vì vậy, tùy thuộc vào thời điểm replace
được đánh giá liên quan đến hai find
cuộc gọi, kết quả sẽ khác nhau.
Nếu chúng ta nhìn vào biểu thức chuỗi và kiểm tra thứ tự đánh giá của một số biểu thức con:
s.replace(0, 4, "" ).replace( s.find( "even" ), 4, "only" )
^ ^ ^ ^ ^ ^ ^ ^ ^
A B | | | C | | |
1 2 3 4 5 6
và:
.replace( s.find( " don't" ), 6, "" );
^ ^ ^ ^
D | | |
7 8 9
Lưu ý, chúng tôi đang bỏ qua thực tế đó 4
và 7
có thể được chia nhỏ thành nhiều biểu thức phụ hơn. Vì thế:
A
được trình tự trước B
đó được trình tự trước C
đó được giải trình tự trướcD
1
để 9
được indeterminately trình tự đối với phụ biểu thức khác với một số trường hợp ngoại lệ được liệt kê dưới đây với
1
để 3
được sắp xếp trướcB
4
để 6
được sắp xếp trướcC
7
để 9
được sắp xếp trướcD
Chìa khóa của vấn đề này là:
4
để 9
được indeterminately trình tự đối với vớiB
Trình tự tiềm năng của sự lựa chọn đánh giá cho 4
và 7
đối với với B
giải thích sự khác biệt trong kết quả giữa clang
và gcc
khi đánh giá f2()
. Trong các bài kiểm tra của tôi clang
đánh giá B
trước khi đánh giá 4
và 7
trong khi gcc
đánh giá nó sau. Chúng tôi có thể sử dụng chương trình thử nghiệm sau để chứng minh điều gì đang xảy ra trong từng trường hợp:
#include <iostream>
#include <string>
std::string::size_type my_find( std::string s, const char *cs )
{
std::string::size_type pos = s.find( cs ) ;
std::cout << "position " << cs << " found in complete expression: "
<< pos << std::endl ;
return pos ;
}
int main()
{
std::string s = "but I have heard it works even if you don't believe in it" ;
std::string copy_s = s ;
std::cout << "position of even before s.replace(0, 4, \"\" ): "
<< s.find( "even" ) << std::endl ;
std::cout << "position of don't before s.replace(0, 4, \"\" ): "
<< s.find( " don't" ) << std::endl << std::endl;
copy_s.replace(0, 4, "" ) ;
std::cout << "position of even after s.replace(0, 4, \"\" ): "
<< copy_s.find( "even" ) << std::endl ;
std::cout << "position of don't after s.replace(0, 4, \"\" ): "
<< copy_s.find( " don't" ) << std::endl << std::endl;
s.replace(0, 4, "" ).replace( my_find( s, "even" ) , 4, "only" )
.replace( my_find( s, " don't" ), 6, "" );
std::cout << "Result: " << s << std::endl ;
}
Kết quả cho gcc
( xem trực tiếp )
position of even before s.replace(0, 4, "" ): 26
position of don't before s.replace(0, 4, "" ): 37
position of even after s.replace(0, 4, "" ): 22
position of don't after s.replace(0, 4, "" ): 33
position don't found in complete expression: 37
position even found in complete expression: 26
Result: I have heard it works evenonlyyou donieve in it
Kết quả cho clang
( xem trực tiếp ):
position of even before s.replace(0, 4, "" ): 26
position of don't before s.replace(0, 4, "" ): 37
position of even after s.replace(0, 4, "" ): 22
position of don't after s.replace(0, 4, "" ): 33
position even found in complete expression: 22
position don't found in complete expression: 33
Result: I have heard it works only if you believe in it
Kết quả cho Visual Studio
( xem trực tiếp ):
position of even before s.replace(0, 4, "" ): 26
position of don't before s.replace(0, 4, "" ): 37
position of even after s.replace(0, 4, "" ): 22
position of don't after s.replace(0, 4, "" ): 33
position don't found in complete expression: 37
position even found in complete expression: 26
Result: I have heard it works evenonlyyou donieve in it
Chi tiết từ tiêu chuẩn
Chúng tôi biết rằng trừ khi được chỉ định, các đánh giá của biểu thức con là không có câu trả lời, đây là từ bản nháp phần chuẩn C ++ 11 Thực 1.9
thi chương trình cho biết:
Ngoại trừ khi được lưu ý, 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ó câu trả lời. [...]
và chúng ta biết rằng một lệnh gọi hàm giới thiệu một mối quan hệ có trình tự trước của hàm gọi biểu thức hậu tố và các đối số liên quan đến thân hàm, từ phần 1.9
:
[...] Khi gọi một hàm (cho dù hàm có nội tuyến hay không), mọi phép tính giá trị và hiệu ứng phụ được liên kết với bất kỳ biểu thức đối số nào hoặc với biểu thức hậu tố chỉ định hàm được gọi, sẽ được sắp xếp theo trình 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. […]
Chúng tôi cũng biết rằng quyền truy cập của thành viên trong lớp và do đó chuỗi sẽ đánh giá từ trái sang phải, từ phần 5.2.5
Quyền truy cập thành viên trong lớp cho biết:
[...] Biểu thức hậu tố trước dấu chấm hoặc mũi tên được đánh giá; 64
kết quả của đánh giá đó, cùng với biểu thức id, xác định kết quả của toàn bộ biểu thức hậu tố.
Lưu ý, trong trường hợp biểu thức id kết thúc là một hàm thành viên không tĩnh, nó không chỉ định thứ tự đánh giá của danh sách biểu thức trong ()
vì đó là một biểu thức con riêng biệt. Ngữ pháp có liên quan từ các 5.2
biểu thức Postfix :
postfix-expression:
postfix-expression ( expression-listopt) // function call
postfix-expression . templateopt id-expression // Class member access, ends
// up as a postfix-expression
C ++ 17 thay đổi
Đề xuất p0145r3: Tinh chỉnh Thứ tự Đánh giá Biểu thức cho Idiomatic C ++ đã thực hiện một số thay đổi. Bao gồm các thay đổi cung cấp cho mã hành vi được chỉ định rõ ràng bằng cách củng cố thứ tự của các quy tắc đánh giá cho các biểu thức hậu tố và danh sách biểu thức của chúng .
[expr.call] p5 nói:
Biểu thức hậu tố được sắp xếp theo trình tự trước mỗi biểu thức trong danh sách biểu thức và bất kỳ đối số mặc định nào . Việc khởi tạo một tham số, bao gồm mọi phép tính giá trị liên quan và hiệu ứng phụ, được sắp xếp theo trình tự không xác định đối với bất kỳ tham số nào khác. [Lưu ý: Tất cả các tác dụng phụ của đánh giá đối số được sắp xếp theo trình tự trước khi hàm được nhập (xem 4.6). —Gửi ghi chú] [Ví dụ:
void f() {
std::string s = "but I have heard it works even if you don’t believe in it";
s.replace(0, 4, "").replace(s.find("even"), 4, "only").replace(s.find(" don’t"), 6, "");
assert(s == "I have heard it works only if you believe in it"); // OK
}
—Gửi ví dụ]
s.replace( s.replace( s.replace(0, 4, "" ).find( "even" ), 4, "only" ).find( " don't" ), 6, "" );