Apa yang mencegah tumpang tindih anggota yang berdekatan di kelas?

12

Pertimbangkan tiga structs berikut :

class blub {
    int i;
    char c;

    blub(const blub&) {}
};

class blob {
    char s;

    blob(const blob&) {}
};

struct bla {
    blub b0;
    blob b1;
};

Pada platform tipikal di mana int4 byte, ukuran, pelurusan dan total padding 1 adalah sebagai berikut:

  struct   size   alignment   padding  
 -------- ------ ----------- --------- 
  blub        8           4         3  
  blob        1           1         0  
  bla        12           4         6  

Tidak ada tumpang tindih antara penyimpanan blubdan blobanggota, meskipun ukuran 1 blobpada prinsipnya "pas" di padding blub.

C ++ 20 memperkenalkan no_unique_addressatribut, yang memungkinkan anggota kosong yang berdekatan untuk berbagi alamat yang sama. Ini juga secara eksplisit memungkinkan skenario yang dijelaskan di atas menggunakan padding dari satu anggota untuk menyimpan yang lain. Dari cppreference (penekanan saya):

Menunjukkan bahwa anggota data ini tidak perlu memiliki alamat yang berbeda dari semua anggota data non-statis lainnya di kelasnya. Ini berarti bahwa jika anggota memiliki tipe kosong (misalnya Allocator stateless), kompiler dapat mengoptimalkannya agar tidak menempati ruang, sama seperti jika itu adalah basis kosong. Jika anggota tidak kosong, bantalan ekor di dalamnya dapat juga digunakan kembali untuk menyimpan anggota data lainnya.

Memang, jika kita menggunakan atribut ini pada blub b0, ukuran blatetes 8, sehingga blobmemang disimpan dalam blub seperti yang terlihat pada godbolt .

Akhirnya, kita sampai pada pertanyaan saya:

Teks apa dalam standar (C ++ 11 hingga C ++ 20) yang mencegah tumpang tindih ini no_unique_address, untuk objek yang tidak dapat disalin secara sepele?

Saya perlu mengecualikan objek trivially copyable (TC) dari atas, karena untuk objek TC, diperbolehkan std::memcpydari satu objek ke yang lain, termasuk sub-objek anggota, dan jika penyimpanan tumpang tindih ini akan pecah (karena semua atau sebagian dari penyimpanan untuk anggota yang berdekatan akan ditimpa) 2 .


1 Kami menghitung padding hanya sebagai perbedaan antara ukuran struktur dan ukuran semua anggota konstituennya, secara rekursif.

2 Inilah sebabnya mengapa saya memiliki salinan konstruktor yang didefinisikan: untuk membuat blubdan blobtidak dapat disalin secara sepele .

BeeOnRope
sumber
Saya belum merisetnya, tapi saya menduga aturan "seolah-olah". Jika tidak ada perbedaan yang dapat diamati (sebuah istilah dengan arti sangat spesifik btw) dengan mesin abstrak (yang merupakan kompilasi terhadap kode Anda), maka kompiler dapat mengubah kode tersebut sesuai keinginan.
Jesper Juhl
Cukup yakin ini adalah penipuan ini: stackoverflow.com/questions/53837373/…
NathanOliver
@JesperJuhl - benar, tapi saya bertanya mengapa tidak bisa , tidak mengapa bisa , dan aturan "seolah-olah" biasanya berlaku untuk yang pertama tetapi tidak masuk akal untuk yang terakhir. Juga, "seolah-olah" tidak jelas untuk tata letak struktur yang biasanya menjadi perhatian global, bukan yang lokal. Akhirnya kompiler harus memiliki seperangkat aturan tata letak yang konsisten, kecuali mungkin untuk struktur yang terbukti tidak pernah "lolos".
BeeOnRope
1
@BeeOnRope Saya tidak bisa menjawab pertanyaan Anda, maaf. Itulah sebabnya saya hanya mengirim komentar dan bukan jawaban. Apa yang Anda dapatkan dalam komentar itu adalah tebakan terbaik saya terhadap penjelasan, tetapi saya tidak tahu jawabannya (ingin mempelajarinya sendiri - itulah sebabnya Anda mendapat suara positif).
Jesper Juhl
1
@NicolBolas - apakah Anda membalas pertanyaan yang benar? Ini bukan tentang mendeteksi salinan aman atau apa pun. Sebaliknya saya ingin tahu mengapa padding tidak dapat digunakan kembali antara anggota. Bagaimanapun, Anda salah: disalin secara sepele adalah properti dari tipe dan selalu telah. Namun, untuk menyalin objek dengan aman, keduanya harus memiliki tipe TC (properti dari tipe), dan bukan subjek yang berpotensi tumpang tindih (properti objek, yang saya duga adalah tempat Anda bingung). Masih tidak tahu mengapa kita berbicara tentang salinan di sini.
BeeOnRope

Jawaban:

1

Standar ini sangat sunyi ketika berbicara tentang model memori dan tidak terlalu eksplisit tentang beberapa istilah yang digunakannya. Tapi saya pikir saya menemukan argumentasi yang berhasil (yang mungkin agak lemah)

Pertama, mari kita cari tahu apa yang bahkan merupakan bagian dari suatu objek. [basic.types] / 4 :

Representasi objek dari objek bertipe Tadalah urutan N unsigned charobjek yang diambil oleh objek bertipe T, di mana Nsama dengan sizeof(T). Representasi nilai suatu objek tipe Tadalah himpunan bit yang berpartisipasi dalam mewakili nilai tipe T. Bit dalam representasi objek yang bukan bagian dari representasi nilai adalah bit padding.

