Mã này từ “Ngôn ngữ lập trình C ++” phần 36.3.6 phiên bản thứ 4 có hành vi được xác định rõ không?


94

Trong phần Ngôn ngữ lập trình C ++ của Bjarne Stroustrup phiên bản thứ 4 36.3.6 Các hoạt động giống STL, đoạn mã sau được sử dụng làm ví dụ về chuỗi :

void f2()
{
    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" ) ;
}

Xác nhận không thành công trong gcc( xem trực tiếp ) và Visual Studio( xem trực tiếp ), nhưng không thất bại khi sử dụng Clang ( xem trực tiếp ).

Tại sao tôi nhận được kết quả khác nhau? Có bất kỳ trình biên dịch nào trong số này đánh giá sai biểu thức chuỗi hay không hoặc mã này có biểu hiện một số dạng hành vi không xác định hoặc không xác định không ?


Tốt hơn:s.replace( s.replace( s.replace(0, 4, "" ).find( "even" ), 4, "only" ).find( " don't" ), 6, "" );
Ben Voigt

20
lỗi sang một bên, tôi là người duy nhất nghĩ rằng đoạn mã xấu xí như thế không nên có trong cuốn sách?
Karoly Horvath

5
@KarolyHorvath Lưu ý rằng cout << a << b << coperator<<(operator<<(operator<<(cout, a), b), c)chỉ bớt xấu đi một chút.
Oktalist

1
@Oktalist: :) ít nhất tôi cũng có ý định ở đó. nó dạy cách tra cứu tên phụ thuộc vào đối số và cú pháp toán tử cùng một lúc ở định dạng ngắn gọn ... và nó không tạo ấn tượng rằng bạn thực sự nên viết mã như vậy.
Karoly Horvath

Câu trả lời:


104

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 replacephả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 findcuộ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ụ stheo 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 findcuộ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ế đó 47có 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 47đối với với Bgiải thích sự khác biệt trong kết quả giữa clanggcckhi đánh giá f2(). Trong các bài kiểm tra của tôi clangđánh giá Btrước khi đánh giá 47trong 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ố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ụ]


7
Tôi hơi ngạc nhiên khi thấy rằng "nhiều chuyên gia" đã bỏ qua vấn đề này, ai cũng biết rằng việc đánh giá biểu thức hậu tố của một lệnh gọi hàm không được giải trình tự trước khi đánh giá các đối số (trong tất cả các phiên bản của C và C ++).
MM

@ShafikYaghmour Các lệnh gọi hàm được sắp xếp theo trình tự không xác định đối với nhau và mọi thứ khác, ngoại trừ các mối quan hệ được sắp xếp theo trình tự trước mà bạn đã lưu ý. Tuy nhiên, đánh giá của 1, 2, 3, 5, 6, 8, 9, "even", "don't"và trong vài trường hợp slà unsequenced tương đối với nhau.
TC

4
@TC không nó không phải là (đó là cách "lỗi" này phát sinh). Ví dụ: foo().func( bar() )nó có thể gọi foo()trước hoặc sau khi gọi bar(). Các postfix thể hiệnfoo().func. Các đối số và biểu thức hậu tố được sắp xếp theo thứ tự trước phần thân của func(), nhưng không có chuỗi liên quan với nhau.
MM

@MattMcNabb À, đúng rồi, tôi đọc nhầm. Bạn đang nói về chính biểu thức hậu tố hơn là lời gọi. Vâng, đúng vậy, chúng không có rào cản (tất nhiên là trừ khi áp dụng một số quy tắc khác).
TC

6
Ngoài ra còn có yếu tố khiến người ta có xu hướng cho rằng mã xuất hiện trong sách B.Stroustrup là đúng nếu không ai đó chắc chắn đã nhận ra! (có liên quan; người dùng SO vẫn tìm thấy những lỗi mới trong K&R)
MM

4

Điều này nhằm mục đích thêm thông tin về vấn đề liên quan đến C ++ 17. Đề xuất ( Tinh chỉnh Thứ tự Đánh giá Biểu thức cho Bản sửa đổi C ++ Idiomatic 2 ) để C++17giải quyết vấn đề trích dẫn mã ở trên là như một mẫu vật.

Như được đề xuất, tôi đã thêm thông tin có liên quan từ đề xuất và trích dẫn (đánh dấu của tôi):

Thứ tự đánh giá biểu thức, như nó hiện được quy định trong tiêu chuẩn, làm suy yếu các lời khuyên, các thành ngữ lập trình phổ biến hoặc sự an toàn tương đối của các cơ sở thư viện tiêu chuẩn. Những cái bẫy không chỉ dành cho người mới hoặc lập trình viên bất cẩn. Chúng ảnh hưởng đến tất cả chúng ta một cách bừa bãi, ngay cả khi chúng ta biết các quy tắc.

Hãy xem xét đoạn chương trình sau:

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");
}

Sự khẳng định được cho là để xác nhận kết quả dự định của lập trình viên. Nó sử dụng "chuỗi" các cuộc gọi hàm thành viên, một thông lệ tiêu chuẩn phổ biến. Mã này đã được đánh giá bởi các chuyên gia C ++ trên toàn thế giới và được xuất bản (Ngôn ngữ lập trình C ++, ấn bả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ụ.

Bài báo đề xuất thay đổi C++17quy tắc trước về thứ tự đánh giá biểu thức đã bị ảnh hưởng Cvà tồn tại trong hơn ba thập kỷ. Nó đề xuất rằng ngôn ngữ nên đảm bảo các thành ngữ đương đại hoặc có nguy cơ "bẫy và nguồn lỗi khó tìm, khó tìm" như những gì đã xảy ra với mẫu mã ở trên.

Xét đề nghị cho C++17là để yêu cầu rằng mỗi biểu hiện có một trật tự đánh giá được xác định rõ :

  • Biểu thức hậu tố được đánh giá từ trái sang phải. Điều này bao gồm các lệnh gọi hàm và biểu thức lựa chọn thành viên.
  • Biểu thức gán được đánh giá từ phải sang trái. Điều này bao gồm các bài tập ghép.
  • Toán hạng để dịch chuyển toán tử được đánh giá từ trái sang phải.
  • Thứ tự đánh giá của một biểu thức liên quan đến một toán tử được nạp chồng được xác định bởi thứ tự liên quan đến toán tử tích hợp tương ứng, không phải các quy tắc cho các lệnh gọi hàm.

Đoạn mã trên biên dịch thành công bằng cách sử dụng GCC 7.1.1Clang 4.0.0.

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.