Mengapa C dan C ++ mendukung penugasan memberwise array dalam struct, tetapi tidak secara umum?

87

Saya memahami bahwa penetapan array berdasarkan anggota tidak didukung, sehingga hal berikut tidak akan berfungsi:

int num1[3] = {1,2,3};
int num2[3];
num2 = num1; // "error: invalid array assignment"

Saya baru saja menerima ini sebagai fakta, membayangkan bahwa tujuan bahasa adalah untuk menyediakan kerangka kerja terbuka, dan membiarkan pengguna memutuskan bagaimana mengimplementasikan sesuatu seperti menyalin sebuah array.

Namun, berikut ini berhasil:

struct myStruct { int num[3]; };
struct myStruct struct1 = {{1,2,3}};
struct myStruct struct2;
struct2 = struct1;

Array num[3]adalah anggota-bijaksana yang ditetapkan dari instansinya di struct1, ke dalam instansinya di struct2.

Mengapa penugasan array yang bijaksana didukung untuk struct, tetapi tidak secara umum?

edit : Komentar Roger Pate di utas std :: string dalam struct - Masalah salin / tugas? sepertinya menunjuk ke arah umum dari jawabannya, tetapi saya tidak cukup tahu untuk memastikannya sendiri.

sunting 2 : Banyak tanggapan yang sangat baik. Saya memilih Luther Blissett karena saya sebagian besar bertanya-tanya tentang alasan filosofis atau historis di balik perilaku tersebut, tetapi referensi James McNellis ke dokumentasi spesifikasi terkait juga berguna.

ozmo
sumber
6
Saya membuat ini memiliki C dan C ++ sebagai tag, karena ini berasal dari C. Juga, pertanyaan yang bagus.
GManNickG
4
Perlu dicatat bahwa dahulu kala di C, penugasan struktur secara umum tidak memungkinkan dan Anda harus menggunakan memcpy()atau serupa.
ggg
Sedikit FYI ... boost::array( boost.org/doc/libs/release/doc/html/array.html ) dan sekarang std::array( en.cppreference.com/w/cpp/container/array ) adalah alternatif yang kompatibel dengan STL untuk array C tua yang berantakan. Mereka mendukung tugas menyalin.
Emile Cormier
@EmileCormier Dan mereka - tada! - struktur di sekitar array.
Peter - Pulihkan Monica

Jawaban:

46

Inilah pendapat saya:

Pengembangan Bahasa C menawarkan beberapa wawasan tentang evolusi tipe array di C:

Saya akan mencoba menguraikan hal array:

Pendahulu C B dan BCPL tidak memiliki tipe larik yang berbeda, deklarasi seperti:

auto V[10] (B)
or 
let V = vec 10 (BCPL)

akan mendeklarasikan V sebagai pointer (tanpa tipe) yang diinisialisasi untuk menunjuk ke wilayah 10 "kata" memori yang tidak digunakan. B sudah digunakan *untuk pointer dereferencing dan memiliki [] notasi tangan pendek, *(V+i)dimaksudkan V[i], seperti di C / C ++ hari ini. Bagaimanapun, Vini bukanlah sebuah array, itu masih sebuah pointer yang menunjuk ke beberapa memori. Ini menimbulkan masalah ketika Dennis Ritchie mencoba memperpanjang B dengan tipe struct. Dia ingin array menjadi bagian dari struct, seperti di C hari ini:

struct {
    int inumber;
    char name[14];
};

Tetapi dengan konsep array B, BCPL sebagai pointer, ini akan membutuhkan namefield untuk memuat pointer yang harus diinisialisasi pada saat runtime ke wilayah memori 14 byte dalam struct. Masalah inisialisasi / tata letak pada akhirnya diselesaikan dengan memberikan perlakuan khusus kepada array: Kompilator akan melacak lokasi array dalam struktur, di tumpukan, dll. Tanpa benar-benar memerlukan penunjuk ke data untuk terwujud, kecuali dalam ekspresi yang melibatkan array. Perlakuan ini memungkinkan hampir semua kode B untuk tetap berjalan dan merupakan sumber dari aturan "larik dikonversi ke penunjuk jika Anda melihatnya" . Ini adalah peretasan kompatibilitas, yang ternyata sangat berguna, karena memungkinkan array berukuran terbuka, dll.

Dan inilah tebakan saya mengapa array tidak dapat ditetapkan: Karena array adalah pointer di B, Anda cukup menulis:

