Haruskah saya menginisialisasi C struct melalui parameter, atau dengan nilai balik? [Tutup]

33

Perusahaan tempat saya bekerja menginisialisasi semua struktur data mereka melalui fungsi inisialisasi seperti:

//the structure
typedef struct{
  int a,b,c;  
} Foo;

//the initialize function
InitializeFoo(Foo* const foo){
   foo->a = x; //derived here based on other data
   foo->b = y; //derived here based on other data
   foo->c = z; //derived here based on other data
}

//initializing the structure  
Foo foo;
InitializeFoo(&foo);

Saya mendapat sedikit dorongan untuk mencoba menginisialisasi struct saya seperti ini:

//the structure
typedef struct{
  int a,b,c;  
} Foo;

//the initialize function
Foo ConstructFoo(int a, int b, int c){
   Foo foo;
   foo.a = a; //part of parameter input (inputs derived outside of function)
   foo.b = b; //part of parameter input (inputs derived outside of function)
   foo.c = c; //part of parameter input (inputs derived outside of function)
   return foo;
}

//initialize (or construct) the structure
Foo foo = ConstructFoo(x,y,z);

Apakah ada keuntungan satu di atas yang lain?
Yang mana yang harus saya lakukan, dan bagaimana saya membenarkannya sebagai praktik yang lebih baik?

Trevor Hickey
sumber
4
@gnat Ini adalah pertanyaan eksplisit tentang inisialisasi struct. Utas itu mewujudkan beberapa alasan yang sama yang ingin saya lihat diterapkan untuk keputusan desain khusus ini.
Trevor Hickey
2
@ Jeffffrey Kita di C, jadi kita tidak bisa benar-benar memiliki metode. Ini tidak selalu merupakan kumpulan nilai langsung. Terkadang menginisialisasi struct adalah untuk mendapatkan nilai (entah bagaimana), dan melakukan beberapa logika untuk menginisialisasi struct.
Trevor Hickey
1
@ JacquesB Saya mendapat "Setiap komponen yang Anda buat akan berbeda dari yang lain. Ada fungsi Initialize () yang digunakan di tempat lain untuk struct. Secara teknis, menyebutnya konstruktor menyesatkan."
Trevor Hickey
1
@ TrevorHickey InitializeFoo()adalah seorang konstruktor. Satu-satunya perbedaan dari konstruktor C ++ adalah, bahwa thispointer dilewatkan secara eksplisit daripada secara implisit. Kode yang dikompilasi dari InitializeFoo()dan C ++ yang sesuai Foo::Foo()persis sama.
cmaster
2
Opsi yang lebih baik: Berhenti menggunakan C lebih dari C ++. Autowin.
Thomas Eding

Jawaban:

25

Dalam pendekatan ke-2 Anda tidak akan pernah memiliki Foo setengah-diinisialisasi. Menempatkan semua konstruksi di satu tempat tampaknya tempat yang lebih masuk akal, dan jelas.

Tapi ... cara pertama tidak begitu buruk, dan sering digunakan di banyak bidang (bahkan ada diskusi tentang cara terbaik untuk menyuntikkan ketergantungan, baik injeksi properti seperti cara pertama Anda, atau injeksi konstruktor seperti cara kedua). . Tidak ada yang salah.

Jadi jika tidak ada yang salah dan sisa perusahaan menggunakan pendekatan # 1, maka Anda harus menyesuaikan dengan basis kode yang ada dan tidak mencoba mengacaukannya dengan memperkenalkan pola baru. Ini benar-benar faktor paling penting yang dimainkan di sini, bermain baik dengan teman-teman baru Anda dan jangan mencoba menjadi kepingan salju khusus yang melakukan hal-hal berbeda.

gbjbaanb
sumber
Ok, sepertinya masuk akal. Saya mendapat kesan bahwa menginisialisasi objek tanpa bisa melihat input apa yang menginisialisasi itu, akan menyebabkan kebingungan. Saya mencoba mengikuti konsep data dalam / data untuk menghasilkan kode yang dapat diprediksi dan diuji. Melakukannya dengan cara lain sepertinya meningkatkan kopling karena file sumber dari struct saya memerlukan dependensi tambahan untuk melakukan inisialisasi. Tapi kau benar, dalam hal itu aku tidak ingin mengguncang kapal kecuali jika satu cara lebih disukai daripada yang lain.
Trevor Hickey
4
@ TrevorHickey: Sebenarnya saya akan mengatakan ada dua perbedaan utama antara contoh yang Anda berikan - (1) Dalam satu fungsi dilewatkan pointer ke struktur untuk diinisialisasi, dan yang lain mengembalikan struktur diinisialisasi; (2) Dalam satu parameter inisialisasi dilewatkan ke fungsi, dan yang lain mereka implisit. Anda tampaknya bertanya lebih banyak tentang (2), tetapi jawabannya di sini berkonsentrasi pada (1). Anda mungkin ingin mengklarifikasi bahwa - Saya menduga kebanyakan orang akan merekomendasikan hibrida dari keduanya menggunakan parameter eksplisit dan pointer:void SetupFoo(Foo *out, int a, int b, int c)
psmears
1
Bagaimana pendekatan pertama akan mengarah pada Foostruct "setengah diinisialisasi" ? Pendekatan pertama juga melakukan semua inisialisasi di satu tempat. (Atau apakah Anda mempertimbangkan struct yang tidak diinisialisasi Foountuk menjadi "setengah diinisialisasi"?)
jamesdlin
1
@jamesdlin dalam kasus di mana Foo dibuat, dan InitialiseFoo tidak sengaja terlewatkan. Itu hanya kiasan untuk menggambarkan inisialisasi 2 fase tanpa mengetik deskripsi panjang. Saya pikir tipe orang yang berpengalaman akan mengerti.
gbjbaanb
22

