Menyalin struct dengan anggota yang tidak diinisialisasi

29

Apakah valid untuk menyalin struct beberapa yang anggotanya tidak diinisialisasi?

Saya menduga itu adalah perilaku yang tidak terdefinisi, tetapi jika demikian, itu membuat meninggalkan anggota yang tidak diinisialisasi dalam sebuah struct (bahkan jika anggota tersebut tidak pernah digunakan secara langsung) sangat berbahaya. Jadi saya bertanya-tanya apakah ada sesuatu dalam standar yang memungkinkannya.

Misalnya, apakah ini valid?

struct Data {
  int a, b;
};

int main() {
  Data data;
  data.a = 5;
  Data data2 = data;
}
Tomek Czajka
sumber
Saya ingat pernah melihat pertanyaan serupa beberapa waktu lalu tetapi tidak dapat menemukannya. Ini pertanyaan terkait seperti yang satu ini .
1201ProgramAlarm

Jawaban:

23

Ya, jika anggota yang tidak diinisialisasi bukan tipe karakter sempit yang tidak ditandatangani atau std::byte, kemudian menyalin struktur yang berisi nilai tak tentu ini dengan konstruktor salinan yang didefinisikan secara implisit adalah perilaku yang tidak ditentukan secara teknis, karena untuk menyalin variabel dengan nilai tak tentu dari jenis yang sama, karena dari [dcl.init] / 12 .

Ini berlaku di sini, karena copy constructor yang dihasilkan secara implisit, kecuali untuk unions, didefinisikan untuk menyalin setiap anggota secara individual seolah-olah dengan inisialisasi langsung, lihat [class.copy.ctor] / 4 .

Ini juga tunduk pada masalah CWG aktif 2264 .

Saya kira dalam praktiknya Anda tidak akan memiliki masalah dengan itu.

Jika Anda ingin 100% yakin, menggunakan std::memcpyselalu memiliki perilaku yang terdefinisi dengan baik jika jenisnya dapat disalin secara sepele , bahkan jika anggota memiliki nilai tak tentu.


Selain masalah-masalah ini, Anda harus selalu menginisialisasi anggota kelas Anda dengan benar dengan nilai yang ditentukan pada konstruksi, dengan asumsi Anda tidak memerlukan kelas untuk memiliki konstruktor default sepele . Anda dapat melakukannya dengan mudah menggunakan sintaks penginisialisasi anggota default untuk misalnya menginisialisasi nilai anggota:

struct Data {
  int a{}, b{};
};

int main() {
  Data data;
  data.a = 5;
  Data data2 = data;
}
kenari
sumber
baik .. struct itu bukan POD (data lama biasa)? Itu berarti anggota akan diinisialisasi dengan nilai default? Ini sebuah keraguan
Kevin Kouketsu
Bukankah ini salinan dangkal dalam hal ini? apa yang bisa salah dengan ini kecuali anggota yang tidak diinisialisasi diakses di struct yang disalin?
TruthSeeker
@KevinKouketsu Saya telah menambahkan kondisi untuk kasus di mana jenis sepele / POD diperlukan.
walnut
@ TruthSeeker Standar mengatakan bahwa itu adalah perilaku yang tidak terdefinisi. Alasannya umumnya perilaku tidak terdefinisi untuk variabel (non-anggota) dijelaskan dalam jawaban oleh AndreySemashev. Pada dasarnya itu untuk mendukung representasi perangkap dengan memori yang tidak diinisialisasi. Apakah ini dimaksudkan untuk diterapkan pada konstruksi tersirat dari struct adalah pertanyaan dari masalah CWG yang ditautkan.
walnut
@ TruthSeeker Konstruktor salinan implisit didefinisikan untuk menyalin setiap anggota secara individual seolah-olah dengan inisialisasi langsung. Tidak didefinisikan untuk menyalin representasi objek seolah-olah oleh memcpy, bahkan untuk jenis yang dapat disalin sepele. Satu-satunya pengecualian adalah serikat, di mana konstruktor copy implisit menyalin representasi objek seolah-olah oleh memcpy.
walnut
11

Secara umum, menyalin data yang tidak diinisialisasi adalah perilaku yang tidak terdefinisi karena data itu mungkin berada dalam kondisi terperangkap. Mengutip halaman ini :

