Apa jaminan urutan evaluasi yang diperkenalkan oleh C ++ 17?

95

Apa implikasi dari pilihan dalam jaminan urutan evaluasi C ++ 17 (P0145) pada kode C ++ khas?

Apa yang berubah tentang hal-hal seperti berikut ini?

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

dan

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

atau

f(g(), h(), j());
Johan Lundberg
sumber
Terkait dengan Urutan evaluasi pernyataan penugasan di C ++ dan Apakah kode dari "The C ++ Programming Language" edisi ke-4 bagian 36.3.6 ini memiliki perilaku yang terdefinisi dengan baik? yang keduanya tertutup oleh kertas. Yang pertama mungkin bisa menjadi contoh tambahan yang bagus dalam jawaban Anda di bawah ini.
Shafik Yaghmour

Jawaban:

83

Beberapa kasus umum di mana urutan evaluasi sejauh ini belum ditentukan , ditetapkan dan valid dengan C++17. Beberapa perilaku tidak terdefinisi kini malah tidak ditentukan.

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

tidak ditentukan, namun sekarang tidak ditentukan. Secara khusus, yang tidak ditentukan adalah urutan di mana setiap argumen fdievaluasi relatif terhadap yang lain. i++mungkin dievaluasi sebelumnya i, atau sebaliknya. Memang, itu mungkin mengevaluasi panggilan kedua dalam urutan yang berbeda, meskipun berada di bawah compiler yang sama.

Namun, evaluasi setiap argumen diperlukan untuk mengeksekusi secara lengkap, dengan semua efek samping, sebelum eksekusi argumen lainnya. Jadi Anda mungkin mendapatkan f(1, 1)(argumen kedua dievaluasi terlebih dahulu) atau f(1, 2)(argumen pertama dievaluasi terlebih dahulu). Tapi Anda tidak akan pernah mendapatkan f(2, 2)atau apapun dari sifat itu.

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

tidak ditentukan, tetapi akan kompatibel dengan prioritas operator sehingga evaluasi pertama fakan didahulukan dalam aliran (contoh di bawah).

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

masih memiliki urutan evaluasi yang tidak ditentukan dari g, h, dan j. Perhatikan bahwa untuk getf()(g(),h(),j()), aturan state yang getf()akan dievaluasi sebelumnya g, h, j.

Perhatikan juga contoh berikut dari teks proposal:

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

Contoh ini berasal dari The C ++ Programming Language , edisi ke-4, Stroustrup, dan digunakan untuk perilaku yang tidak ditentukan, tetapi dengan C ++ 17 ini akan berfungsi seperti yang diharapkan. Ada masalah serupa dengan fungsi yang dapat dilanjutkan ( .then( . . . )).

Sebagai contoh lain, pertimbangkan hal berikut:

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

Dengan C ++ 14 dan sebelumnya kita mungkin (dan akan) mendapatkan hasil seperti

play
no,and,Work,All,

dari pada

All,work,and,no,play

Perhatikan bahwa efek di atas sama seperti

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

Tapi tetap saja, sebelum C ++ 17 tidak ada jaminan bahwa panggilan pertama akan datang lebih dulu ke streaming.

Referensi: Dari proposal yang diterima :

Ekspresi postfix dievaluasi dari kiri ke kanan. Ini termasuk panggilan fungsi dan ekspresi pemilihan anggota.

Ekspresi tugas dievaluasi dari kanan ke kiri. Ini termasuk tugas gabungan.

Operator untuk operator shift dievaluasi dari kiri ke kanan. Singkatnya, ekspresi berikut dievaluasi dalam urutan a, lalu b, lalu c, lalu 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

Lebih lanjut, kami menyarankan aturan tambahan berikut: urutan evaluasi ekspresi yang melibatkan operator yang kelebihan beban ditentukan oleh urutan yang terkait dengan operator bawaan terkait, bukan aturan untuk pemanggilan fungsi.

Edit catatan: Jawaban asli saya salah ditafsirkan a(b1, b2, b3). Urutan b1, b2, b3masih tidak ditentukan. (terima kasih @KABoissonault, semua pemberi komentar.)

Namun, (sebagai @Yakk menunjukkan) dan ini penting: Bahkan ketika b1, b2, b3adalah ekspresi non-sepele, masing-masing dari mereka benar-benar dievaluasi dan diikat ke masing-masing parameter fungsi sebelum yang lain mulai dievaluasi. Standar menyatakan ini seperti ini:

