Mengapa T * dapat dilewatkan dalam register, tetapi unique_ptr <T> tidak bisa?

85

Saya menonton ceramah Chandler Carruth di CppCon 2019:

Tidak ada Abstraksi Tanpa Biaya

di dalamnya, dia memberikan contoh bagaimana dia dikejutkan dengan berapa banyak biaya overhead yang Anda keluarkan dengan menggunakan std::unique_ptr<int>lebih dari satu int*; segmen itu dimulai pada titik waktu 17:25.

Anda dapat melihat hasil kompilasi contoh pasangan potongannya (godbolt.org) - untuk menyaksikan bahwa, memang, tampaknya kompiler tidak mau memberikan nilai unique_ptr - yang sebenarnya pada intinya adalah hanya alamat - di dalam register, hanya dalam memori lurus.

Salah satu poin yang dibuat oleh Mr. Carruth sekitar pukul 27:00 adalah bahwa C ++ ABI memerlukan parameter nilai-nilai (beberapa tetapi tidak semua; mungkin - tipe non-primitif? Tipe non-trivial-konstruktif?) Untuk dilewatkan dalam memori daripada dalam register.

Pertanyaan saya:

  1. Apakah ini sebenarnya persyaratan ABI pada beberapa platform? (yang mana?) Atau mungkin itu hanya pesimisasi dalam skenario tertentu?
  2. Kenapa ABI seperti itu? Yaitu, jika bidang struct / kelas cocok dengan register, atau bahkan satu register - mengapa kita tidak bisa meneruskannya dalam register itu?
  3. Pernahkah komite standar C ++ membahas hal ini dalam beberapa tahun terakhir, atau tidak pernah?

PS - Agar tidak meninggalkan pertanyaan ini tanpa kode:

Pointer polos:

void bar(int* ptr) noexcept;
void baz(int* ptr) noexcept;

void foo(int* ptr) noexcept {
    if (*ptr > 42) {
        bar(ptr); 
        *ptr = 42; 
    }
    baz(ptr);
}

Pointer unik:

using std::unique_ptr;
void bar(int* ptr) noexcept;
void baz(unique_ptr<int> ptr) noexcept;

void foo(unique_ptr<int> ptr) noexcept {
    if (*ptr > 42) { 
        bar(ptr.get());
        *ptr = 42; 
    }
    baz(std::move(ptr));
}
einpoklum
sumber
8
Saya tidak yakin apa persyaratan ABI sebenarnya, tetapi tidak melarang memasukkan struct ke register
harold
6
Jika saya harus menebak saya akan mengatakan itu ada hubungannya dengan fungsi anggota non-sepele yang membutuhkan thispointer yang menunjuk pada lokasi yang valid. unique_ptrmemiliki itu. Menumpahkan register untuk tujuan itu agak akan meniadakan seluruh optimasi "pass in a register".
StoryTeller - Unslander Monica
2
itanium-cxx-abi.github.io/cxx-abi/abi.html#calls . Jadi perilaku ini diperlukan. Mengapa? itanium-cxx-abi.github.io/cxx-abi/cxx-closed.html , cari masalah C-7. Ada beberapa penjelasan di sana, tetapi tidak terlalu rinci. Tapi ya, perilaku ini tampaknya tidak masuk akal bagi saya. Benda-benda ini dapat melewati tumpukan secara normal. Mendorong mereka untuk menumpuk, dan kemudian melewati referensi (hanya untuk objek "non-sepele") tampaknya sia-sia.
geza
6
Tampaknya C ++ melanggar prinsipnya sendiri di sini, yang cukup menyedihkan. Saya yakin 140% setiap Unique_ptr hilang begitu saja setelah kompilasi. Setelah semua itu hanya panggilan destruktor yang ditangguhkan yang dikenal pada waktu kompilasi.
One Man Monkey Squad
7
@ MaximEgorushkin: Jika Anda menulisnya dengan tangan, Anda harus meletakkan pointer di register dan bukan di stack.
einpoklum

Jawaban:

49
  1. Apakah ini sebenarnya persyaratan ABI, atau mungkin itu hanya pesimisasi dalam skenario tertentu?

