Ý 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)
và
std::cout << f() << f() << f();
hoặc là
f(g(), h(), j());
Ý 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)
và
std::cout << f() << f() << f();
hoặc là
f(g(), h(), j());
Câu trả lời:
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 đó i
hoặ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 f
sẽ 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:
- ab
- a-> b
- a -> * b
- a (b1, b2, b3)
- b @ = a
- a [b]
- a << b
- 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
, b3
vẫ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
, b3
là 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ụ là ở đó. 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.
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 b
biể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)
)
a(b1()(), b2()())
có thể đặt hàng b1()()
và 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.
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_ptr
là rõ ràng. Cuối cùng, x++ + ++x
là không xác định, thời gian.
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
new A
unique_ptr<A>
constructornew B
unique_ptr<B>
constructorThứ 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.
Tôi đã tìm thấy một số lưu ý về thứ tự đánh giá biểu thức:
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.
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 )
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>