Kedua pendekatan bundel kode inisialisasi menjadi panggilan fungsi tunggal. Sejauh ini baik.

Namun, ada dua masalah dengan pendekatan kedua:

  1. Yang kedua tidak benar-benar membangun objek yang dihasilkan, itu menginisialisasi objek lain di stack, yang kemudian disalin ke objek akhir. Inilah mengapa saya akan melihat pendekatan kedua sedikit lebih rendah. Push-back yang Anda terima kemungkinan besar karena salinan yang asing ini.

    Ini bahkan lebih buruk ketika Anda mendapatkan kelas Deriveddari Foo(struct sebagian besar digunakan untuk orientasi objek di C): Dengan pendekatan kedua, fungsi ConstructDerived()akan memanggil ConstructFoo(), salin Fooobjek sementara yang dihasilkan ke dalam slot superclass dari Derivedobjek; menyelesaikan inisialisasi Derivedobjek; hanya untuk memiliki objek yang dihasilkan disalin lagi saat kembali. Tambahkan lapisan ketiga, dan semuanya menjadi sangat konyol.

  2. Dengan pendekatan kedua, ConstructClass()fungsi tidak memiliki akses ke alamat objek yang sedang dibangun. Ini membuat tidak mungkin untuk menghubungkan objek selama konstruksi, karena diperlukan ketika suatu objek perlu mendaftarkan dirinya dengan objek lain untuk panggilan balik.


Akhirnya, tidak semua structskelas penuh. Beberapa structssecara efektif hanya membundel sekelompok variabel bersama-sama, tanpa batasan internal untuk nilai-nilai variabel ini. typedef struct Point { int x, y; } Point;akan menjadi contoh yang baik untuk ini. Untuk penggunaan fungsi inisialisasi ini tampaknya berlebihan. Dalam kasus ini, sintaks literal majemuk mungkin lebih nyaman (C99):

Point = { .x = 7, .y = 9 };

atau

Point foo(...) {
    //other stuff

    return (Point){ .x = n, .y = n*n };
}
cmaster
sumber
5
Saya tidak berpikir salinan akan menjadi masalah karena en.wikipedia.org/wiki/Copy_elision
Trevor Hickey
5
Bahwa kompiler dapat menghilangkan salinan tidak mengurangi fakta bahwa Anda telah menulis salinan. Dalam C, menulis operasi berlebihan dan mengandalkan kompiler untuk memperbaikinya dianggap sebagai stile yang buruk. Ini berbeda dari C ++ di mana orang bangga ketika mereka dapat membuktikan bahwa kompiler secara teoritis dapat menghapus semua cacat yang ditinggalkan oleh templat bersarang mereka. Di C, orang mencoba menulis kode persis yang harus dijalankan mesin. Lagi pula, intinya tentang alamat yang tidak dapat diakses tetap ada, salin elision tidak dapat membantu Anda di sana.
cmaster
3
Siapa pun yang menggunakan kompiler harus mengharapkan kode yang mereka tulis diubah oleh kompiler. Kecuali jika mereka menjalankan interpreter perangkat keras C, kode yang mereka tulis tidak akan menjadi kode yang mereka jalankan, bahkan jika mudah untuk percaya sebaliknya. Jika mereka mengerti kompiler mereka, mereka akan memahami elision, dan itu tidak berbeda dengan int x = 3;tidak menyimpan string xdalam biner. Alamat dan poin warisan bagus; anggapan kegagalan elisi itu bodoh.
Yakk
@ Yakk: Secara historis, C diciptakan untuk melayani sebagai bentuk bahasa assembly tingkat tinggi untuk pemrograman sistem. Pada tahun-tahun sejak itu, identitasnya menjadi semakin keruh. Beberapa orang ingin itu menjadi bahasa aplikasi yang dioptimalkan, tetapi karena tidak ada bentuk bahasa assembly tingkat tinggi yang lebih baik telah muncul, C masih diperlukan untuk melayani peran yang terakhir. Saya melihat tidak ada yang salah dengan gagasan bahwa kode program yang ditulis dengan baik harus berperilaku setidaknya dengan sopan bahkan ketika dikompilasi dengan optimasi minimal, meskipun untuk membuat itu benar-benar bekerja akan membutuhkan C menambahkan beberapa hal yang telah lama kekurangan.
supercat
@ Yakk: Memiliki arahan, misalnya, yang akan memberi tahu kompiler "Variabel-variabel berikut dapat disimpan dengan aman di register selama rangkaian kode berikut" serta sarana untuk menyalin-blok suatu jenis selain dari yang unsigned charakan memungkinkan optimasi yang mana Aturan Aliasing yang Ketat tidak akan cukup, sementara secara bersamaan membuat harapan programmer lebih jelas.
supercat
1