§5.2.2 - Panggilan fungsi 5.2.2.4:

. . . Ekspresi-postfix diurutkan sebelum setiap ekspresi dalam daftar ekspresi dan argumen default apa pun. Setiap penghitungan nilai dan efek samping yang terkait dengan inisialisasi parameter, dan inisialisasi itu sendiri, diurutkan sebelum setiap penghitungan nilai dan efek samping yang terkait dengan inisialisasi parameter berikutnya.

Namun, salah satu kalimat baru ini hilang dari draf GitHub :

Setiap penghitungan nilai dan efek samping yang terkait dengan inisialisasi parameter, dan inisialisasi itu sendiri, diurutkan sebelum setiap penghitungan nilai dan efek samping yang terkait dengan inisialisasi parameter berikutnya.

Contoh adalah ada. Ini memecahkan masalah yang sudah berusia puluhan tahun ( seperti yang dijelaskan oleh Herb Sutter ) dengan pengecualian keamanan di mana hal-hal seperti itu

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

f(get_raw_a(), get_raw_a());

akan bocor jika salah satu panggilan get_raw_a()akan terlempar sebelum pointer mentah lainnya diikat ke parameter smart pointer-nya.

Seperti yang ditunjukkan oleh TC, contohnya cacat karena konstruksi unique_ptr dari pointer mentah bersifat eksplisit, mencegah ini dari kompilasi. *

Catat juga pertanyaan klasik ini (diberi tag C , bukan C ++ ):

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

masih belum ditentukan.

Johan Lundberg
sumber
1
"Proposal anak kedua menggantikan urutan evaluasi pemanggilan fungsi sebagai berikut: fungsi dievaluasi sebelum semua argumennya, tetapi pasangan argumen apa pun (dari daftar argumen) diurutkan secara tidak pasti; artinya salah satu dievaluasi sebelum yang lain tetapi urutan tidak ditentukan; dijamin bahwa fungsi tersebut dievaluasi sebelum argumen. Ini mencerminkan saran yang dibuat oleh beberapa anggota Kelompok Kerja Inti. "
Yakk - Adam Nevraumont
1
Saya mendapatkan kesan dari makalah yang mengatakan bahwa "ekspresi berikut dievaluasi dalam urutan a, lalu b, kemudian c, kemudian d" dan kemudian ditampilkan a(b1, b2, b3), menunjukkan bahwa semua bekspresi tidak perlu dievaluasi dalam urutan apa pun (jika tidak, akan demikian a(b, c, d))
KABoissonneault
1
@KABoissault, Anda benar dan saya telah memperbarui jawabannya sesuai dengan itu. Juga, semua: tanda kutip adalah dari versi 3, yang merupakan versi yang dipilih sejauh yang saya mengerti.
Johan Lundberg
2
@JohanLundberg Ada hal lain dari makalah yang saya yakini penting. a(b1()(), b2()())dapat memesan b1()()dan b2()()dalam urutan apapun, tetapi tidak dapat melakukan b1()maka b2()()kemudian b1()(): hal itu mungkin tidak lagi interleave eksekusi mereka. Singkatnya, "8. ALTERNATE EVALUATION ORDER FOR FUNCTION CALLS" adalah bagian dari perubahan yang disetujui.
Yakk - Adam Nevraumont
3
f(i++, i)tidak ditentukan. Sekarang tidak ditentukan. Contoh string Stroustrup mungkin tidak ditentukan, bukan tidak ditentukan. `f (get_raw_a (), get_raw_a ());` tidak akan dikompilasi karena unique_ptrkonstruktor yang relevan eksplisit. Akhirnya, x++ + ++xtidak ditentukan, titik.
TC
44

Interleaving dilarang di C ++ 17

Di C ++ 14, berikut ini tidak aman:

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

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

Ada empat operasi yang terjadi di sini selama pemanggilan fungsi

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

Urutan ini sama sekali tidak ditentukan, dan urutan yang benar-benar valid adalah (1), (3), (2), (4). Jika urutan ini dipilih dan (3) terlempar, maka memori dari (1) bocor - kami belum menjalankan (2), yang akan mencegah kebocoran.


Di C ++ 17, aturan baru melarang interleaving. Dari [intro.execution]:

Untuk setiap pemanggilan fungsi F, untuk setiap evaluasi A yang terjadi dalam F dan setiap evaluasi B yang tidak terjadi dalam F tetapi dievaluasi pada utas yang sama dan sebagai bagian dari penangan sinyal yang sama (jika ada), baik A diurutkan sebelum B atau B diurutkan sebelum A.

