Các đảm bảo thứ tự đánh giá được giới thiệu bởi C ++ 17 là gì?


95

Ý nghĩa của các đảm bảo thứ tự đánh giá được bình chọn trong C ++ 17 (P0145) trên mã C ++ điển hình là gì?

Nó thay đổi gì về những thứ như sau?

i = 1;
f(i++, i)

std::cout << f() << f() << f();

hoặc là

f(g(), h(), j());

Liên quan đến Thứ tự đánh giá câu lệnh gán trong C ++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? cả hai đều được bao phủ bởi giấy. Cái đầu tiên có thể đưa ra một ví dụ bổ sung tuyệt vời trong câu trả lời của bạn bên dưới.
Shafik Yaghmour

Câu trả lời:


83

Một số trường hợp phổ biến mà thứ tự đánh giá cho đến nay vẫn chưa được xác định , được chỉ định và hợp lệ với C++17. Thay vào đó, một số hành vi không xác định được chuyển thành không xác định.

i = 1;
f(i++, i)

là không xác định, nhưng bây giờ nó không được xác định. Cụ thể, những gì không được chỉ định là thứ tự mà mỗi đối số fđược đánh giá so với các đối số khác. i++có thể được đánh giá trước đó ihoặc ngược lại. Thật vậy, nó có thể đánh giá cuộc gọi thứ hai theo một thứ tự khác, mặc dù nằm trong cùng một trình biên dịch.

Tuy nhiên, việc đánh giá mỗi đối số được yêu cầu thực thi hoàn toàn, với tất cả các tác dụng phụ, trước khi thực thi bất kỳ đối số nào khác. Vì vậy, bạn có thể nhận được f(1, 1)(đối số thứ hai được đánh giá đầu tiên) hoặc f(1, 2)(đối số thứ nhất được đánh giá trước). Nhưng bạn sẽ không bao giờ có được f(2, 2)hoặc bất cứ thứ gì khác có tính chất đó.

std::cout << f() << f() << f();

là không xác định, nhưng nó sẽ trở nên tương thích với ưu tiên của toán tử để đánh giá đầu tiên của fsẽ xuất hiện đầu tiên trong luồng (ví dụ bên dưới).

f(g(), h(), j());

vẫn có thứ tự đánh giá không xác định của g, h và j. Lưu ý rằng đối với getf()(g(),h(),j()), trạng thái quy tắc getf()sẽ được đánh giá trước đó g, h, j.

Cũng lưu ý ví dụ sau từ văn bản đề xuất:

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

Ví dụ đến từ Ngôn ngữ lập trình C ++ , phiên bản thứ 4, Stroustrup, và được sử dụng là hành vi không xác định, nhưng với C ++ 17, nó sẽ hoạt động như mong đợi. Có những vấn đề tương tự với các hàm có thể tiếp tục lại ( .then( . . . )).

Như một ví dụ khác, hãy xem xét những điều sau:

#include <iostream>
#include <string>
#include <vector>
#include <cassert>

struct Speaker{
    int i =0;
    Speaker(std::vector<std::string> words) :words(words) {}
    std::vector<std::string> words;
    std::string operator()(){
        assert(words.size()>0);
        if(i==words.size()) i=0;
        // Pre-C++17 version:
        auto word = words[i] + (i+1==words.size()?"\n":",");
        ++i;
        return word;
        // Still not possible with C++17:
        // return words[i++] + (i==words.size()?"\n":",");

    }
};

int main() {
    auto spk = Speaker{{"All", "Work", "and", "no", "play"}};
    std::cout << spk() << spk() << spk() << spk() << spk() ;
}

Với C ++ 14 trở về trước, chúng tôi có thể (và sẽ) nhận được các kết quả như

play
no,and,Work,All,

thay vì

All,work,and,no,play

Lưu ý rằng những điều trên có hiệu lực giống như

(((((std::cout << spk()) << spk()) << spk()) << spk()) << spk()) ;

Tuy nhiên, trước C ++ 17, không có gì đảm bảo rằng các lệnh gọi đầu tiên sẽ xuất hiện trước trong luồng.

Tài liệu tham khảo: Từ đề xuất được chấp nhận :

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. Tóm lại, các biểu thức sau được đánh giá theo thứ tự a, sau đó b, sau đó c, sau đó d:

  1. ab
  2. a-> b
  3. a -> * b
  4. a (b1, b2, b3)
  5. b @ = a
  6. a [b]
  7. a << b
  8. a >> b

