Thứ tự thực thi C ++ trong chuỗi phương thức


108

Đầu ra của chương trình này:

#include <iostream> 
class c1
{   
  public:
    c1& meth1(int* ar) {
      std::cout << "method 1" << std::endl;
      *ar = 1;
      return *this;
    }
    void meth2(int ar)
    {
      std::cout << "method 2:"<< ar << std::endl;
    }
};

int main()
{
  c1 c;
  int nu = 0;
  c.meth1(&nu).meth2(nu);
}

Là:

method 1
method 2:0

Tại sao nukhông phải là 1 khi meth2()bắt đầu?


41
@MartinBonner: Mặc dù tôi biết câu trả lời, nhưng tôi sẽ không gọi nó là "hiển nhiên" theo bất kỳ nghĩa nào của từ này và, ngay cả khi có, đó sẽ không phải là một lý do chính đáng để giảm giá. Thật đáng thất vọng!
Lightness Races in Orbit

4
Đây là những gì bạn nhận được khi sửa đổi các đối số của mình. Các hàm sửa đổi đối số của chúng khó đọc hơn, tác động của chúng là bất ngờ đối với lập trình viên tiếp theo làm việc trên mã và chúng dẫn đến những bất ngờ như thế này. Tôi thực sự khuyên bạn nên tránh sửa đổi bất kỳ tham số nào ngoại trừ bất kỳ thứ gì. Việc sửa đổi invocant sẽ không thành vấn đề ở đây, bởi vì phương thức thứ hai được gọi trên kết quả của phương thức đầu tiên, vì vậy các hiệu ứng được sắp xếp theo thứ tự trên nó. Vẫn có một số trường hợp mà chúng sẽ không xảy ra.
Jan Hudec


@JanHudec Đây chính là lý do tại sao lập trình hàm lại chú trọng nhiều đến độ tinh khiết của hàm.
Pharap

2
Ví dụ, một quy ước gọi ngăn xếp dựa trên có lẽ sẽ thích push nu, &nucvào ngăn xếp theo thứ tự, sau đó invoke meth1, đẩy kết quả vào ngăn xếp, sau đó invoke meth2, trong khi một quy ước đăng ký dựa trên gọi muốn nạp c&nuvào thanh ghi, gọi meth1, nạp nuvào thanh ghi, sau đó gọi meth2.
Neil

Câu trả lời:


66

Bởi vì thứ tự đánh giá là không xác định.

Bạn đang thấy nutrong mainviệc được đánh giá 0trước khi thậm chí meth1được gọi. Đây là vấn đề với chuỗi. Tôi khuyên không nên làm điều đó.

Chỉ cần tạo một chương trình đẹp, đơn giản, rõ ràng, dễ đọc, dễ hiểu:

int main()
{
  c1 c;
  int nu = 0;
  c.meth1(&nu);
  c.meth2(nu);
}

14
Có khả năng, một đề xuất để làm rõ thứ tự đánh giá trong một số trường hợp , để khắc phục sự cố này, sẽ được thông qua cho C ++ 17
Revolver_Ocelot