Salah satu contohnya adalah System B Application Interface Binary, AMD64 Architecture Processor Supplement . ABI ini untuk CPU 64-bit x86-kompatibel (Linux x86_64 architecure). Itu diikuti pada Solaris, Linux, FreeBSD, macOS, Windows Subsystem untuk Linux:

Jika objek C ++ memiliki konstruktor salinan non-sepele atau destruktor non-sepele, itu dilewatkan oleh referensi yang tidak terlihat (objek diganti dalam daftar parameter oleh pointer yang memiliki kelas INTEGER).

Objek dengan konstruktor salinan non-trivial atau destruktor non-trivial tidak dapat diteruskan oleh nilai karena objek tersebut harus memiliki alamat yang ditentukan dengan baik. Masalah serupa berlaku ketika mengembalikan objek dari suatu fungsi.

Perhatikan, bahwa hanya 2 register tujuan umum yang dapat digunakan untuk melewatkan 1 objek dengan konstruktor salinan sepele dan destruktor trivial, yaitu hanya nilai-nilai objek dengan sizeoftidak lebih dari 16 yang dapat diteruskan dalam register. Lihat Konvensi pemanggilan oleh Agner Fog untuk perawatan terperinci dari konvensi pemanggilan, khususnya §7.1 Melewati dan mengembalikan benda. Ada konvensi panggilan terpisah untuk meneruskan tipe SIMD dalam register.

Ada ABI yang berbeda untuk arsitektur CPU lainnya.


  1. Kenapa ABI seperti itu? Yaitu, jika bidang struct / kelas cocok dengan register, atau bahkan satu register - mengapa kita tidak bisa meneruskannya dalam register itu?

Ini adalah detail implementasi, tetapi ketika pengecualian ditangani, selama tumpukan dibuka, objek dengan durasi penyimpanan otomatis yang dihancurkan harus dapat dialamatkan relatif terhadap bingkai fungsi stack karena register telah musnah pada saat itu. Stack unwinding code memerlukan alamat objek untuk memanggil destruktor mereka tetapi objek dalam register tidak memiliki alamat.

Pedantically, destructor beroperasi pada objek :

Objek menempati wilayah penyimpanan dalam periode konstruksinya ([class.cdtor]), sepanjang masa pakainya, dan dalam periode penghancurannya.

dan sebuah objek tidak bisa ada di C ++ jika tidak ada penyimpanan addressable dialokasikan untuk itu karena identitas objek adalah alamatnya .

Ketika alamat objek dengan konstruktor salinan sepele disimpan dalam register diperlukan, kompiler hanya dapat menyimpan objek ke dalam memori dan mendapatkan alamat. Jika copy constructor bersifat non-sepele, di sisi lain, compiler tidak bisa hanya menyimpannya ke dalam memori, itu lebih baik untuk memanggil copy constructor yang mengambil referensi dan karenanya memerlukan alamat objek dalam register. Konvensi panggilan mungkin tidak dapat bergantung pada apakah pembuat salinan disejajarkan dalam callee atau tidak.

Cara lain untuk memikirkan hal ini, adalah bahwa untuk tipe yang dapat disalin secara sepele, kompiler mentransfer nilai suatu objek dalam register, dari mana suatu objek dapat dipulihkan oleh penyimpanan memori biasa jika perlu. Misalnya:

void f(long*);
void g(long a) { f(&a); }

pada x86_64 dengan System V ABI mengkompilasi ke:

g(long):                             // Argument a is in rdi.
        push    rax                  // Align stack, faster sub rsp, 8.
        mov     qword ptr [rsp], rdi // Store the value of a in rdi into the stack to create an object.
        mov     rdi, rsp             // Load the address of the object on the stack into rdi.
        call    f(long*)             // Call f with the address in rdi.
        pop     rax                  // Faster add rsp, 8.
        ret                          // The destructor of the stack object is trivial, no code to emit.

Dalam ceramahnya yang menggugah pemikiran, Chandler Carruth menyebutkan bahwa perubahan ABI yang pecah mungkin diperlukan (antara lain) untuk menerapkan langkah destruktif yang dapat memperbaiki keadaan. IMO, perubahan ABI dapat menjadi non-breaking jika fungsi menggunakan ABI baru secara eksplisit memilih untuk memiliki tautan baru yang berbeda, misalnya mendeklarasikannya dalamextern "C++20" {} blok (mungkin, dalam ruang nama inline baru untuk memigrasi API yang ada). Sehingga hanya kode yang dikompilasi terhadap deklarasi fungsi baru dengan tautan baru yang dapat menggunakan ABI baru.

