Siapa yang harus disalahkan untuk rentang ini berdasarkan untuk referensi sementara?


15

Kode berikut terlihat agak tidak berbahaya pada pandangan pertama. Seorang pengguna menggunakan fungsi bar()untuk berinteraksi dengan beberapa fungsi perpustakaan. (Ini mungkin telah bekerja untuk waktu yang lama sejak bar()mengembalikan referensi ke nilai non-temporer atau serupa.) Namun sekarang hanya mengembalikan contoh baru B. Blagi memiliki fungsi a()yang mengembalikan referensi ke objek dengan tipe iterateable A. Pengguna ingin menanyakan objek ini yang mengarah ke segfault karena Bobjek sementara yang dikembalikan oleh bar()dihancurkan sebelum iterasi dimulai.

Saya ragu siapa (perpustakaan atau pengguna) yang harus disalahkan untuk ini. Semua kelas yang disediakan perpustakaan terlihat bersih bagi saya dan tentu saja tidak melakukan hal yang berbeda (mengembalikan referensi ke anggota, mengembalikan instance stack, ...) daripada begitu banyak kode lain di luar sana. Pengguna tampaknya tidak melakukan sesuatu yang salah juga, ia hanya mengulangi beberapa objek tanpa melakukan apa-apa mengenai objek itu seumur hidup.

(Pertanyaan terkait mungkin: Haruskah seseorang menetapkan aturan umum bahwa kode tidak boleh "rentang-berdasarkan-untuk-iterate" atas sesuatu yang diambil oleh lebih dari satu panggilan berantai di header loop karena salah satu dari panggilan ini dapat mengembalikan menilai kembali?)

#include <algorithm>
#include <iostream>

// "Library code"
struct A
{
    A():
        v{0,1,2}
    {
        std::cout << "A()" << std::endl;
    }

    ~A()
    {
        std::cout << "~A()" << std::endl;
    }

    int * begin()
    {
        return &v[0];
    }

    int * end()
    {
        return &v[3];
    }

    int v[3];
};

struct B
{
    A m_a;

    A & a()
    {
        return m_a;
    }
};

B bar()
{
    return B();
}

// User code
int main()
{
    for( auto i : bar().a() )
    {
        std::cout << i << std::endl;
    }
}

6
Ketika Anda menemukan siapa yang harus disalahkan, apa langkah selanjutnya? Berteriak padanya?
JensG

7
Tidak, mengapa saya harus melakukannya? Saya sebenarnya lebih tertarik untuk mengetahui di mana proses pemikiran mengembangkan "program" ini gagal menghindari masalah ini di masa depan.
hllnll

Ini tidak ada hubungannya dengan nilai-nilai, atau rentang berbasis untuk loop, tetapi dengan pengguna tidak memahami objek seumur hidup dengan benar.
James

Site-comment: Ini adalah CWG 900 yang ditutup sebagai Bukan Cacat. Mungkin berita acara berisi beberapa diskusi.
dyp

7
Siapa yang harus disalahkan untuk ini? Bjarne Stroustrup dan Dennis Ritchie, pertama dan terutama.
Mason Wheeler

Jawaban:


14

Saya pikir masalah mendasar adalah kombinasi fitur bahasa (atau ketiadaan) dari C ++. Baik kode perpustakaan dan kode klien masuk akal (sebagaimana dibuktikan oleh fakta bahwa masalahnya jauh dari jelas). Jika masa pakai sementara Bitu sesuai diperpanjang (sampai akhir loop) tidak akan ada masalah.

Membuat hidup sementara cukup lama, dan tidak lagi, sangat sulit. Bahkan tidak ad-hoc "semua temporaries yang terlibat dalam pembuatan kisaran untuk berbasis rentang untuk hidup sampai akhir loop" akan tanpa efek samping. Pertimbangkan kasus B::a()mengembalikan rentang yang tidak tergantung pada Bobjek dengan nilai. Maka sementara Bbisa segera dibuang. Bahkan jika seseorang dapat secara tepat mengidentifikasi kasus-kasus di mana perpanjangan seumur hidup diperlukan, karena kasus-kasus ini tidak jelas bagi programmer, efeknya (destruktor disebut jauh kemudian) akan mengejutkan dan mungkin sumber bug yang sama halusnya.