Jadi representasi objek b0terdiri dari sizeof(blub) unsigned charobjek, jadi 8 byte. Bit padding adalah bagian dari objek.

Tidak ada objek yang dapat menempati ruang yang lain jika tidak bersarang di dalamnya [basic.life] /1.5 :

Umur objek obertipe Tberakhir ketika:

[...]

(1.5) penyimpanan yang diduduki oleh objek dilepaskan, atau digunakan kembali oleh objek yang tidak bersarang di dalam o([intro.object]).

Jadi masa hidup b0akan berakhir, ketika penyimpanan yang ditempati olehnya akan digunakan kembali oleh objek lain, yaitu b1. Saya belum memeriksanya, tetapi saya pikir mandat standar bahwa sub objek objek yang hidup juga harus hidup (dan saya tidak bisa membayangkan bagaimana ini harus bekerja secara berbeda).

Jadi penyimpanan yang b0 menempati mungkin tidak digunakan oleh b1. Saya tidak menemukan definisi "menempati" dalam standar, tetapi saya pikir interpretasi yang masuk akal akan menjadi "bagian dari representasi objek". Dalam representasi objek deskripsi kutipan, kata-kata "mengambil" digunakan 1 . Di sini, ini akan menjadi 8 byte, jadi blaperlu setidaknya satu lagi untuk b1.

Khusus untuk subobyek (jadi antara lain anggota data non-statis) ada juga ketentuan [intro.object] / 9 (tetapi ini ditambahkan dengan C ++ 20, thx @BeeOnRope)

Dua objek dengan masa hidup yang tumpang tindih yang bukan bidang bit dapat memiliki alamat yang sama jika satu bersarang di dalam yang lain, atau jika setidaknya satu adalah sub-objek ukuran nol dan mereka dari jenis yang berbeda; jika tidak, mereka memiliki alamat yang berbeda dan menempati byte penyimpanan yang terpisah .

(penekanan saya) Di sini lagi, kita memiliki masalah yang "menempati" tidak didefinisikan dan sekali lagi saya akan berdebat untuk mengambil byte dalam representasi objek. Perhatikan bahwa ada catatan kaki untuk ini [basic.memobj] / catatan kaki 29

Di bawah aturan "as-if", suatu implementasi diperbolehkan untuk menyimpan dua objek pada alamat mesin yang sama atau tidak menyimpan objek sama sekali jika program tidak dapat mengamati perbedaannya ([intro.execution]).

Yang dapat memungkinkan kompiler untuk memecahkan ini jika dapat membuktikan bahwa tidak ada efek samping yang dapat diamati. Saya akan berpikir bahwa ini cukup rumit untuk hal mendasar seperti tata letak objek. Mungkin itu sebabnya optimasi ini hanya diambil ketika pengguna memberikan info bahwa tidak ada alasan untuk memisahkan objek dengan menambahkan [no_unique_address]atribut.

tl; dr: Padding mungkin bagian dari objek dan anggota harus dipisahkan.


1 Saya tidak bisa menahan diri untuk tidak menambahkan referensi yang menempati mungkin berarti: Kamus Revisi Unabridged Webster, G. & C. Merriam, 1913 (penekanan milik saya)

  1. Untuk memegang, atau mengisi, dimensi; untuk mengambil ruang atau ruang; untuk menutupi atau mengisi; sebagai, kamp menempati lima hektar tanah. Tuan J. Herschel.

Perayapan standar apa yang akan lengkap tanpa perayapan kamus?

n314159
sumber
2
Bagian "menempati byte terpisah dari penyimpanan" dari into.storage akan cukup, saya pikir, bagi saya - tetapi kata-kata ini hanya ditambahkan dalam C ++ 20 sebagai bagian dari perubahan yang ditambahkan no_unique_address. Itu membuat situasi sebelum C ++ 20 kurang jelas. Saya tidak mengerti alasan Anda yang mengarah ke "Tidak ada objek yang dapat menempati ruang yang lain jika bukan bersarang di dalamnya" dari basic.life/1.5, khususnya cara mendapatkan dari "penyimpanan tempat benda yang ditempati dilepaskan" untuk "tidak ada objek yang dapat menempati ruang yang lain".
BeeOnRope
1
Saya menambahkan klarifikasi kecil ke paragraf itu. Saya harap itu membuatnya lebih dimengerti. Kalau tidak, aku akan melihatnya lagi besok, sekarang ini sudah terlambat bagiku.
n314159
"Dua objek dengan masa hidup yang tumpang tindih yang bukan bidang bit dapat memiliki alamat yang sama jika satu bersarang di dalam yang lain, atau jika setidaknya satu adalah sub-objek dengan ukuran nol dan mereka dari jenis yang berbeda" 2 objek dengan masa hidup yang tumpang tindih, dari tipe yang sama, punya alamat yang sama .
Pengacara Bahasa
Maaf, bisakah Anda menjelaskan? Anda mengutip kutipan standar dari jawaban saya dan membawa contoh yang sedikit bertentangan dengan itu. Saya tidak yakin apakah ini adalah komentar atas jawaban saya dan apakah itu yang seharusnya diberitahukan kepada saya. Mengenai contoh Anda, saya akan mengatakan bahwa seseorang harus mempertimbangkan masih bagian lain dari standar (ada paragraf tentang array char unsigned menyediakan penyimpanan untuk objek lain, sesuatu mengenai optimasi basis berukuran nol dan lebih jauh lagi juga harus melihat apakah penempatan baru memiliki tunjangan khusus, semua hal yang saya pikir tidak relevan dengan contoh
OPs
@ n314159 Saya pikir kata-kata ini mungkin rusak.
Pengacara Bahasa