Hơn nữa, chúng tôi đề xuất quy tắc bổ sung sau: thứ tự đánh giá biểu thức liên quan đến toán tử nạp chồng được xác định bởi thứ tự được liên kết với toán tử tích hợp tương ứng, không phải quy tắc cho lời gọi hàm.

Chỉnh sửa ghi chú: Câu trả lời ban đầu của tôi bị hiểu sai a(b1, b2, b3). Trình tự b1, b2, b3vẫn còn chưa được xác định. (cảm ơn @KABoissonneault, tất cả những người bình luận.)

Tuy nhiên, (như @Yakk chỉ ra) và điều này là rất quan trọng: Ngay cả khi b1, b2, b3là những biểu hiện không tầm thường, mỗi người trong số họ hoàn toàn đánh giá và gắn liền với tham số chức năng tương ứng trước khi những người khác đang bắt đầu được đánh giá. Tiêu chuẩn nói rõ như thế này:

§5.2.2 - Lệnh gọi hàm 5.2.2.4:

. . . Biểu thức hậu tố được sắp xếp theo thứ 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. Mọi phép tính giá trị và hiệu ứng phụ liên quan đến việc khởi tạo một tham số và chính quá trình khởi tạo, đều được giải trình tự trước mọi phép tính giá trị và hiệu ứng phụ liên quan đến việc khởi tạo bất kỳ tham số tiếp theo nào.

Tuy nhiên, một trong những câu mới này bị thiếu trong bản nháp GitHub :

Mọi phép tính giá trị và hiệu ứng phụ liên quan đến việc khởi tạo một tham số và chính quá trình khởi tạo, đều được giải trình tự trước mọi phép tính giá trị và hiệu ứng phụ liên quan đến việc khởi tạo bất kỳ tham số tiếp theo nào.

Ví dụ ở đó. Nó giải quyết các vấn đề đã tồn tại hàng thập kỷ ( theo giải thích của Herb Sutter ) với sự an toàn ngoại lệ ở những nơi như

f(std::unique_ptr<A> a, std::unique_ptr<B> b);

f(get_raw_a(), get_raw_a());

sẽ bị rò rỉ nếu một trong các lệnh gọi xuất hiện get_raw_a()trước khi con trỏ thô khác được gắn với tham số con trỏ thông minh của nó.

Như đã chỉ ra bởi TC, ví dụ này có sai sót vì cấu trúc unique_ptr từ con trỏ thô là rõ ràng, ngăn điều này biên dịch. *

Cũng lưu ý câu hỏi cổ điển này (được gắn thẻ C , không phải C ++ ):

int x=0;
x++ + ++x;

vẫn chưa được xác định.


1
"Đề xuất phụ thứ hai thay thế thứ tự đánh giá của các lệnh gọi hàm như sau: hàm được đánh giá trước tất cả các đối số của nó, nhưng bất kỳ cặp đối số nào (từ danh sách đối số) đều được sắp xếp theo trình tự không xác định; có nghĩa là hàm này được đánh giá trước đối số khác nhưng thứ tự không được chỉ định; nó được đảm bảo rằng chức năng được đánh giá trước các đối số. Điều này phản ánh một đề xuất của một số thành viên của Nhóm làm việc cốt lõi. "
Yakk - Adam Nevraumont

1
Tôi nhận được ấn tượng đó từ bài báo nói rằng "các biểu thức sau được đánh giá theo thứ tự a, sau đó b, sau đó c, sau đó d" và sau đó hiển thị a(b1, b2, b3), gợi ý rằng tất cả các bbiểu thức không nhất thiết phải được đánh giá theo bất kỳ thứ tự nào (nếu không thì sẽ như vậy a(b, c, d))
KABoissonneault

1
@KABoissoneault, Bạn nói đúng và tôi đã cập nhật câu trả lời cho phù hợp. Ngoài ra, tất cả: các trích dẫn phiên bản 3, là phiên bản được bình chọn theo như tôi hiểu.
Johan Lundberg

2
@JohanLundberg Có một điều khác từ bài báo mà tôi tin là quan trọng. a(b1()(), b2()())có thể đặt hàng b1()()b2()()theo thứ tự nào, nhưng nó không thể làm b1()sau đó b2()()sau đó b1()(): nó có thể không còn xen kẽ hành của họ. Tóm lại, "8. LỆNH ĐÁNH GIÁ THAY THẾ CHO CÁC CUỘC GỌI CHỨC NĂNG" là một phần của thay đổi đã được phê duyệt.
Yakk - Adam Nevraumont