Perhatikan bahwa ABI tidak berlaku ketika fungsi yang dipanggil telah digariskan. Seperti halnya dengan pembuatan kode tautan-waktu, kompiler dapat menyejajarkan fungsi-fungsi yang didefinisikan dalam unit terjemahan lain atau menggunakan konvensi panggilan khusus.

Maxim Egorushkin
sumber
Komentar bukan untuk diskusi panjang; percakapan ini telah dipindahkan ke obrolan .
Samuel Liew
8

Dengan ABI biasa, destruktor non-sepele -> tidak bisa lewat register

(Ilustrasi titik dalam jawaban @ MaximEgorushkin menggunakan contoh @ harold dalam komentar; dikoreksi sesuai komentar @ Yakk.)

Jika Anda mengkompilasi:

struct Foo { int bar; };
Foo test(Foo byval) { return byval; }

Anda mendapatkan:

test(Foo):
        mov     eax, edi
        ret

yaitu Fooobjek dilewatkan ke testdalam register ( edi) dan juga dikembalikan dalam register (eax ).

Ketika destructor tidak sepele (seperti std::unique_ptrcontoh OP) - ABI umum memerlukan penempatan pada stack. Ini benar bahkan jika destruktor tidak menggunakan alamat objek sama sekali.

Jadi, bahkan dalam kasus ekstrim penghancur apa-apa, jika Anda mengkompilasi:

struct Foo2 {
    int bar;
    ~Foo2() {  }
};

Foo2 test(Foo2 byval) { return byval; }

Anda mendapatkan:

test(Foo2):
        mov     edx, DWORD PTR [rsi]
        mov     rax, rdi
        mov     DWORD PTR [rdi], edx
        ret

dengan pemuatan dan penyimpanan yang tidak berguna.

einpoklum
sumber
Saya tidak yakin dengan argumen ini. Destructor non-sepele tidak melakukan apa pun untuk melarang aturan as-if. Jika alamat tidak diamati, sama sekali tidak ada alasan mengapa harus ada satu. Jadi kompilator yang menyesuaikan dapat dengan senang hati memasukkannya ke dalam register, jika hal itu tidak mengubah perilaku yang dapat diamati (dan kompiler saat ini akan melakukannya jika penelepon diketahui ).
ComicSansMS
1
Sayangnya, ini sebaliknya (Saya setuju bahwa beberapa dari ini sudah tidak masuk akal). Tepatnya: Saya tidak yakin bahwa alasan yang Anda berikan harus membuat ABI yang memungkinkan yang memungkinkan melewati arus std::unique_ptrdalam register yang tidak sesuai.
ComicSansMS
3
"Penghancur sepele [CITATION NEEDED]" jelas salah; jika tidak ada kode yang benar-benar tergantung pada alamat, maka as-if berarti alamat tersebut tidak perlu ada pada mesin yang sebenarnya . Alamat harus ada di mesin abstrak , tetapi hal-hal di mesin abstrak yang tidak berdampak pada mesin yang sebenarnya adalah hal-hal yang seolah-olah diizinkan untuk dihilangkan.
Yakk - Adam Nevraumont
2
@einpoklum Tidak ada dalam standar yang menyatakan register ada. Kata kunci register hanya menyatakan "Anda tidak dapat mengambil alamat". Hanya ada mesin abstrak sejauh standar yang bersangkutan. "seolah-olah" berarti bahwa setiap implementasi mesin nyata hanya perlu berperilaku "seolah-olah" mesin abstrak berperilaku, hingga perilaku yang tidak ditentukan oleh standar. Sekarang, ada masalah yang sangat menantang di sekitar memiliki objek dalam register, yang semua orang bicarakan secara luas. Juga, konvensi panggilan, yang standar juga tidak membahas, memiliki kebutuhan praktis.
Yakk - Adam Nevraumont
1
@einpoklum Tidak, dalam mesin abstrak itu, semua hal memiliki alamat; tetapi alamat hanya dapat diamati dalam keadaan tertentu. Kata registerkunci itu dimaksudkan untuk membuatnya sepele bagi mesin fisik untuk menyimpan sesuatu dalam register dengan memblokir hal-hal yang secara praktis membuatnya lebih sulit untuk "tidak memiliki alamat" di mesin fisik.
Yakk - Adam Nevraumont
2