Akan lebih baik jika hanya mendeteksi dan melarang omong kosong seperti itu, memaksa programmer untuk secara eksplisit meningkat bar()ke variabel lokal. Ini tidak mungkin di C ++ 11, dan mungkin tidak akan pernah mungkin karena membutuhkan anotasi. Rust melakukan ini, di mana tanda tangannya .a()adalah:

fn a<'x>(bar: &'x B) -> &'x A { bar.a }
// If we make it as explicit as possible, or
fn a(&self) -> &A { self.a }
// if we make it a method and rely on lifetime elision.

Berikut 'xadalah variabel atau wilayah seumur hidup, yang merupakan nama simbolis untuk periode waktu sumber daya tersedia. Terus terang, masa hidup sulit untuk dijelaskan - atau kami belum menemukan penjelasan terbaik - jadi saya akan membatasi diri pada minimum yang diperlukan untuk contoh ini dan merujuk pembaca yang cenderung ke dokumentasi resmi .

Pemeriksa pinjaman akan melihat bahwa hasil dari bar().a()kebutuhan untuk hidup selama loop berjalan. Diutarakan sebagai kendala pada seumur hidup 'x, kita menulis: 'loop <= 'x. Ini juga akan memperhatikan bahwa penerima panggilan metode bar(),, bersifat sementara. Karena itu, kedua petunjuk tersebut dikaitkan dengan masa hidup yang sama'x <= 'temp merupakan kendala lain.

Dua kendala ini saling bertentangan! Kami membutuhkan 'loop <= 'x <= 'temptetapi 'temp <= 'loop, yang menangkap masalah dengan tepat. Karena persyaratan yang bertentangan, kode kereta ditolak. Perhatikan bahwa ini adalah pemeriksaan waktu kompilasi dan kode Rust biasanya menghasilkan kode mesin yang sama dengan kode C ++ yang setara, jadi Anda tidak perlu membayar biaya waktu untuk menjalankannya.

Namun demikian ini adalah fitur besar untuk ditambahkan ke bahasa, dan hanya berfungsi jika semua kode menggunakannya. desain API juga terpengaruh (beberapa desain yang akan terlalu berbahaya di C ++ menjadi praktis, yang lain tidak dapat dibuat bermain bagus dengan masa hidup). Sayangnya, itu berarti tidak praktis untuk menambahkan C ++ (atau bahasa apa pun sebenarnya) secara surut. Singkatnya, kesalahannya adalah pada bahasa inersia yang berhasil dimiliki dan fakta bahwa Bjarne pada tahun 1983 tidak memiliki bola kristal dan pandangan jauh ke depan untuk memasukkan pelajaran dari 30 tahun terakhir penelitian dan pengalaman C ++ ;-)

Tentu saja, itu sama sekali tidak membantu dalam menghindari masalah di masa depan (kecuali jika Anda beralih ke Rust dan tidak pernah menggunakan C ++ lagi). Seseorang dapat menghindari ekspresi yang lebih lama dengan beberapa pemanggilan metode berantai (yang cukup membatasi, dan bahkan tidak memperbaiki semua masalah seumur hidup). Atau seseorang dapat mencoba mengadopsi kebijakan kepemilikan yang lebih disiplin tanpa bantuan penyusun: Dokumentasikan dengan jelas bahwa barpengembalian berdasarkan nilai dan bahwa hasil B::a()tidak boleh hidup lebih lama Bdari yang a()diminta. Ketika mengubah fungsi untuk kembali berdasarkan nilai alih-alih referensi yang berumur panjang, sadarilah bahwa ini adalah perubahan kontrak . Masih rawan kesalahan, tetapi dapat mempercepat proses mengidentifikasi penyebab ketika hal itu terjadi.


14

Bisakah kita mengatasi masalah ini menggunakan fitur C ++?