Jika representasi objek tidak mewakili nilai apa pun dari jenis objek, itu dikenal sebagai representasi perangkap. Mengakses representasi jebakan dengan cara apa pun selain membacanya melalui ekspresi nilai tinggi dari tipe karakter adalah perilaku yang tidak terdefinisi.

Signalling NaN dimungkinkan untuk tipe floating point, dan pada beberapa platform bilangan bulat mungkin memiliki representasi trap.

Namun, untuk jenis yang dapat disalin sepele , dimungkinkan untuk digunakan memcpyuntuk menyalin representasi mentah objek. Melakukannya aman karena nilai objek tidak ditafsirkan, dan sebaliknya urutan byte mentah representasi objek disalin.

Andrey Semashev
sumber
Bagaimana dengan data tipe yang semua pola bitnya mewakili nilai-nilai yang valid (mis. Struct 64-byte yang mengandung a unsigned char[64])? Memperlakukan byte dari sebuah struct sebagai memiliki nilai-nilai yang tidak ditentukan dapat menghambat optimasi, tetapi membutuhkan programmer untuk secara manual mengisi array dengan nilai-nilai yang tidak berguna akan menghambat efisiensi bahkan lebih.
supercat
Menginisialisasi data tidak sia-sia, itu mencegah UB, apakah itu disebabkan oleh representasi perangkap atau dengan menggunakan data yang tidak diinisialisasi nanti. Menurunkan 64 byte (1 atau 2 garis cache) tidak semahal kelihatannya. Dan jika Anda memiliki struktur besar yang harganya mahal, Anda harus berpikir dua kali sebelum menyalinnya. Dan saya cukup yakin Anda harus menginisialisasi mereka di beberapa titik.
Andrey Semashev
Operasi kode mesin yang tidak mungkin memengaruhi perilaku program tidak berguna. Gagasan bahwa tindakan apa pun yang dicirikan sebagai UB oleh Standar harus dihindari dengan cara apa pun, alih-alih mengatakan bahwa [dalam kata-kata Komite Standar C] UB "mengidentifikasi bidang-bidang yang mungkin terkait perluasan bahasa", relatif baru. Meskipun saya belum melihat Dasar Pemikiran yang diterbitkan untuk Standar C ++, ini secara tegas menghapus yurisdiksi atas apa yang "diizinkan" program C ++ dengan menolak mengategorikan program sebagai sesuai atau tidak sesuai, yang berarti akan memungkinkan ekstensi serupa.
supercat
-1

Dalam beberapa kasus, seperti yang dijelaskan, Standar C ++ memungkinkan kompiler untuk memproses konstruksi dengan cara apa pun yang menurut pelanggan mereka paling berguna, tanpa mengharuskan perilaku dapat diprediksi. Dengan kata lain, konstruksi semacam itu memanggil "Perilaku Tidak Terdefinisi". Itu tidak berarti, bagaimanapun, bahwa konstruksi seperti itu dimaksudkan untuk "dilarang" karena Standar C ++ secara eksplisit mengesampingkan yurisdiksi atas apa yang "boleh" dilakukan oleh program yang dibentuk dengan baik. Sementara saya tidak mengetahui adanya dokumen Rationale yang diterbitkan untuk Standar C ++, fakta bahwa itu menggambarkan Perilaku Tidak Terdefinisi seperti halnya C89 akan menyarankan makna yang dimaksudkan adalah serupa: "Perilaku tidak terdefinisi memberikan lisensi implementor untuk tidak menangkap kesalahan program tertentu yang sulit. untuk mendiagnosis.

Ada banyak situasi di mana cara paling efisien untuk memproses sesuatu akan melibatkan penulisan bagian-bagian dari struktur yang akan dipedulikan kode hilir, sementara mengabaikan yang tidak dipedulikan kode hilir. Mengharuskan program menginisialisasi semua anggota struktur, termasuk yang tidak ada yang peduli, akan menghambat efisiensi.

Lebih lanjut, ada beberapa situasi di mana mungkin paling efisien untuk memiliki data yang tidak diinisialisasi berperilaku dengan cara non-deterministik. Misalnya, diberikan:

struct q { unsigned char dat[256]; } x,y;

void test(unsigned char *arr, int n)
{
  q temp;
  for (int i=0; i<n; i++)
    temp.dat[arr[i]] = i;
  x=temp;
  y=temp;
}