Apakah ini sebenarnya persyaratan ABI pada beberapa platform? (yang mana?) Atau mungkin itu hanya pesimisasi dalam skenario tertentu?

Jika sesuatu terlihat di batas unit kepatuhan maka apakah itu didefinisikan secara implisit atau eksplisit itu menjadi bagian dari ABI.

Kenapa ABI seperti itu?

Masalah mendasar adalah register disimpan dan dipulihkan sepanjang waktu saat Anda bergerak ke bawah dan naik ke tumpukan panggilan. Jadi tidak praktis untuk memiliki referensi atau penunjuk kepada mereka.

In-lining dan optimisasi yang dihasilkan dari itu bagus ketika itu terjadi, tetapi seorang desainer ABI tidak dapat mengandalkan itu terjadi. Mereka harus merancang ABI dengan asumsi kasus terburuk. Saya tidak berpikir programmer akan sangat senang dengan kompiler di mana ABI berubah tergantung pada level optimisasi.

Jenis yang dapat disalin sepele dapat dilewatkan dalam register karena operasi penyalinan logis dapat dibagi menjadi dua bagian. Parameter disalin ke register yang digunakan untuk melewatkan parameter oleh pemanggil dan kemudian disalin ke variabel lokal oleh callee. Dengan demikian, apakah variabel lokal memiliki lokasi memori atau tidak, hanya menjadi perhatian callee.

Tipe di mana salinan atau memindahkan konstruktor harus digunakan di sisi lain tidak dapat memiliki operasi salinan itu terpecah dengan cara ini, sehingga harus dilewatkan dalam memori.

Pernahkah komite standar C ++ membahas hal ini dalam beberapa tahun terakhir, atau tidak pernah?

Saya tidak tahu apakah badan standar telah mempertimbangkan hal ini.

Solusi yang jelas bagi saya adalah menambahkan gerakan destruktif yang tepat (daripada rumah setengah jalan saat ini dari "negara yang valid tetapi tidak ditentukan") ke langauge, kemudian memperkenalkan cara untuk menandai suatu jenis yang memungkinkan untuk "gerakan destruktif sepele "Bahkan jika itu tidak memungkinkan untuk salinan sepele.

tetapi solusi semacam itu AKAN membutuhkan pemecahan ABI dari kode yang ada untuk menerapkan untuk jenis yang sudah ada, yang dapat membawa sedikit perlawanan (meskipun ABI pecah sebagai akibat dari versi standar C ++ baru yang belum pernah terjadi sebelumnya, misalnya perubahan std :: string di C ++ 11 menghasilkan istirahat ABI ..

plugwash
sumber
Bisakah Anda menguraikan bagaimana langkah-langkah destruktif yang tepat memungkinkan untuk unique_ptr dilewatkan dalam register? Apakah itu karena itu akan memungkinkan menjatuhkan persyaratan untuk penyimpanan beralamat?
einpoklum
Langkah-langkah destruktif yang tepat akan memungkinkan konsep langkah-langkah destruktif sepele untuk diperkenalkan. Ini akan memungkinkan langkah sepele tersebut untuk dipecah oleh ABI dengan cara yang sama seperti salinan sepele bisa hari ini.
plugwash
Meskipun Anda juga ingin menambahkan aturan bahwa kompiler dapat mengimplementasikan pass parameter sebagai gerakan biasa atau salin diikuti oleh "langkah destruktif sepele" untuk memastikan bahwa selalu mungkin untuk lulus dalam register di mana pun parameter berasal.
plugwash
Karena ukuran register dapat menyimpan pointer, tetapi struktur unique_ptr? Apa itu sizeof (unique_ptr <T>)?
Mel Viso Martinez
@MelVisoMartinez Anda mungkin membingungkan unique_ptrdan shared_ptrsemantik: shared_ptr<T>memungkinkan Anda memberikan ke ctor 1) ptr x ke objek turunan yang akan dihapus dengan tipe U statis / ekspresi delete x;(jadi Anda tidak memerlukan virtual dtor di sini) 2) atau bahkan fungsi pembersihan kustom. Itu berarti bahwa keadaan runtime digunakan di dalam shared_ptrblok kontrol untuk menyandikan informasi itu. OTOH unique_ptrtidak memiliki fungsi seperti itu dan tidak menyandikan perilaku penghapusan di negara; satu-satunya cara untuk menyesuaikan pembersihan adalah dengan membuat instanciation template lain (tipe kelas lain).
curiousguy
-1