7
Tôi thích chuỗi phương thức (ví dụ: <<đối với đầu ra và "trình tạo đối tượng" cho các đối tượng phức tạp có quá nhiều đối số với các hàm tạo - nhưng nó kết hợp thực sự tồi tệ với các đối số đầu ra.
Martin Bonner hỗ trợ Monica

34
Tôi hiểu điều này đúng không? thứ tự đánh giá của meth1meth2được xác định, nhưng đánh giá tham số cho meth2có thể xảy ra trước đó meth1được gọi là ...?
Roddy

7
Chuỗi phương thức là tốt miễn là các phương thức hợp lý và chỉ sửa đổi cái gọi là bất kỳ (mà các hiệu ứng được sắp xếp tốt, vì phương thức thứ hai được gọi dựa trên kết quả của phương thức đầu tiên).
Jan Hudec

4
Nó là hợp lý, khi bạn nghĩ về nó. Nó hoạt động giống nhưmeth2(meth1(c, &nu), nu)
BartekChom

29

Tôi nghĩ rằng phần này của tiêu chuẩn dự thảo về trình tự đánh giá là có liên quan:

1.9 Thực thi chương trình

...

  1. 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. Các phép tính giá trị của các toán hạng của một toán tử được sắp xếp theo trình tự trước khi tính toán giá trị của kết quả của toán tử. Nếu một hiệu ứng phụ trên một đối tượng vô hướng là không có hàng rào so với 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 và chúng không có khả năng xảy ra đồng thời, thì hành vi đó là không xác định

và cả:

5.2.2 Lệnh gọi hàm

...

  1. [Lưu ý: Các đánh giá của biểu thức hậu tố và của các đối số là tất cả các đánh giá có liên quan đến nhau. Tất cả các tác dụng phụ của đánh giá đối số được giải trình tự trước khi hàm được nhập - ghi chú cuối]

Vì vậy, đối với dòng của bạn c.meth1(&nu).meth2(nu);, hãy xem xét điều gì đang xảy ra trong toán tử về mặt toán tử gọi hàm cho lần gọi cuối cùng meth2, vì vậy chúng tôi thấy rõ ràng phân tích thành biểu thức hậu tố và đối số nu:

operator()(c.meth1(&nu).meth2, nu);

Các đánh giá của biểu thức hậu tố và đối số cho lời gọi hàm cuối cùng (tức là biểu thức hậu tố c.meth1(&nu).meth2nu) là không có câu trả lời liên quan đến nhau theo quy tắc gọi hàm ở trên. Do đó, tác dụng phụ của việc tính toán biểu thức hậu tố trên đối tượng vô hướng là vô hướng arliên quan đến đánh giá đối số nutrước khi meth2gọi hàm. Theo quy tắc thực thi chương trình ở trên, đây là hành vi không xác định.

Nói cách khác, không có yêu cầu nào đối với trình biên dịch để đánh giá nuđối số cho lệnh meth2gọi sau lệnh meth1gọi - có thể tự do giả định rằng không có tác dụng phụ nào meth1ảnh hưởng đến việc nuđánh giá.

Mã hợp ngữ được tạo ra ở trên chứa trình tự sau trong mainhàm:

  1. Biến nuđược cấp phát trên ngăn xếp và được khởi tạo bằng 0.
  2. Một sổ đăng ký ( ebxtrong trường hợp của tôi) nhận được một bản sao giá trị củanu
  3. Địa chỉ của nucđược tải vào thanh ghi tham số
  4. meth1 được gọi là
  5. Thanh ghi giá trị trả về và các giá trị được lưu trữ trước đó của nutrong ebxsổ đăng ký được nạp vào thanh ghi thông số
  6. meth2 được gọi là

Đặc biệt, trong bước 5 ở trên, trình biên dịch cho phép nusử dụng lại giá trị được lưu trong bộ nhớ cache của bước 2 trong lệnh gọi hàm tới meth2. Ở đây, nó không tính đến khả năng nucó thể đã bị thay đổi bởi lệnh gọi meth1- 'hành vi không xác định' đang hoạt động.

LƯU Ý: Câu trả lời này đã thay đổi về chất so với dạng ban đầu. Lời giải thích ban đầu của tôi về tác dụng phụ của việc tính toán toán hạng không được giải trình tự trước khi gọi hàm cuối cùng là không chính xác, bởi vì chúng đúng như vậy. Vấn đề là thực tế là việc tính toán các toán hạng được giải trình tự không xác định.


2
Cái này sai. Các lệnh gọi hàm được sắp xếp theo trình tự không xác định với các đánh giá khác trong hàm đang gọi (trừ khi áp đặt ràng buộc trước theo trình tự); chúng không xen kẽ.
TC

1
@TC - Tôi chưa bao giờ nói gì về việc các lệnh gọi hàm được xen kẽ. Tôi chỉ đề cập đến tác dụng phụ của các toán tử. Nếu bạn nhìn vào mã lắp ráp được tạo bởi phần trên, bạn sẽ thấy nó meth1được thực thi trước đó meth2, nhưng tham số for meth2là một giá trị được nulưu trong bộ nhớ cache vào một thanh ghi trước khi gọi đến meth1- tức là trình biên dịch đã bỏ qua các tác dụng phụ tiềm ẩn, đó là phù hợp với câu trả lời của tôi.
Smeeheey

1
Bạn đang tuyên bố chính xác rằng - "tác dụng phụ của nó (tức là đặt giá trị của ar) không được đảm bảo sẽ được giải trình tự trước lệnh gọi". Việc đánh giá biểu thức hậu tố trong một lệnh gọi hàm (là c.meth1(&nu).meth2) và đánh giá đối số cho lệnh gọi đó ( nu) nói chung là không có kết quả, nhưng 1) các tác dụng phụ của chúng đều được giải trình tự trước khi nhập meth2và 2) vì c.meth1(&nu)là một lệnh gọi hàm , nó được sắp xếp theo trình tự không xác định với việc đánh giá nu. Bên trong meth2, nếu bằng cách nào đó nó thu được một con trỏ đến biến trong main, nó sẽ luôn nhìn thấy 1.
TC