jika kode hilir tidak akan peduli dengan nilai-nilai elemen apa pun x.datatau y.datyang indeksnya tidak tercantum arr, kode tersebut mungkin dioptimalkan untuk:

void test(unsigned char *arr, int n)
{
  q temp;
  for (int i=0; i<n; i++)
  {
    int it = arr[i];
    x.dat[index] = i;
    y.dat[index] = i;
  }
}

Peningkatan dalam efisiensi ini tidak akan mungkin jika programmer diminta untuk secara eksplisit menulis setiap elemen temp.dat , termasuk yang hilir tidak akan peduli, sebelum menyalinnya.

Di sisi lain, ada beberapa aplikasi yang penting untuk menghindari kemungkinan kebocoran data. Dalam aplikasi seperti itu, mungkin berguna untuk memiliki versi kode yang diinstruksikan untuk menjebak setiap upaya untuk menyalin penyimpanan yang tidak diinisialisasi tanpa memperhatikan apakah kode hilir akan melihatnya, atau mungkin berguna untuk memiliki jaminan implementasi bahwa penyimpanan apa pun yang isinya dapat dibocorkan akan menjadi nol atau ditimpa dengan data yang tidak rahasia.

Dari apa yang dapat saya katakan, Standar C ++ tidak berusaha untuk mengatakan bahwa salah satu dari perilaku ini cukup berguna daripada yang lain untuk membenarkan mandatnya. Ironisnya, kurangnya spesifikasi ini dapat dimaksudkan untuk memfasilitasi optimasi, tetapi jika programmer tidak dapat mengeksploitasi segala jenis jaminan perilaku yang lemah, setiap optimasi akan dinegasikan.

supercat
sumber
-2

Karena semua anggota Dataadalah tipe primitif, data2akan mendapatkan "salinan bit-demi-bit" yang tepat dari semua anggota data. Jadi nilai data2.bakan persis sama dengan nilai data.b. Namun, nilai pasti dari data.btidak dapat diprediksi, karena Anda belum menginisialisasi secara eksplisit. Ini akan tergantung pada nilai byte di wilayah memori yang dialokasikan untuk data.

ivan.ukr
sumber
Bisakah Anda mendukung ini dengan mengacu pada standar? Tautan yang disediakan oleh @walnut menyiratkan bahwa ini adalah perilaku yang tidak terdefinisi. Apakah ada pengecualian untuk POD dalam standar?
Tomek Czajka
Meskipun berikut ini tidak tertaut ke standar, masih: en.cppreference.com/w/cpp/language/… "Objek triviallyCopyable dapat disalin dengan menyalin representasi objek mereka secara manual, misalnya dengan std :: memmove. Semua tipe data yang kompatibel dengan C bahasa (tipe POD) dapat disalin secara sepele. "
ivan.ukr
Satu-satunya "perilaku tidak terdefinisi" dalam hal ini adalah bahwa kami tidak dapat memprediksi nilai variabel anggota yang tidak diinisialisasi. Tetapi kode tersebut mengkompilasi dan berjalan dengan sukses.
ivan.ukr
1
Fragmen yang Anda kutip berbicara tentang perilaku memmove, tetapi itu tidak benar-benar relevan di sini karena dalam kode saya, saya menggunakan copy constructor, bukan memmove. Jawaban lain menyiratkan bahwa menggunakan hasil konstruktor salinan dalam perilaku yang tidak ditentukan. Saya pikir Anda juga salah memahami istilah "perilaku tidak terdefinisi". Ini berarti bahwa bahasa tidak memberikan jaminan sama sekali, misalnya program mungkin macet atau merusak data secara acak atau melakukan apa pun. Itu tidak hanya berarti bahwa beberapa nilai tidak dapat diprediksi, itu akan menjadi perilaku yang tidak ditentukan.
Tomek Czajka
@ ivan.ukr Standar C ++ menetapkan bahwa konstruktor copy / move implisit bertindak berdasarkan anggota seolah-olah dengan inisialisasi langsung, lihat tautan dalam jawaban saya. Oleh karena itu konstruksi salinan tidak membuat " " salinan bit-demi-bit " ". Anda hanya benar untuk jenis serikat, yang copy constructor implisit yang ditentukan untuk menyalin representasi objek seolah-olah dengan manual std::memcpy. Tak satu pun dari ini mencegah penggunaan std::memcpyatau std::memmove. Ini hanya mencegah penggunaan copy constructor implisit.
walnut