Pertama kita perlu kembali ke apa artinya lulus dengan nilai dan referensi.

Untuk bahasa seperti Java dan SML, pass by value sangat mudah (dan tidak ada pass by reference), sama seperti menyalin nilai variabel, karena semua variabel hanyalah skalar dan telah dibangun di dalam semantic copy: mereka adalah apa yang dihitung sebagai aritmatika ketik C ++, atau "referensi" (pointer dengan nama dan sintaks yang berbeda).

Di C kami memiliki tipe skalar dan yang ditentukan pengguna:

  • Skalar memiliki nilai numerik atau abstrak (pointer bukan angka, mereka memiliki nilai abstrak) yang disalin.
  • Jenis agregat memiliki semua anggota yang mungkin diinisialisasi yang mungkin disalin:
    • untuk jenis produk (array dan struktur): secara rekursif, semua anggota struktur dan elemen array disalin (sintaks fungsi C tidak memungkinkan untuk meneruskan array dengan nilai secara langsung, hanya susunan anggota struct, tetapi itu adalah detail ).
    • untuk tipe penjumlahan (serikat pekerja): nilai "anggota aktif" dipertahankan; jelas, salinan anggota demi anggota tidak berurutan karena tidak semua anggota dapat diinisialisasi.

Dalam C ++ tipe yang ditentukan pengguna dapat memiliki semantik salin yang ditentukan pengguna, yang memungkinkan pemrograman "berorientasi objek" dengan objek dengan kepemilikan sumber daya dan operasi "salinan dalam". Dalam kasus seperti itu, operasi penyalinan sebenarnya adalah panggilan ke fungsi yang hampir dapat melakukan operasi sewenang-wenang.

Untuk C struct yang dikompilasi sebagai C ++, "penyalinan" masih didefinisikan sebagai memanggil operasi penyalinan yang ditentukan pengguna (baik operator konstruktor atau penugasan), yang secara implisit dihasilkan oleh kompiler. Ini berarti bahwa semantik program subset umum C / C ++ berbeda dalam C dan C ++: dalam C tipe agregat keseluruhan disalin, dalam C ++ fungsi penyalinan yang dihasilkan secara implisit dipanggil untuk menyalin setiap anggota; hasil akhirnya adalah bahwa dalam setiap kasus masing-masing anggota disalin.

(Saya pikir, ada pengecualian ketika struct di dalam sebuah serikat disalin.)

Jadi untuk tipe kelas, satu-satunya cara (di luar salinan union) untuk membuat instance baru adalah melalui konstruktor (bahkan bagi mereka yang memiliki konstruktor kompiler yang dihasilkan sepele).

Anda tidak dapat mengambil alamat nilai melalui operator unary &tetapi itu tidak berarti bahwa tidak ada objek nilai; dan suatu objek, menurut definisi, memiliki alamat ; dan alamat itu bahkan diwakili oleh konstruksi sintaks: objek tipe kelas hanya dapat dibuat oleh konstruktor, dan memiliki thispointer; tetapi untuk jenis sepele, tidak ada konstruktor yang ditulis pengguna sehingga tidak ada tempat untuk meletakkan thissampai setelah salinan dibuat, dan dinamai.

Untuk jenis skalar, nilai suatu objek adalah nilai dari objek, nilai matematika murni yang disimpan ke dalam objek.

Untuk jenis kelas, satu-satunya gagasan tentang nilai objek adalah salinan lain dari objek, yang hanya dapat dibuat oleh copy constructor, fungsi nyata (walaupun untuk tipe sepele yang fungsinya sangat khusus sepele, kadang-kadang ini bisa menjadi dibuat tanpa memanggil konstruktor). Itu berarti bahwa nilai objek adalah hasil dari perubahan status program global dengan eksekusi . Itu tidak mengakses secara matematis.