2
"Tuy nhiên, tác dụng phụ của việc tính toán các toán hạng (tức là đặt giá trị của ar) không được đảm bảo sẽ được sắp xếp theo trình tự trước bất kỳ thứ gì (như phần 2 ở trên)." Nó hoàn toàn đảm bảo được sắp xếp theo trình tự trước khi cuộc gọi đến meth2, như đã lưu ý trong mục 3 của trang hội nghị mà bạn đang trích dẫn (mà bạn cũng đã quên trích dẫn đúng cách).
TC

1
Bạn đã làm sai điều gì đó và làm cho nó tồi tệ hơn. Hoàn toàn không có hành vi không xác định ở đây. Tiếp tục đọc [intro.execution] / 15, qua ví dụ.
TC

9

Trong tiêu chuẩn C ++ 1998, Phần 5, đoạn 4

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ị 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.

(Tôi đã bỏ qua một tham chiếu đến chú thích số 53 không liên quan đến câu hỏi này).

Về cơ bản, &nuphải được đánh giá trước khi gọi c1::meth1(), và nuphải được đánh giá trước khi gọi c1::meth2(). Tuy nhiên, không có yêu cầu nào nuđược đánh giá trước &nu(ví dụ: nó được phép nuđánh giá trước, sau đó &nuvà sau đó c1::meth1()được gọi - đó có thể là những gì trình biên dịch của bạn đang làm). Do đó, biểu thức *ar = 1in c1::meth1()không được đảm bảo sẽ được đánh giá trước khi nuin main()được đánh giá, để được chuyển đến c1::meth2().

Các tiêu chuẩn C ++ sau này (mà tôi hiện không có trên PC mà tôi đang sử dụng tối nay) về cơ bản có cùng một mệnh đề.


7

Tôi nghĩ rằng khi biên dịch, trước khi các funtions meth1 và meth2 thực sự được gọi, các paramaters đã được chuyển cho chúng. Ý tôi là khi bạn sử dụng "c.meth1 (& nu) .meth2 (nu);" giá trị nu = 0 đã được chuyển cho meth2, vì vậy không có vấn đề gì sau này "nu" được thay đổi.

bạn có thể thử điều này:

#include <iostream> 
class c1
{
public:
    c1& meth1(int* ar) {
        std::cout << "method 1" << std::endl;
        *ar = 1;
        return *this;
    }
    void meth2(int* ar)
    {
        std::cout << "method 2:" << *ar << std::endl;
    }
};

int main()
{
    c1 c;
    int nu = 0;
    c.meth1(&nu).meth2(&nu);
    getchar();
}

nó sẽ nhận được câu trả lời bạn muốn

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.