auto V[10];
V=V+5;

untuk me-rebase sebuah "array". Ini sekarang tidak ada artinya, karena basis variabel array bukan lagi nilai l. Jadi tugas ini tidak diizinkan, yang membantu menangkap beberapa program yang melakukan pemeringkatan ini pada array yang dideklarasikan. Dan kemudian gagasan ini macet: Karena array tidak pernah dirancang untuk menjadi kelas pertama yang dikutip dari sistem tipe C, mereka kebanyakan diperlakukan sebagai binatang khusus yang menjadi penunjuk jika Anda menggunakannya. Dan dari sudut pandang tertentu (yang mengabaikan bahwa C-array adalah hack yang gagal), pelarangan penetapan array masih masuk akal: Array terbuka atau parameter fungsi array diperlakukan sebagai penunjuk tanpa informasi ukuran. Kompilator tidak memiliki informasi untuk menghasilkan penetapan larik untuk mereka dan penugasan penunjuk diperlukan untuk alasan kompatibilitas.

/* Example how array assignment void make things even weirder in C/C++, 
   if we don't want to break existing code.
   It's actually better to leave things as they are...
*/
typedef int vec[3];

void f(vec a, vec b) 
{
    vec x,y; 
    a=b; // pointer assignment
    x=y; // NEW! element-wise assignment
    a=x; // pointer assignment
    x=a; // NEW! element-wise assignment
}

