Urutan eksekusi C ++ dalam rangkaian metode

108

Output dari program ini:

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

Adalah:

method 1
method 2:0

Mengapa nubukan 1 saat meth2()dimulai?

Moises Viñas
sumber
41
@MartinBonner: Meskipun saya tahu jawabannya, saya tidak akan menyebutnya "jelas" di setiap arti kata dan, bahkan jika itu, yang tidak akan menjadi alasan yang layak untuk drive-by downvote. Mengecewakan!
Balapan Lightness di Orbit
4
Inilah yang Anda dapatkan saat Anda mengubah argumen Anda. Fungsi yang mengubah argumennya lebih sulit dibaca, efeknya tidak terduga bagi programmer berikutnya untuk mengerjakan kode dan menyebabkan kejutan seperti ini. Saya sangat menyarankan untuk menghindari modifikasi parameter apa pun kecuali invocant. Memodifikasi invocant tidak akan menjadi masalah di sini, karena metode kedua dipanggil pada hasil dari yang pertama, jadi efeknya diurutkan. Masih ada beberapa kasus di mana mereka tidak akan melakukannya.
Jan Hudec
@JanHudec Inilah tepatnya mengapa pemrograman fungsional sangat menekankan pada kemurnian fungsi.
Pharap
2
Sebagai contoh, konvensi pemanggilan berbasis stack mungkin lebih memilih untuk mendorong nu,, &nudan cke tumpukan dalam urutan itu, kemudian memanggil meth1, mendorong hasil ke stack, kemudian memanggil meth2, sementara konvensi pemanggilan berbasis register ingin memuat cdan &nuke register, memanggil meth1, memuat nuke register, lalu memanggil meth2.
Neil

Jawaban:

66

Karena urutan evaluasi tidak ditentukan.

Anda melihat nudi mainsedang dievaluasi untuk 0bahkan sebelum meth1disebut. Ini adalah masalah dengan rantai. Saya menyarankan untuk tidak melakukannya.

Buat saja program yang bagus, sederhana, jelas, mudah dibaca, dan mudah dipahami:

int main()
{
  c1 c;
  int nu = 0;
  c.meth1(&nu);
  c.meth2(nu);
}
Balapan Ringan dalam Orbit
sumber
14
Ada kemungkinan, bahwa proposal untuk mengklarifikasi urutan evaluasi dalam beberapa kasus , yang memperbaiki masalah ini, akan masuk untuk C ++ 17
Revolver_Ocelot
7
Saya suka metode perangkaian (misalnya <<untuk keluaran, dan "pembangun objek" untuk objek kompleks dengan terlalu banyak argumen ke konstruktor - tetapi sangat buruk bercampur dengan argumen keluaran.
Martin Bonner mendukung Monica
34
Apakah saya memahami hak ini? Urutan evaluasi meth1dan meth2ditentukan, tetapi evaluasi parameter untuk meth2mungkin terjadi sebelum meth1disebut ...?
Roddy
7
Perangkaian metode baik-baik saja selama metode tersebut masuk akal dan hanya memodifikasi invocant (yang efeknya diurutkan dengan baik, karena metode kedua dipanggil pada hasil yang pertama).
Jan Hudec
4
Itu logis, jika Anda memikirkannya. Ia bekerja sepertimeth2(meth1(c, &nu), nu)
BartekChom
29

Menurut saya, bagian dari draf standar mengenai urutan evaluasi ini relevan:

1.9 Eksekusi Program

...

  1. Kecuali jika disebutkan, evaluasi operan operator individu dan subekspresi ekspresi individu tidak diurutkan. Perhitungan nilai operan operator diurutkan sebelum perhitungan nilai hasil operator. Jika efek samping pada objek skalar tidak diurutkan secara relatif terhadap efek samping lain pada objek skalar yang sama atau penghitungan nilai menggunakan nilai objek skalar yang sama, dan keduanya tidak berpotensi bersamaan, perilakunya tidak ditentukan

dan juga:

5.2.2 Panggilan fungsi

...

  1. [Catatan: Evaluasi ekspresi postfix dan argumen semuanya tidak diurutkan relatif satu sama lain. Semua efek samping evaluasi argumen diurutkan sebelum fungsi dimasukkan - catatan akhir]

Jadi untuk baris Anda c.meth1(&nu).meth2(nu);, pertimbangkan apa yang terjadi di operator dalam hal operator pemanggilan fungsi untuk panggilan terakhir ke meth2, jadi kita dengan jelas melihat perincian ke dalam ekspresi dan argumen postfix nu:

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

The evaluasi dari ekspresi postfix dan argumen untuk final panggilan fungsi (yaitu ekspresi postfix c.meth1(&nu).meth2dan nu) yang relatif unsequenced satu sama lain sesuai dengan fungsi panggilan aturan di atas. Oleh karena itu, efek samping dari penghitungan ekspresi postfix pada objek skalar artidak diurutkan secara relatif terhadap evaluasi argumen nusebelum meth2pemanggilan fungsi. Dengan aturan eksekusi program di atas, ini adalah perilaku yang tidak ditentukan.

Dengan kata lain, tidak ada persyaratan bagi kompilator untuk mengevaluasi nuargumen meth2panggilan setelah meth1panggilan - itu bebas untuk mengasumsikan tidak ada efek samping dari meth1mempengaruhi nuevaluasi.

Kode assembly yang dihasilkan di atas berisi urutan mainfungsi berikut:

  1. Variabel nudialokasikan pada stack dan diinisialisasi dengan 0.
  2. Register ( ebxdalam kasus saya) menerima salinan nilainu
  3. Alamat nudan cdimuat ke register parameter
  4. meth1 disebut
  5. Nilai kembali mendaftar dan nilai yang sebelumnya cache dari nudalam ebxregister dimuat ke register parameter
  6. meth2 disebut

Secara kritis, pada langkah 5 di atas compiler memungkinkan nilai yang di-cache nudari langkah 2 untuk digunakan kembali dalam pemanggilan fungsi ke meth2. Di sini mengabaikan kemungkinan yang numungkin telah diubah oleh ajakan untuk meth1- 'perilaku yang tidak ditentukan' dalam tindakan.

CATATAN: Jawaban ini telah berubah substansi dari bentuk aslinya. Penjelasan awal saya dalam hal efek samping komputasi operan tidak diurutkan sebelum pemanggilan fungsi terakhir tidak benar, karena memang demikian. Masalahnya adalah fakta bahwa penghitungan operan itu sendiri diurutkan secara tidak pasti.

Smeeheey
sumber
2
Ini salah. Pemanggilan fungsi diurutkan secara tidak pasti dengan evaluasi lain dalam fungsi pemanggilan (kecuali batasan yang diurutkan sebelum diberlakukan); mereka tidak menyela.
TC
1
@TC - Saya tidak pernah mengatakan apa pun tentang panggilan fungsi yang disisipkan. Saya hanya merujuk pada efek samping dari operator. Jika Anda melihat kode assembly yang dihasilkan di atas, Anda akan melihat bahwa kode meth1tersebut dijalankan sebelumnya meth2, tetapi parameter untuk meth2adalah nilai yang di- nucache ke dalam register sebelum panggilan ke meth1- yaitu kompilator telah mengabaikan potensi efek sampingnya, yaitu konsisten dengan jawaban saya.
Smeeheey
1
Anda benar-benar mengklaim bahwa - "efek sampingnya (yaitu menyetel nilai ar) tidak dijamin akan diurutkan sebelum panggilan". Evaluasi ekspresi postfix dalam panggilan fungsi (yaitu c.meth1(&nu).meth2) dan evaluasi argumen ke panggilan tersebut ( nu) umumnya tidak diurutkan, tetapi 1) efek sampingnya semua diurutkan sebelum masuk ke meth2dan 2) karena c.meth1(&nu)adalah panggilan fungsi , itu diurutkan secara tidak pasti dengan evaluasi nu. Di dalam meth2, jika entah bagaimana memperoleh pointer ke variabel di main, itu akan selalu melihat 1.
TC
2
"Namun, efek samping dari penghitungan operan (yaitu menyetel nilai ar) tidak dijamin akan diurutkan sebelum apa pun (sesuai 2) di atas)." Ini benar-benar dijamin untuk diurutkan sebelum panggilan ke meth2, seperti yang disebutkan dalam item 3 dari halaman cppreference yang Anda kutip (yang juga Anda lalai untuk mengutip dengan benar).
TC
1
Anda mengambil sesuatu yang salah, dan memperburuknya. Sama sekali tidak ada perilaku yang tidak terdefinisi di sini. Teruslah membaca [intro.execution] / 15, melewati contoh.
TC
9