3
f(i++, i)không được xác định. Bây giờ nó không được xác định. Ví dụ chuỗi của Stroustrup có lẽ là không xác định, không phải là không xác định. `f (get_raw_a (), get_raw_a ());` sẽ không biên dịch vì hàm tạo liên quan unique_ptrlà rõ ràng. Cuối cùng, x++ + ++xlà không xác định, thời gian.
TC

44

Việc xen kẽ bị cấm trong C ++ 17

Trong C ++ 14, điều sau không an toàn:

void foo(std::unique_ptr<A>, std::unique_ptr<B>);

foo(std::unique_ptr<A>(new A), std::unique_ptr<B>(new B));

Có bốn hoạt động xảy ra ở đây trong khi gọi hàm

  1. new A
  2. unique_ptr<A> constructor
  3. new B
  4. unique_ptr<B> constructor

Thứ tự của những thứ này hoàn toàn không xác định, và vì vậy một thứ tự hoàn toàn hợp lệ là (1), (3), (2), (4). Nếu thứ tự này được chọn và (3) ném, thì bộ nhớ từ (1) bị rò rỉ - chúng tôi chưa chạy (2), điều này sẽ ngăn chặn sự rò rỉ.


Trong C ++ 17, các quy tắc mới cấm xen kẽ. Từ [intro.execution]:

Đối với mỗi lệnh gọi hàm F, đối với mọi đánh giá A xảy ra trong F và mọi đánh giá B không xảy ra trong F nhưng được đánh giá trên cùng một luồng và là một phần của cùng một trình xử lý tín hiệu (nếu có), A được sắp xếp trước B hoặc B được sắp xếp trước A.

Có một chú thích cho câu đó có nội dung:

Nói cách khác, các thực thi chức năng không xen kẽ với nhau.

Điều này khiến chúng ta có hai câu lệnh hợp lệ: (1), (2), (3), (4) hoặc (3), (4), (1), (2). Không xác định được thứ tự nào được thực hiện, nhưng cả hai đều an toàn. Tất cả các thử thách trong đó (1) (3) cả trước (2) và (4) đều bị cấm.


1
Ngoài lề một chút, nhưng đây là một trong những lý do cho boost :: make_shared và sau này là std :: make_shared (lý do khác là phân bổ ít hơn + địa phương tốt hơn). Có vẻ như động cơ ngoại lệ-an toàn / rò rỉ tài nguyên không còn được áp dụng nữa. Xem Mã Ví dụ 3, boost.org/doc/libs/1_67_0/libs/smart_ptr/doc/html/… Chỉnh sửastackoverflow.com/a/48844115 , herbutter.com/2013/05/29/gotw-89-solution- thông minh-con trỏ
Max Barraclough

3
Tôi tự hỏi làm thế nào thay đổi này ảnh hưởng đến tối ưu hóa. Trình biên dịch hiện đã giảm nghiêm trọng số lượng tùy chọn về cách kết hợp và xen kẽ các lệnh CPU liên quan đến tính toán đối số, vì vậy nó có thể dẫn đến việc sử dụng CPU kém hơn?
Violet Giraffe

2

Tôi đã tìm thấy một số lưu ý về thứ tự đánh giá biểu thức:

  • Hỏi nhanh: Tại sao c ++ không có thứ tự cụ thể để đánh giá các đối số của hàm?

    Một số thứ tự đánh giá đảm bảo xung quanh các toán tử được nạp chồng và các quy tắc đối số hoàn chỉnh được thêm vào trong C ++ 17. Nhưng nó vẫn là đối số nào đi trước vẫn chưa được xác định. Trong C ++ 17, giờ đây người ta chỉ định rằng biểu thức đưa ra lệnh gọi (mã ở bên trái (của lệnh gọi hàm) đi trước đối số và đối số nào được đánh giá trước sẽ được đánh giá đầy đủ trước đối số tiếp theo bắt đầu, và trong trường hợp của một phương thức đối tượng, giá trị của đối tượng được đánh giá trước các đối số của phương thức.

  • Thứ tự đánh giá

    21) Mọi biểu thức trong danh sách biểu thức được phân tách bằng dấu phẩy trong bộ khởi tạo có dấu ngoặc đơn được đánh giá như thể đối với một lệnh gọi hàm ( theo trình tự không xác định )

  • Biểu thức mơ hồ

    Ngôn ngữ C ++ không đảm bảo thứ tự mà các đối số của một lệnh gọi hàm được đánh giá.

Trong P0145R3, tôi đã tìm thấy Thứ tự Đánh giá Biểu thức cho Idiomatic C ++ :