Bergantung pada isi struktur dan kompiler tertentu yang digunakan, pendekatan mana pun bisa lebih cepat. Pola tipikal adalah bahwa struktur yang memenuhi kriteria tertentu dapat dikembalikan dalam register; untuk fungsi yang mengembalikan tipe struktur lain, penelepon diharuskan mengalokasikan ruang untuk struktur sementara di suatu tempat (biasanya di stack) dan meneruskan alamatnya sebagai parameter "tersembunyi"; dalam kasus di mana fungsi kembali disimpan secara langsung ke variabel lokal yang alamatnya tidak dipegang oleh kode luar, beberapa kompiler mungkin dapat melewati alamat variabel itu secara langsung.

Jika tipe struktur memenuhi persyaratan implementasi tertentu untuk dikembalikan dalam register (mis. Tidak lebih dari satu kata mesin, atau mengisi tepat dua kata mesin) memiliki fungsi mengembalikan struktur mungkin lebih cepat daripada melewati alamat struktur, terutama karena mengekspos alamat variabel ke kode luar yang mungkin menyimpan salinannya bisa menghalangi beberapa optimasi yang bermanfaat. Jika tipe tidak memenuhi persyaratan seperti itu, kode yang dihasilkan untuk fungsi yang mengembalikan struct akan sama dengan yang untuk fungsi yang menerima pointer tujuan; kode panggilan kemungkinan akan lebih cepat untuk formulir yang mengambil pointer, tetapi formulir itu kehilangan beberapa peluang optimasi.

Sayang sekali C tidak menyediakan sarana untuk mengatakan bahwa suatu fungsi dilarang menyimpan salinan dari pass-in pointer (semantik mirip dengan referensi C ++) karena melewati pointer terbatas seperti itu akan mendapatkan keuntungan kinerja langsung dari melewatkan pointer ke objek yang sudah ada sebelumnya, tetapi pada saat yang sama menghindari biaya semantik yang memerlukan kompiler untuk mempertimbangkan alamat variabel "terbuka".

supercat
sumber
3
Ke poin terakhir Anda: Tidak ada dalam C ++ untuk menghentikan fungsi menjaga salinan pointer yang diteruskan sebagai referensi, fungsi hanya dapat mengambil alamat objek. Juga bebas menggunakan referensi untuk membangun objek lain yang berisi referensi itu (tidak ada pointer telanjang yang dibuat). Namun demikian, baik salinan pointer atau referensi di objek dapat hidup lebih lama dari objek yang mereka tuju, menciptakan pointer / referensi menggantung. Jadi poin tentang keamanan referensi cukup bisu.
cmaster
@ cmaster: Pada platform yang mengembalikan struktur dengan melewatkan pointer ke penyimpanan sementara, kompiler C tidak menyediakan fungsi yang disebut dengan cara apa pun untuk mengakses alamat penyimpanan itu. Dalam C ++, dimungkinkan untuk mendapatkan alamat variabel yang dilewatkan oleh referensi, tetapi kecuali jika penelepon menjamin masa berlaku item yang lewat (dalam hal ini biasanya akan melewati pointer) Perilaku Tidak Terdefinisi kemungkinan akan menghasilkan.
supercat
1

Salah satu argumen yang mendukung gaya "output-parameter" adalah bahwa ia memungkinkan fungsi untuk mengembalikan kode kesalahan.

struct MyStruct {
    int x;
    char *y;
    // ...
};

int MyStruct_init(struct MyStruct *out) {
    // ...
    char *c = malloc(n);
    if (!c) {
        return -1;
    }
    out->y = c;
    return 0;  // Success!
}

Mempertimbangkan beberapa set struct terkait, jika inisialisasi dapat gagal untuk salah satu dari mereka, mungkin ada baiknya jika mereka semua menggunakan style out-parameter untuk konsistensi.

Viktor Dahl
sumber
1
Padahal orang hanya bisa mengatur errno.
Deduplicator
0

Saya berasumsi bahwa fokus Anda adalah pada inisialisasi melalui parameter output vs inisialisasi melalui kembali, bukan perbedaan dalam bagaimana argumen konstruksi disediakan.

Perhatikan bahwa pendekatan pertama bisa Foomenjadi buram (meskipun tidak dengan cara Anda menggunakannya saat ini), dan itu biasanya diinginkan untuk pemeliharaan jangka panjang. Anda dapat mempertimbangkan, misalnya, fungsi yang mengalokasikan struktur buram Footanpa menginisialisasi. Atau mungkin Anda perlu menginisialisasi ulang Foostruct yang sebelumnya diinisialisasi dengan nilai yang berbeda.

jamesdlin
sumber
Downvoter, mau jelaskan? Apakah sesuatu yang saya katakan secara faktual salah?
jamesdlin