Ini tidak berubah ketika revisi C pada tahun 1978 menambahkan penetapan struct ( http://cm.bell-labs.com/cm/cs/who/dmr/cchanges.pdf ). Meskipun record adalah tipe yang berbeda di C, itu tidak mungkin untuk menetapkannya di awal K&R C. Anda harus menyalinnya dari segi anggota dengan memcpy dan Anda hanya dapat meneruskan pointer ke mereka sebagai parameter fungsi. Assigment (dan parameter passing) sekarang hanya didefinisikan sebagai memcpy dari memori mentah struct dan karena ini tidak dapat merusak kode yang sudah ada, maka dengan mudah diadpot. Sebagai efek samping yang tidak disengaja, ini secara implisit memperkenalkan beberapa jenis penugasan array, tetapi ini terjadi di suatu tempat di dalam struktur, jadi ini tidak bisa benar-benar memperkenalkan masalah dengan cara array digunakan.

Mainframe Nordik
sumber
Sayang sekali C tidak mendefinisikan sintaks, misalnya int[10] c;untuk membuat lvalue cberperilaku sebagai larik yang terdiri dari sepuluh item, bukan sebagai penunjuk ke item pertama dari larik sepuluh item. Ada beberapa situasi di mana berguna untuk dapat membuat typedef yang mengalokasikan ruang saat digunakan untuk variabel, tetapi meneruskan penunjuk saat digunakan sebagai argumen fungsi, tetapi ketidakmampuan untuk memiliki nilai tipe array adalah kelemahan semantik yang signifikan dalam bahasa tersebut.
supercat
Alih-alih mengatakan "penunjuk yang harus menunjuk ke beberapa memori", hal yang penting adalah bahwa penunjuk itu sendiri harus disimpan dalam memori seperti penunjuk biasa. Ini memang muncul dalam penjelasan Anda nanti, tapi saya pikir itu menyoroti perbedaan utama dengan lebih baik. (Dalam C modern, nama variabel larik memang merujuk ke satu blok memori, jadi itu bukan perbedaannya. Itu penunjuk itu sendiri tidak secara logis disimpan di mana pun di mesin abstrak.)
Peter Cordes
Lihat keengganan C pada array untuk ringkasan sejarah yang bagus.
Peter Cordes
31

Mengenai operator penugasan, standar C ++ mengatakan yang berikut (C ++ 03 §5.17 / 1):

Ada beberapa operator penugasan ... semua memerlukan nilai l yang dapat dimodifikasi sebagai operan kiri mereka

Array bukanlah nilai l yang dapat dimodifikasi.

Namun, tugas ke objek tipe kelas didefinisikan secara khusus (§5.17 / 4):

Tugas ke objek kelas ditentukan oleh operator tugas salin.

Jadi, kita melihat untuk melihat apa yang dilakukan operator penugasan salinan yang dideklarasikan secara implisit untuk sebuah kelas (§12.8 / 13):

Operator penugasan salinan yang didefinisikan secara implisit untuk kelas X melakukan penugasan sesuai anggota dari subobjeknya. ... Setiap subobjek ditugaskan dengan cara yang sesuai dengan tipenya:
...
- jika subobjek adalah sebuah array, setiap elemen ditetapkan, dengan cara yang sesuai dengan tipe elemen
...

Jadi, untuk objek tipe kelas, array disalin dengan benar. Perhatikan bahwa jika Anda menyediakan operator penugasan salinan yang dideklarasikan pengguna, Anda tidak dapat memanfaatkan ini, dan Anda harus menyalin array elemen demi elemen.


Alasannya serupa di C (C99 §6.5.16 / 2):

Operator penugasan harus memiliki nilai l yang dapat dimodifikasi sebagai operan kirinya.

Dan §6.3.2.1 / 1:

Nilai l yang dapat dimodifikasi adalah nilai l yang tidak memiliki tipe larik ... [kendala lain mengikuti]

Di C, penugasan jauh lebih sederhana daripada di C ++ (§6.5.16.1 / 2):

Dalam tugas sederhana (=), nilai operan kanan diubah ke jenis ekspresi tugas dan menggantikan nilai yang disimpan dalam objek yang ditentukan oleh operan kiri.

Untuk penugasan objek tipe struct, operan kiri dan kanan harus memiliki tipe yang sama, jadi nilai operan kanan cukup disalin ke operan kiri.

James McNellis
sumber
1
Mengapa array tidak berubah? Atau lebih tepatnya, mengapa penugasan tidak didefinisikan secara khusus untuk larik seperti saat itu dalam tipe kelas?
GManNickG
1
@GMan: Itu pertanyaan yang lebih menarik, bukan. Untuk C ++ jawabannya mungkin "karena begitulah di C," dan untuk C, saya kira itu hanya karena cara bahasa berkembang (yaitu, alasannya historis, bukan teknis), tetapi saya tidak hidup ketika sebagian besar terjadi, jadi saya akan menyerahkan kepada seseorang yang lebih berpengetahuan untuk menjawab bagian itu :-P (FWIW, saya tidak dapat menemukan apa pun di dokumen alasan C90 atau C99).
James McNellis
2
Apakah ada yang tahu di mana definisi "lvalue yang dapat dimodifikasi" dalam standar C ++ 03? Ini harus dalam §3.10. Indeks mengatakan itu didefinisikan di halaman itu, tetapi sebenarnya tidak. Catatan (non-normatif) di §8.3.4 / 5 mengatakan "Objek tipe array tidak dapat dimodifikasi, lihat 3.10," tetapi §3.10 tidak sekali menggunakan kata "array."
James McNellis
@ James: Saya hanya melakukan hal yang sama. Sepertinya ini mengacu pada definisi yang dihapus. Dan ya, saya selalu ingin tahu alasan sebenarnya di balik itu semua, tapi sepertinya itu misteri. Saya pernah mendengar hal-hal seperti "cegah orang menjadi tidak efisien dengan menetapkan array secara tidak sengaja", tapi itu konyol.
GManNickG
1
@GMan, James: Baru-baru ini ada diskusi di comp.lang.c ++ groups.google.com/group/comp.lang.c++/browse_frm/thread/… jika Anda melewatkannya dan masih tertarik. Rupanya itu bukan karena larik bukan nilai l yang dapat dimodifikasi (larik pasti adalah nilai l dan semua nilai l non-konst dapat dimodifikasi), tetapi karena =memerlukan nilai r pada RHS dan larik tidak bisa menjadi nilai r ! Konversi lvalue-to-rvalue dilarang untuk array, diganti dengan lvalue-to-pointer. static_casttidak lebih baik dalam membuat nilai r karena didefinisikan dalam istilah yang sama.
Potatoswatter
2

Di tautan ini: http://www2.research.att.com/~bs/bs_faq2.html ada bagian tentang penetapan array:

Dua masalah mendasar dengan array adalah itu

  • sebuah array tidak mengetahui ukurannya sendiri
  • nama sebuah array diubah menjadi sebuah pointer ke elemen pertamanya dengan sedikit provokasi

Dan saya pikir inilah perbedaan mendasar antara array dan struct. Variabel array adalah elemen data tingkat rendah dengan pengetahuan diri yang terbatas. Pada dasarnya, ini adalah bagian dari memori dan cara untuk mengindeksnya.

Jadi, kompilator tidak bisa membedakan antara int a [10] dan int b [20].

Struktur, bagaimanapun, tidak memiliki ambiguitas yang sama.

Scott Turley
sumber
3
Halaman itu berbicara tentang melewatkan array ke fungsi (yang tidak bisa dilakukan, jadi itu hanya sebuah pointer, yang dia maksud ketika dia mengatakan itu kehilangan ukurannya). Itu tidak ada hubungannya dengan menugaskan array ke array. Dan tidak, variabel array bukan hanya "benar-benar" penunjuk ke elemen pertama, itu adalah array. Array bukanlah penunjuk.
GManNickG
Terima kasih atas komentarnya, tetapi ketika saya membaca bagian artikel itu, dia mengatakan di depan bahwa array tidak tahu ukurannya sendiri, kemudian menggunakan contoh di mana array dilewatkan sebagai argumen untuk menggambarkan fakta itu. Jadi, ketika array dilewatkan sebagai argumen, apakah mereka kehilangan informasi tentang ukurannya, atau apakah mereka tidak pernah memiliki informasi untuk memulai. Saya berasumsi yang terakhir.
Scott Turley
3
Kompilator dapat membedakan antara dua larik berukuran berbeda - coba mencetak sizeof(a)vs. sizeof(b)atau meneruskan ake void f(int (&)[20]);.
Georg Fritzsche
Penting untuk dipahami bahwa setiap ukuran larik merupakan tipenya sendiri. Aturan untuk parameter passing memastikan bahwa Anda dapat menulis fungsi "generik" orang miskin yang mengambil argumen larik dengan ukuran berapa pun, dengan mengabaikan keharusan meneruskan ukuran secara terpisah. Jika bukan itu masalahnya (dan dalam C ++ Anda dapat - dan harus! - menentukan parameter referensi ke array dengan ukuran tertentu), Anda memerlukan fungsi khusus untuk setiap ukuran yang berbeda, jelas tidak masuk akal. Saya menulis tentang itu di posting lain .
Peter - Pulihkan Monica
0

Saya tahu, semua yang menjawab adalah pakar C / C ++. Tapi saya pikir, inilah alasan utamanya.

num2 = num1;

Di sini Anda mencoba mengubah alamat dasar larik, yang tidak diizinkan.

dan tentu saja, struct2 = struct1;

Di sini, objek struct1 ditugaskan ke objek lain.

nsivakr.dll
sumber
Dan menetapkan struct pada akhirnya akan menetapkan anggota array, yang memunculkan pertanyaan yang sama persis. Mengapa yang satu diizinkan dan bukan yang lain, jika itu adalah larik dalam kedua situasi?
GManNickG
1
Sepakat. Tetapi yang pertama dicegah oleh kompilator (num2 = num1). Yang kedua tidak dicegah oleh kompilator. Itu membuat perbedaan besar.
nsivakr
Jika array dapat dialihkan, num2 = num1akan berperilaku sangat baik. Elemen-elemen dari num2akan memiliki nilai yang sama dengan elemen yang terkait num1.
juanchopanza
0

Alasan lain tidak ada upaya lebih lanjut dilakukan untuk daging sapi sampai array di C mungkin bahwa array tugas tidak akan yang berguna. Meskipun dapat dengan mudah dicapai di C dengan membungkusnya dalam sebuah struct (dan alamat struct dapat dengan mudah dilemparkan ke alamat array atau bahkan alamat elemen pertama array untuk diproses lebih lanjut) fitur ini jarang digunakan. Salah satu alasannya adalah bahwa array dengan ukuran berbeda tidak kompatibel sehingga membatasi manfaat penugasan atau, terkait, meneruskan fungsi berdasarkan nilai.

Sebagian besar fungsi dengan parameter array dalam bahasa di mana array adalah tipe kelas satu ditulis untuk array dengan ukuran arbitrer. Fungsi ini kemudian biasanya mengulangi jumlah elemen yang diberikan, sebuah informasi yang disediakan oleh array. (Dalam C ungkapannya, tentu saja, untuk melewatkan pointer dan jumlah elemen yang terpisah.) Sebuah fungsi yang menerima array hanya dengan satu ukuran tertentu tidak diperlukan sesering mungkin, jadi tidak banyak yang terlewat. (Ini berubah ketika Anda dapat menyerahkannya ke compiler untuk menghasilkan fungsi terpisah untuk ukuran array apa pun yang terjadi, seperti pada template C ++; inilah alasan mengapa std::arrayberguna.)

Peter - Kembalikan Monica
sumber