Apa keuntungan dari mengembalikan pointer ke struktur sebagai lawan mengembalikan seluruh struktur dalam return
pernyataan fungsi?
Saya berbicara tentang fungsi seperti fopen
dan fungsi level rendah lainnya, tetapi mungkin ada fungsi level yang lebih tinggi yang mengembalikan pointer ke struktur juga.
Saya percaya bahwa ini lebih merupakan pilihan desain daripada hanya pertanyaan pemrograman dan saya ingin tahu lebih banyak tentang kelebihan dan kekurangan dari dua metode.
Salah satu alasan saya pikir itu akan menjadi keuntungan untuk mengembalikan pointer ke struktur adalah untuk dapat memberi tahu lebih mudah jika fungsi gagal dengan mengembalikan NULL
pointer.
Mengembalikan struktur penuh yang NULL
akan lebih sulit saya kira atau kurang efisien. Apakah ini alasan yang sah?
sumber
gets()
untuk dihapus. Beberapa programmer masih memiliki keengganan untuk menyalin struct, kebiasaan lama sulit.FILE*
secara efektif merupakan pegangan yang buram. Kode pengguna seharusnya tidak peduli apa struktur internalnya.&
dan mengakses anggota.
."Jawaban:
Ada beberapa alasan praktis mengapa fungsi seperti
fopen
pointer kembali ke bukan jenis contohstruct
:struct
tipe dari pengguna;Dalam hal tipe suka
FILE *
, itu karena Anda tidak ingin mengekspos detail representasi tipe ke pengguna -FILE *
objek berfungsi sebagai pegangan buram, dan Anda hanya meneruskan pegangan itu ke berbagai rutinitas I / O (dan sementaraFILE
sering sering diimplementasikan sebagaistruct
jenis, tidak memiliki untuk menjadi).Jadi, Anda dapat mengekspos jenis yang tidak lengkap
struct
di header di suatu tempat:Meskipun Anda tidak dapat mendeklarasikan instance dari tipe yang tidak lengkap, Anda bisa mendeklarasikan pointer ke situ. Jadi saya bisa membuat
FILE *
dan menetapkan untuk itu melaluifopen
,freopen
, dll, tapi aku tidak bisa langsung memanipulasi objek itu menunjuk ke.Mungkin juga
fopen
fungsi tersebut mengalokasikanFILE
objek secara dinamis, menggunakanmalloc
atau serupa. Dalam hal ini, masuk akal untuk mengembalikan pointer.Akhirnya, mungkin Anda menyimpan semacam keadaan di suatu
struct
objek, dan Anda perlu membuat keadaan itu tersedia di beberapa tempat berbeda. Jika Anda mengembalikan instance daristruct
tipe tersebut, instance tersebut akan menjadi objek yang terpisah dalam memori satu sama lain, dan pada akhirnya akan keluar dari sinkronisasi. Dengan mengembalikan pointer ke objek tunggal, semua orang merujuk ke objek yang sama.sumber
Ada dua cara "mengembalikan struktur." Anda dapat mengembalikan salinan data, atau Anda dapat mengembalikan referensi (penunjuk) ke sana. Biasanya lebih disukai untuk mengembalikan (dan menyebarkan secara umum) sebuah pointer, karena beberapa alasan.
Pertama, menyalin struktur membutuhkan waktu CPU lebih banyak daripada menyalin pointer. Jika ini sesuatu yang sering dilakukan kode Anda, ini dapat menyebabkan perbedaan kinerja yang nyata.
Kedua, tidak peduli berapa kali Anda menyalin pointer, masih menunjuk ke struktur yang sama dalam memori. Semua modifikasi akan tercermin pada struktur yang sama. Tetapi jika Anda menyalin struktur itu sendiri, dan kemudian membuat modifikasi, perubahan hanya muncul pada salinan itu . Kode apa pun yang memiliki salinan berbeda tidak akan melihat perubahan. Terkadang, sangat jarang, ini adalah apa yang Anda inginkan, tetapi sebagian besar waktu tidak, dan itu dapat menyebabkan bug jika Anda salah.
sumber
Selain jawaban lain, kadang-kadang mengembalikan yang kecil
struct
dengan nilai bermanfaat. Misalnya, seseorang dapat mengembalikan pasangan satu data, dan beberapa kode kesalahan (atau keberhasilan) yang terkait dengannya.Untuk mengambil contoh,
fopen
mengembalikan hanya satu data (yang dibukaFILE*
) dan jika terjadi kesalahan, berikan kode kesalahan melaluierrno
variabel pseudo-global. Tapi mungkin akan lebih baik untuk mengembalikanstruct
dua anggota:FILE*
pegangan, dan kode kesalahan (yang akan ditetapkan jika pegangan fileNULL
). Untuk alasan historis, bukan itu masalahnya (dan kesalahan dilaporkan melaluierrno
global, yang saat ini adalah makro).Perhatikan bahwa bahasa Go memiliki notasi yang bagus untuk mengembalikan dua (atau beberapa) nilai.
Perhatikan juga, bahwa di Linux / x86-64 konvensi ABI dan pemanggilan (lihat halaman x86-psABI ) menetapkan bahwa a
struct
dari dua anggota skalar (misal, pointer dan integer, atau dua pointer, atau dua integer) dikembalikan melalui dua register (dan ini sangat efisien dan tidak melalui memori).Jadi dalam kode C baru, mengembalikan C kecil
struct
bisa lebih mudah dibaca, lebih ramah thread, dan lebih efisien.sumber
rdx:rax
. Jadistruct foo { int a,b; };
dikembalikan ke dalamrax
(misalnya dengan shift / atau), dan harus dibongkar dengan shift / mov. Berikut ini contoh di Godbolt . Tetapi x86 dapat menggunakan register 32-bit rendah 32-bit untuk operasi 32-bit tanpa peduli dengan bit-bit tinggi, jadi itu selalu terlalu buruk, tapi jelas lebih buruk daripada menggunakan 2 register sebagian besar waktu untuk struct 2-anggota.std::optional<int>
mengembalikan boolean di bagian atasrax
, jadi Anda memerlukan konstanta masker 64-bit untuk mengujinyatest
. Atau Anda bisa menggunakannyabt
. Tapi itu menyebalkan bagi penelepon dan callee dibandingkan dengan menggunakandl
, yang kompiler harus lakukan untuk fungsi "pribadi". Juga terkait: libstdc ++std::optional<T>
tidak dapat disalin secara trivial bahkan ketika T, sehingga selalu kembali melalui pointer tersembunyi: stackoverflow.com/questions/46544019/… . (libc ++'s mudah ditiru)struct { int a; _Bool b; };
dalam C, jika penelepon ingin menguji boolean, karena struct C ++ yang dapat disalin secara sepele menggunakan ABI yang sama dengan C.div_t div()
Anda berada di jalur yang benar
Kedua alasan yang Anda sebutkan valid:
Jika Anda memiliki tekstur (misalnya) di suatu tempat di memori, dan Anda ingin referensi tekstur itu di beberapa tempat di program Anda; tidak bijaksana untuk membuat salinan setiap kali Anda ingin referensi itu. Sebaliknya, jika Anda hanya membagikan pointer untuk referensi tekstur, program Anda akan berjalan lebih cepat.
Alasan terbesarnya adalah alokasi memori dinamis. Sering kali, ketika sebuah program dikompilasi, Anda tidak yakin persis berapa banyak memori yang Anda butuhkan untuk struktur data tertentu. Ketika ini terjadi, jumlah memori yang perlu Anda gunakan akan ditentukan saat runtime. Anda dapat meminta memori menggunakan 'malloc' dan kemudian membebaskannya setelah Anda selesai menggunakan 'gratis'.
Contoh yang baik dari ini adalah membaca dari file yang ditentukan oleh pengguna. Dalam hal ini, Anda tidak tahu seberapa besar file itu ketika Anda menyusun program. Anda hanya dapat mengetahui berapa banyak memori yang Anda butuhkan saat program benar-benar berjalan.
Baik malloc dan pointer pengembalian gratis ke lokasi dalam memori. Jadi fungsi yang memanfaatkan alokasi memori dinamis akan mengembalikan pointer ke tempat mereka telah membuat struktur mereka dalam memori.
Juga, di komentar saya melihat bahwa ada pertanyaan, apakah Anda dapat mengembalikan struct dari suatu fungsi. Anda memang bisa melakukan ini. Berikut ini harus bekerja:
sumber
struct incomplete* foo(void)
. Dengan begitu saya bisa mendeklarasikan fungsi dalam header, tetapi hanya mendefinisikan struct dalam file C, sehingga memungkinkan untuk enkapsulasi.Sesuatu seperti
FILE*
bukan benar-benar pointer ke struktur sejauh menyangkut kode klien, tetapi sebaliknya merupakan bentuk pengidentifikasi buram yang terkait dengan beberapa entitas lain seperti file. Ketika sebuah program memanggilfopen
, umumnya program tidak akan peduli dengan isi dari struktur yang dikembalikan - semua yang akan dipedulikan adalah bahwa fungsi-fungsi lain sepertifread
akan melakukan apa pun yang perlu mereka lakukan dengannya.Jika pustaka standar menyimpan di dalam
FILE*
informasi tentang misalnya posisi baca saat ini dalam file itu, panggilan kefread
harus dapat memperbarui informasi itu. Setelahfread
menerima pointer keFILE
membuatnya mudah. Jikafread
sebaliknya menerimaFILE
, itu tidak akan memiliki cara untuk memperbaruiFILE
objek yang dipegang oleh penelepon.sumber
Menyembunyikan Informasi
Yang paling umum adalah penyembunyian informasi . C tidak memiliki, katakanlah, kemampuan untuk membuat bidang
struct
pribadi, apalagi memberikan metode untuk mengaksesnya.Jadi, jika Anda ingin secara paksa mencegah pengembang tidak dapat melihat dan merusak konten pointee, seperti
FILE
, maka satu-satunya cara adalah mencegah mereka dari terkena definisi dengan memperlakukan pointer sebagai buram yang ukuran pointee-nya dan ukurannya. definisi tidak diketahui dunia luar. DefinisiFILE
maka hanya akan terlihat oleh mereka yang melaksanakan operasi yang memerlukan definisi, sepertifopen
, sementara hanya deklarasi struktur akan terlihat oleh header publik.Kompatibilitas Biner
Menyembunyikan definisi struktur juga dapat membantu menyediakan ruang bernapas untuk menjaga kompatibilitas biner dalam API dylib. Hal ini memungkinkan pelaksana perpustakaan untuk mengubah bidang dalam struktur buram tanpa memutus kompatibilitas biner dengan mereka yang menggunakan perpustakaan, karena sifat kode mereka hanya perlu tahu apa yang dapat mereka lakukan dengan struktur, bukan seberapa besar itu atau bidang apa memiliki.
Sebagai contoh, saya benar-benar dapat menjalankan beberapa program kuno yang dibangun selama era Windows 95 hari ini (tidak selalu sempurna, tetapi ternyata masih banyak yang berfungsi). Kemungkinannya adalah beberapa kode untuk binari kuno itu menggunakan pointer buram ke struktur yang ukuran dan isinya telah berubah dari era Windows 95. Namun program terus bekerja di versi windows baru karena mereka tidak terpapar dengan isi struktur tersebut. Ketika bekerja di perpustakaan di mana kompatibilitas biner penting, apa yang klien tidak terpapar pada umumnya diizinkan untuk berubah tanpa merusak kompatibilitas ke belakang.
Efisiensi
Ini biasanya kurang efisien dengan asumsi tipe tersebut praktis dapat ditampung dan dialokasikan pada stack kecuali jika biasanya ada pengalokasi memori yang jauh lebih umum digunakan di belakang layar daripada
malloc
, seperti memori pooling pengalokasi berukuran berukuran lebih daripada variabel yang sudah dialokasikan. Ini merupakan trade-off keselamatan dalam kasus ini, kemungkinan besar, untuk memungkinkan pengembang perpustakaan untuk mempertahankan invarian (jaminan konseptual) terkaitFILE
.Itu bukan alasan yang valid setidaknya dari sudut pandang kinerja untuk membuat
fopen
pengembalian pointer karena satu-satunya alasan itu akan kembaliNULL
adalah kegagalan untuk membuka file. Itu akan mengoptimalkan skenario luar biasa dengan imbalan memperlambat semua jalur eksekusi kasus umum. Mungkin ada alasan produktivitas yang valid dalam beberapa kasus untuk membuat desain lebih mudah untuk membuatnya kembali pointer untuk memungkinkanNULL
dikembalikan pada beberapa kondisi pasca.Untuk operasi file, overhead relatif cukup sepele dibandingkan dengan operasi file itu sendiri, dan manual perlu
fclose
tidak dapat dihindari. Jadi tidak seperti kita dapat menyelamatkan klien dari kesulitan membebaskan (menutup) sumber daya dengan mengekspos definisiFILE
dan mengembalikannya dengan nilaifopen
atau mengharapkan banyak peningkatan kinerja mengingat biaya relatif dari operasi file sendiri untuk menghindari alokasi tumpukan .Hotspot dan Perbaikan
Namun untuk kasus lain, saya telah membuat banyak kode C yang sia-sia dalam basis kode lama dengan hotspot
malloc
dan cache wajib yang tidak perlu karena penggunaan praktik ini terlalu sering dengan pointer buram dan mengalokasikan terlalu banyak hal secara sia-sia di tumpukan, kadang-kadang di loop besar.Praktik alternatif yang saya gunakan sebagai gantinya adalah mengekspos definisi struktur, bahkan jika klien tidak bermaksud merusaknya, dengan menggunakan standar konvensi penamaan untuk berkomunikasi bahwa tidak ada orang lain yang boleh menyentuh bidang:
Jika ada masalah kompatibilitas biner di masa depan, maka saya merasa cukup baik untuk hanya menyediakan ruang ekstra untuk keperluan di masa mendatang, seperti:
Ruang yang dipesan itu agak boros tetapi bisa menjadi penyelamat jika kita menemukan di masa depan bahwa kita perlu menambahkan lebih banyak data
Foo
tanpa merusak biner yang menggunakan perpustakaan kita.Menurut pendapat saya, penyembunyian informasi dan kompatibilitas biner biasanya merupakan satu-satunya alasan yang layak untuk hanya mengijinkan alokasi tumpukan struktur di samping struct variabel-panjang (yang akan selalu membutuhkannya, atau setidaknya menjadi sedikit canggung untuk digunakan sebaliknya jika klien harus mengalokasikan memori pada tumpukan dengan cara VLA untuk mengalokasikan VLS). Bahkan struct besar seringkali lebih murah untuk dikembalikan dengan nilai jika itu berarti perangkat lunak bekerja lebih banyak dengan memori panas pada stack. Dan bahkan jika mereka tidak lebih murah untuk kembali berdasarkan nilai pada kreasi, orang bisa melakukan ini:
... untuk memulai
Foo
dari tumpukan tanpa kemungkinan salinan yang berlebihan. Atau klien bahkan memiliki kebebasan untuk mengalokasikanFoo
pada heap jika mereka ingin karena suatu alasan.sumber