Jadi, lulus dengan nilai sebenarnya bukan hal: itu lewat panggilan copy constructor , yang kurang cantik. Copy constructor diharapkan untuk melakukan operasi "copy" yang masuk akal sesuai dengan semantik yang tepat dari tipe objek, dengan menghormati invarian internalnya (yang merupakan properti pengguna abstrak, bukan properti C ++ intrinsik).

Lewati dengan nilai objek kelas berarti:

  • buat contoh lain
  • kemudian buat fungsi dipanggil bertindak pada contoh itu.

Perhatikan bahwa masalah tidak ada hubungannya dengan apakah salinan itu sendiri adalah objek dengan alamat: semua parameter fungsi adalah objek dan memiliki alamat (pada tingkat semantik bahasa).

Masalahnya adalah apakah:

  • salinan adalah objek baru yang diinisialisasi dengan nilai matematika murni (true pure rvalue) dari objek asli, seperti halnya skalar;
  • atau salinannya adalah nilai objek asli , seperti pada kelas.

Dalam kasus tipe kelas sepele, Anda masih dapat menentukan anggota salinan anggota asli, sehingga Anda bisa menentukan nilai murni asli karena sepele dari operasi penyalinan (copy constructor dan assignment). Tidak demikian halnya dengan fungsi pengguna khusus yang sewenang-wenang: nilai dokumen asli harus berupa salinan yang dibuat.

Objek kelas harus dibangun oleh penelepon; konstruktor secara formal memiliki thispointer tetapi formalisme tidak relevan di sini: semua objek secara formal memiliki alamat tetapi hanya mereka yang benar-benar mendapatkan alamatnya digunakan dengan cara yang tidak murni lokal (tidak seperti *&i = 1;yang menggunakan alamat lokal murni) perlu memiliki definisi yang baik alamat.

Objek harus benar-benar melalui alamat jika harus memiliki alamat di kedua fungsi yang dikompilasi secara terpisah ini:

void callee(int &i) {
  something(&i);
}

void caller() {
  int i;
  callee(i);
  something(&i);
}

Di sini bahkan jika something(address)adalah fungsi murni atau makro atau apa pun (seperti printf("%p",arg)) yang tidak dapat menyimpan alamat atau berkomunikasi dengan entitas lain, kami memiliki persyaratan untuk melewati alamat karena alamat harus didefinisikan dengan baik untuk objek unik intyang memiliki keunikan identitas.

Kami tidak tahu apakah fungsi eksternal akan "murni" dalam hal alamat yang diteruskan ke sana.

Di sini potensi untuk penggunaan nyata dari alamat baik dalam konstruktor non sepele atau destruktor di sisi pemanggil mungkin alasan untuk mengambil rute yang aman dan sederhana dan memberikan objek identitas di pemanggil dan menyampaikan alamatnya, karena itu membuat yakin bahwa setiap penggunaan non-sepele alamatnya di konstruktor, setelah konstruksi dan di destruktor konsisten : thisharus tampak sama atas keberadaan objek.

Konstruktor atau destruktor non trivial seperti fungsi lain dapat menggunakan thispointer dengan cara yang membutuhkan konsistensi atas nilainya meskipun beberapa objek dengan hal-hal non sepele mungkin tidak:

struct file_handler { // don't use that class!
    file_handler () { this->fileno = -1; }
    file_handler (int f) { this->fileno = f; }
    file_handler (const file_handler& rhs) {
        if (this->fileno != -1)
            this->fileno = dup(rhs.fileno);
        else
            this->fileno = -1;
    }
    ~file_handler () {
        if (this->fileno != -1)
            close(this->fileno); 
    }
    file_handler &operator= (const file_handler& rhs);
};

Perhatikan bahwa dalam kasus itu, meskipun menggunakan pointer secara eksplisit (sintaksis eksplisit this->), identitas objek tidak relevan: kompiler dapat menggunakan bitwise untuk menyalin objek sekitar untuk memindahkannya dan melakukan "salin elisi". Ini didasarkan pada tingkat "kemurnian" penggunaan thisdalam fungsi anggota khusus (alamat tidak lepas).

Tetapi kemurnian bukanlah atribut yang tersedia di tingkat deklarasi standar (ada ekstensi kompiler yang menambahkan deskripsi kemurnian pada deklarasi fungsi non-inline), jadi Anda tidak dapat mendefinisikan ABI berdasarkan pada kemurnian kode yang mungkin tidak tersedia (kode mungkin atau mungkin tidak sebaris dan tersedia untuk analisis).