C ++ 11 telah menambahkan kualifikasi fungsi anggota, yang memungkinkan membatasi kategori nilai instance kelas (ekspresi) fungsi anggota dapat dipanggil. Sebagai contoh:

struct foo {
    void bar() & {} // lvalue-ref-qualified
};

foo& lvalue ();
foo  prvalue();

lvalue ().bar(); // OK
prvalue().bar(); // error

Saat memanggil beginfungsi anggota, kita tahu bahwa kemungkinan besar kita juga perlu memanggil endfungsi anggota (atau sesuatu seperti size, untuk mendapatkan ukuran kisaran). Ini mengharuskan kami beroperasi dengan nilai tinggi, karena kami harus mengatasinya dua kali. Karena itu Anda dapat berpendapat bahwa fungsi-fungsi anggota ini harus bernilai-ref-kualifikasi.

Namun, ini mungkin tidak menyelesaikan masalah mendasar: aliasing. Fungsi begindan endanggota alias objek, atau sumber daya yang dikelola oleh objek. Jika kita mengganti begindan enddengan satu fungsi range, kita harus menyediakan satu yang dapat dipanggil pada nilai:

struct foo {
    vector<int> arr;

    auto range() & // C++14 return type deduction for brevity
    { return std::make_pair(arr.begin(), arr.end()); }
};

for(auto const& e : foo().range()) // error

Ini mungkin kasus penggunaan yang valid, tetapi definisi di atas rangemelarangnya. Karena kami tidak dapat mengatasi sementara setelah panggilan fungsi anggota, mungkin lebih masuk akal untuk mengembalikan sebuah wadah, yaitu rentang kepemilikan:

struct foo {
    vector<int> arr;

    auto range() &
    { return std::make_pair(arr.begin(), arr.end()); }

    auto range() &&
    { return std::move(arr); }
};

for(auto const& e : foo().range()) // OK

Menerapkan ini ke kasus OP, dan sedikit ulasan kode

struct B {
    A m_a;
    A & a() { return m_a; }
};

Fungsi anggota ini mengubah kategori nilai dari ekspresi: B()adalah nilai awal, tetapi B().a()nilai rendah. Di sisi lain, B().m_aadalah nilai. Jadi mari kita mulai dengan membuat ini konsisten. Ada dua cara untuk melakukan ini:

struct B {
    A m_a;
    A &  a() &  { return m_a; }

    A && a() && { return std::move(m_a); }
    // or
    A    a() && { return std::move(m_a); }
};

Versi kedua, seperti yang disebutkan di atas, akan memperbaiki masalah di OP.

Selain itu, kami dapat membatasi Bfungsi anggota:

struct A {
    // [...]

    int * begin() & { return &v[0]; }
    int * end  () & { return &v[3]; }

    int v[3];
};

Ini tidak akan berdampak pada kode OP, karena hasil dari ekspresi setelah :dalam rentang berbasis untuk loop terikat ke variabel referensi. Dan variabel ini (sebagai ekspresi yang digunakan untuk mengakses fungsi anggota begindan end) adalah nilai.

Tentu saja, pertanyaannya adalah apakah aturan default harus "alias fungsi anggota pada nilai harus mengembalikan objek yang memiliki semua sumber dayanya, kecuali ada alasan bagus untuk tidak" . Alias ​​yang dikembalikannya dapat digunakan secara legal, tetapi berbahaya seperti yang Anda alami: alias tidak dapat memperpanjang masa hidup "induk" sementara:

// using the OP's definition of `struct B`,
// or version 1, `A && a() &&;`

A&&      a = B().a(); // bug: binds directly, dangling reference
A const& a = B().a(); // bug: same as above
A        a = B().a(); // OK

A&&      a = B().m_a; // OK: extends the lifetime of the temporary

Di C ++ 2a, saya pikir Anda harus mengatasi masalah ini (atau yang serupa) sebagai berikut:

for( B b = bar(); auto i : b.a() )

bukannya OP

for( auto i : bar().a() )

Solusinya secara manual menentukan bahwa masa hidup badalah seluruh blok for-loop.

Proposal yang memperkenalkan init-statement ini

Demo Langsung


Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.