Việc tính toán giá trị và tác dụng phụ liên quan của biểu thức hậu tố được sắp xếp theo trình tự trước các biểu thức trong danh sách biểu thức. Việc khởi tạo các tham số được khai báo được sắp xếp theo trình tự không xác định mà không có sự xen kẽ.

Nhưng tôi không tìm thấy nó trong tiêu chuẩn, thay vào đó trong tiêu chuẩn tôi đã tìm thấy:

6.8.1.8 Thực thi tuần tự [int.execution] Một biểu thức X được cho là được sắp xếp theo trình tự trước biểu thức Y nếu mọi phép tính giá trị và mọi hiệu ứng phụ liên quan đến biểu thức X được sắp xếp theo trình tự trước mọi phép tính giá trị và mọi hiệu ứng phụ liên quan đến biểu thức Y .

6.8.1.9 Thực thi tuần tự [int.execution] Mọi phép tính giá trị và hiệu ứng phụ được liên kết với một biểu thức đầy đủ được sắp xếp theo trình tự trước khi mọi phép tính giá trị và hiệu ứng phụ liên kết với biểu thức đầy đủ tiếp theo được đánh giá.

7.6.19.1 Toán tử dấu phẩy [expr.comma] Một cặp biểu thức được phân tách bằng dấu phẩy được đánh giá từ trái sang phải; ...

Vì vậy, tôi đã so sánh theo hành vi trong ba trình biên dịch cho tiêu chuẩn 14 và 17. Mã được khám phá là:

#include <iostream>

struct A
{
    A& addInt(int i)
    {
        std::cout << "add int: " << i << "\n";
        return *this;
    }

    A& addFloat(float i)
    {
        std::cout << "add float: " << i << "\n";
        return *this;
    }
};

int computeInt()
{
    std::cout << "compute int\n";
    return 0;
}

float computeFloat()
{
    std::cout << "compute float\n";
    return 1.0f;
}

void compute(float, int)
{
    std::cout << "compute\n";
}

int main()
{
    A a;
    a.addFloat(computeFloat()).addInt(computeInt());
    std::cout << "Function call:\n";
    compute(computeFloat(), computeInt());
}

Kết quả (càng phù hợp là tiếng kêu):

<style type="text/css">
  .tg {
    border-collapse: collapse;
    border-spacing: 0;
    border-color: #aaa;
  }
  
  .tg td {
    font-family: Arial, sans-serif;
    font-size: 14px;
    padding: 10px 5px;
    border-style: solid;
    border-width: 1px;
    overflow: hidden;
    word-break: normal;
    border-color: #aaa;
    color: #333;
    background-color: #fff;
  }
  
  .tg th {
    font-family: Arial, sans-serif;
    font-size: 14px;
    font-weight: normal;
    padding: 10px 5px;
    border-style: solid;
    border-width: 1px;
    overflow: hidden;
    word-break: normal;
    border-color: #aaa;
    color: #fff;
    background-color: #f38630;
  }
  
  .tg .tg-0pky {
    border-color: inherit;
    text-align: left;
    vertical-align: top
  }
  
  .tg .tg-fymr {
    font-weight: bold;
    border-color: inherit;
    text-align: left;
    vertical-align: top
  }
</style>
<table class="tg">
  <tr>
    <th class="tg-0pky"></th>
    <th class="tg-fymr">C++14</th>
    <th class="tg-fymr">C++17</th>
  </tr>
  <tr>
    <td class="tg-fymr"><br>gcc 9.0.1<br></td>
    <td class="tg-0pky">compute float<br>add float: 1<br>compute int<br>add int: 0<br>Function call:<br>compute int<br>compute float<br>compute</td>
    <td class="tg-0pky">compute float<br>add float: 1<br>compute int<br>add int: 0<br>Function call:<br>compute int<br>compute float<br>compute</td>
  </tr>
  <tr>
    <td class="tg-fymr">clang 9</td>
    <td class="tg-0pky">compute float<br>add float: 1<br>compute int<br>add int: 0<br>Function call:<br>compute float<br>compute int<br>compute</td>
    <td class="tg-0pky">compute float<br>add float: 1<br>compute int<br>add int: 0<br>Function call:<br>compute float<br>compute int<br>compute</td>
  </tr>
  <tr>
    <td class="tg-fymr">msvs 2017</td>
    <td class="tg-0pky">compute int<br>compute float<br>add float: 1<br>add int: 0<br>Function call:<br>compute int<br>compute float<br>compute</td>
    <td class="tg-0pky">compute float<br>add float: 1<br>compute int<br>add int: 0<br>Function call:<br>compute int<br>compute float<br>compute</td>
  </tr>
</table>

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.