Kemurnian diukur sebagai "tentu murni" atau "tidak murni atau tidak diketahui". Dasar bersama, atau batas atas semantik (sebenarnya maksimum), atau LCM (Least Common Multiple) "tidak diketahui". Jadi ABI memutuskan tidak diketahui.

Ringkasan:

  • Beberapa konstruk memerlukan kompiler untuk mendefinisikan identitas objek.
  • ABI didefinisikan dalam istilah kelas program dan bukan kasus khusus yang mungkin dioptimalkan.

Kemungkinan pekerjaan di masa depan:

Apakah penjelasan kemurnian cukup bermanfaat untuk digeneralisasi dan distandarisasi?

curiousguy
sumber
1
Contoh pertama Anda tampak menyesatkan. Saya pikir Anda hanya membuat titik secara umum, tetapi pada awalnya saya pikir Anda membuat analogi terhadap kode dalam pertanyaan. Tetapi void foo(unique_ptr<int> ptr)mengambil objek kelas dengan nilai . Objek itu memiliki anggota pointer, tetapi kita berbicara tentang objek kelas itu sendiri yang dilewatkan oleh referensi. (Karena itu tidak dapat disalin secara trivial maka konstruktor / destruktornya perlu konsisten this.) Itulah argumen yang sebenarnya dan tidak terhubung dengan contoh pertama lewat referensi secara eksplisit ; dalam hal ini pointer dilewatkan dalam register.
Peter Cordes
@PeterCordes " Anda membuat analogi dengan kode dalam pertanyaan. " Saya melakukan hal itu. " objek kelas dengan nilai " Ya saya mungkin harus menjelaskan bahwa secara umum tidak ada yang namanya "nilai" dari objek kelas sehingga nilai untuk jenis bukan matematika tidak "oleh nilai". " Objek itu memiliki anggota penunjuk " Sifat seperti ptr dari "smart ptr" tidak relevan; dan begitu juga anggota ptr dari "smart ptr". Ptr hanyalah skalar seperti int: Saya menulis contoh "smart fileno" yang menggambarkan bahwa "kepemilikan" tidak ada hubungannya dengan "membawa ptr".
curiousguy
1
Nilai objek kelas adalah representasi objeknya. Sebab unique_ptr<T*>, ini adalah ukuran dan tata letak yang sama seperti T*dan cocok dalam register. Objek kelas yang dapat disalin secara sepele dapat diteruskan dengan nilai dalam register di Sistem x86-64, seperti kebanyakan konvensi pemanggilan. Hal ini membuat sebuah salinan dari unique_ptrobjek, tidak seperti di Anda intcontoh di mana callee ini &i adalah alamat dari penelepon itu ikarena Anda lewat referensi di C tingkat ++ , bukan hanya sebagai detail implementasi asm.
Peter Cordes
1
Err, koreksi komentar terakhir saya. Ini tidak hanya membuat salinan dari unique_ptrobjek; itu menggunakan std::movesehingga aman untuk menyalinnya karena itu tidak akan menghasilkan 2 salinan yang sama unique_ptr. Tetapi untuk jenis yang dapat disalin secara sepele, ya, ia memang menyalin seluruh objek agregat. Jika itu adalah anggota tunggal, konvensi pemanggilan yang baik memperlakukannya sama seperti skalar dari jenis itu.
Peter Cordes
1
Terlihat lebih baik. Catatan: Untuk C struct yang dikompilasi sebagai C ++ - Ini bukan cara yang berguna untuk memperkenalkan perbedaan antara C ++. Dalam C ++ struct{}adalah sebuah C ++ struct. Mungkin Anda harus mengatakan "struct sederhana", atau "tidak seperti C". Karena ya, ada perbedaan. Jika Anda menggunakan atomic_intsebagai anggota struct, C akan secara non-atomik menyalinnya, kesalahan C ++ pada konstruktor salinan yang dihapus. Saya lupa apa yang dilakukan C ++ pada struct dengan volatileanggota. C akan membiarkan Anda melakukannya struct tmp = volatile_struct;untuk menyalin semuanya (berguna untuk SeqLock); C ++ tidak akan.
Peter Cordes