Dalam standar 1998 C ++, Bagian 5, paragraf 4

Kecuali jika disebutkan, urutan evaluasi operand dari masing-masing operator dan subekspresi dari ekspresi individu, dan urutan efek samping yang terjadi, tidak ditentukan. Antara titik urutan sebelumnya dan berikutnya, nilai simpanan objek skalar harus dimodifikasi paling banyak sekali dengan evaluasi ekspresi. Lebih lanjut, nilai sebelumnya harus diakses hanya untuk menentukan nilai yang akan disimpan. Persyaratan paragraf ini harus dipenuhi untuk setiap urutan subekspresi lengkap yang diizinkan; jika tidak, perilakunya tidak terdefinisi.

(Saya telah menghilangkan referensi ke catatan kaki # 53 yang tidak relevan dengan pertanyaan ini).

Pada dasarnya, &nuharus dievaluasi sebelum menelepon c1::meth1(), dan nuharus dievaluasi sebelum menelepon c1::meth2(). Namun, tidak ada persyaratan yang nudievaluasi sebelumnya &nu(misalnya, diizinkan nuuntuk dievaluasi terlebih dahulu, kemudian &nu, dan kemudian c1::meth1()dipanggil - yang mungkin dilakukan oleh compiler Anda). Oleh karena itu, ekspresi *ar = 1dalam c1::meth1()tidak dijamin akan dievaluasi sebelum nuin main()dievaluasi, untuk diteruskan ke c1::meth2().

Standar C ++ kemudian (yang saat ini tidak saya miliki di PC yang saya gunakan malam ini) pada dasarnya memiliki klausa yang sama.

Peter
sumber
7

Saya pikir ketika menyusun, sebelum fungsi meth1 dan meth2 benar-benar dipanggil, parameter telah diteruskan ke mereka. Maksud saya saat Anda menggunakan "c.meth1 (& nu) .meth2 (nu);" nilai nu = 0 telah diteruskan ke meth2, jadi tidak masalah apakah "nu" diubah nantinya.

Anda bisa mencoba ini:

#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();
}

itu akan mendapatkan jawaban yang Anda inginkan

Santo kaos
sumber