Ada catatan kaki untuk kalimat itu yang berbunyi:

Dengan kata lain, eksekusi fungsi tidak saling terkait satu sama lain.

Ini membuat kita memiliki dua urutan yang valid: (1), (2), (3), (4) atau (3), (4), (1), (2). Tidak ditentukan pemesanan mana yang dilakukan, tetapi keduanya aman. Semua urutan di mana (1) (3) keduanya terjadi sebelumnya (2) dan (4) sekarang dilarang.

Barry
sumber
1
Sedikit dikesampingkan, tapi ini adalah salah satu alasan untuk boost :: make_shared, dan kemudian std :: make_shared (alasan lain adalah alokasi yang lebih sedikit + lokalitas yang lebih baik). Sepertinya motivasi pengecualian-keamanan / kebocoran sumber daya tidak lagi berlaku. Lihat Contoh Kode 3, boost.org/doc/libs/1_67_0/libs/smart_ptr/doc/html/… Edit dan stackoverflow.com/a/48844115 , herbutter.com/2013/05/29/gotw-89-solution- smart-pointers
Max Barraclough
3
Saya bertanya-tanya bagaimana perubahan ini memengaruhi pengoptimalan. Kompilator sekarang telah sangat mengurangi jumlah opsi tentang cara menggabungkan dan menyisipkan instruksi CPU yang terkait dengan komputasi argumen, sehingga dapat menyebabkan penggunaan CPU yang lebih buruk?
Violet Giraffe
2

Saya telah menemukan beberapa catatan tentang urutan evaluasi ekspresi:

  • Quick Q: Mengapa c ++ tidak memiliki urutan yang ditentukan untuk mengevaluasi argumen fungsi?

    Beberapa urutan evaluasi menjamin sekitar operator yang kelebihan beban dan aturan argumen lengkap jika ditambahkan dalam C ++ 17. Tapi tetap saja argumen mana yang lebih dulu dibiarkan tidak ditentukan. Dalam C ++ 17, sekarang ditentukan bahwa ekspresi yang memberikan apa yang harus dipanggil (kode di sebelah kiri (dari pemanggilan fungsi) diletakkan sebelum argumen, dan argumen mana pun yang dievaluasi terlebih dahulu dievaluasi sepenuhnya sebelum yang berikutnya adalah dimulai, dan dalam kasus metode objek, nilai objek dievaluasi sebelum argumen ke metode tersebut.

  • Urutan evaluasi

    21) Setiap ekspresi dalam daftar ekspresi yang dipisahkan koma dalam penginisialisasi dalam tanda kurung dievaluasi seolah-olah untuk panggilan fungsi ( diurutkan secara tidak pasti )

  • Ekspresi ambigu

    Bahasa C ++ tidak menjamin urutan argumen ke pemanggilan fungsi dievaluasi.

Di P0145R3. Mendefinisikan Urutan Evaluasi Ekspresi untuk C ++ Idiomatik saya telah menemukan:

Perhitungan nilai dan efek samping terkait dari ekspresi postfix diurutkan sebelum ekspresi dalam daftar ekspresi. Inisialisasi dari parameter yang dideklarasikan adalah tidak pasti diurutkan tanpa interleaving.

Tetapi saya tidak menemukannya dalam standar, sebaliknya dalam standar yang saya temukan:

6.8.1.8 Eksekusi berurutan [intro.execution] Ekspresi X dikatakan diurutkan sebelum ekspresi Y jika setiap perhitungan nilai dan setiap efek samping yang terkait dengan ekspresi X diurutkan sebelum setiap komputasi nilai dan setiap efek samping yang terkait dengan ekspresi Y .

6.8.1.9 Eksekusi berurutan [intro.execution] Setiap penghitungan nilai dan efek samping yang terkait dengan ekspresi penuh diurutkan sebelum setiap penghitungan nilai dan efek samping yang terkait dengan ekspresi penuh berikutnya yang akan dievaluasi.

7.6.19.1 Operator koma [expr.comma] Sepasang ekspresi yang dipisahkan oleh koma dievaluasi dari kiri-ke-kanan; ...

Jadi, saya membandingkan perilaku yang sesuai di tiga kompiler untuk 14 dan 17 standar. Kode yang dieksplorasi adalah:

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

Hasil (yang lebih konsisten adalah dentang):

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

lvccgd
sumber