Mengapa banyak fungsi yang mengembalikan struktur dalam C, sebenarnya mengembalikan pointer ke struktur?

49

Apa keuntungan dari mengembalikan pointer ke struktur sebagai lawan mengembalikan seluruh struktur dalam returnpernyataan fungsi?

Saya berbicara tentang fungsi seperti fopendan 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 NULLpointer.

Mengembalikan struktur penuh yang NULLakan lebih sulit saya kira atau kurang efisien. Apakah ini alasan yang sah?

yoyo_fun
sumber
10
@ JohnR.Strohm saya mencobanya dan ternyata berfungsi. Suatu fungsi dapat mengembalikan struct .... Jadi apa alasannya tidak dilakukan?
yoyo_fun
28
Pra-standardisasi C tidak memungkinkan struct untuk disalin atau diteruskan oleh nilai. Pustaka standar C memiliki banyak ketidaksesuaian dari era itu yang tidak akan ditulis seperti itu hari ini, misalnya dibutuhkan hingga C11 untuk fungsi yang benar-benar salah dirancang gets()untuk dihapus. Beberapa programmer masih memiliki keengganan untuk menyalin struct, kebiasaan lama sulit.
amon
26
FILE*secara efektif merupakan pegangan yang buram. Kode pengguna seharusnya tidak peduli apa struktur internalnya.
CodesInChaos
3
Kembali dengan referensi hanya merupakan standar yang wajar ketika Anda memiliki pengumpulan sampah.
Idan Arye
7
@ JohnR.Strohm "sangat senior" di profil Anda tampaknya akan kembali sebelum 1989 ;-) - ketika ANSI C mengizinkan apa yang tidak dilakukan K&R C: Menyalin struktur dalam penugasan, melewati parameter dan mengembalikan nilai. Buku asli K&R memang dinyatakan secara eksplisit (saya parafrase): "Anda dapat melakukan dua hal dengan struktur, mengambil alamatnya & dan mengakses anggota .."
Peter - Pasang kembali Monica

Jawaban:

61

Ada beberapa alasan praktis mengapa fungsi seperti fopenpointer kembali ke bukan jenis contoh struct:

  1. Anda ingin menyembunyikan representasi structtipe dari pengguna;
  2. Anda mengalokasikan objek secara dinamis;
  3. Anda merujuk ke satu instance objek melalui beberapa referensi;

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 sementara FILEsering sering diimplementasikan sebagai structjenis, tidak memiliki untuk menjadi).

Jadi, Anda dapat mengekspos jenis yang tidak lengkap struct di header di suatu tempat:

typedef struct __some_internal_stream_implementation FILE;

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 melalui fopen, freopen, dll, tapi aku tidak bisa langsung memanipulasi objek itu menunjuk ke.

Mungkin juga fopenfungsi tersebut mengalokasikan FILEobjek secara dinamis, menggunakan mallocatau serupa. Dalam hal ini, masuk akal untuk mengembalikan pointer.

Akhirnya, mungkin Anda menyimpan semacam keadaan di suatu structobjek, dan Anda perlu membuat keadaan itu tersedia di beberapa tempat berbeda. Jika Anda mengembalikan instance dari structtipe 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.

John Bode
sumber
31
Keuntungan khusus menggunakan pointer sebagai jenis buram adalah bahwa struktur itu sendiri dapat berubah di antara versi pustaka dan Anda tidak perlu mengkompilasi ulang penelepon.
Barmar
6
@Barmar: Memang, ABI Stabilitas adalah yang titik penjualan besar C, dan itu tidak akan stabil tanpa pointer buram.
Matthieu M.
37

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.

Mason Wheeler
sumber
54
Kelemahan pengembalian dengan penunjuk: sekarang Anda harus melacak kepemilikan objek itu dan mungkin membebaskannya. Juga, pointer tipuan mungkin lebih mahal daripada salinan cepat. Ada banyak variabel di sini, jadi menggunakan pointer tidak secara universal lebih baik.
amon
17
Juga, pointer hari ini adalah 64 bit pada kebanyakan platform desktop dan server. Saya telah melihat lebih dari beberapa struct dalam karir saya yang akan muat dalam 64 bit. Jadi, Anda tidak bisa selalu mengatakan bahwa menyalin sebuah pointer lebih murah daripada menyalin sebuah struct.
Solomon Slow
37
Ini sebagian besar merupakan jawaban yang baik, tetapi saya tidak setuju tentang bagian itu kadang-kadang, sangat jarang, ini yang Anda inginkan, tetapi sebagian besar waktu itu tidak - justru sebaliknya. Mengembalikan pointer memungkinkan beberapa jenis efek samping yang tidak diinginkan, dan beberapa jenis cara jahat untuk membuat kepemilikan pointer salah. Dalam kasus di mana waktu CPU tidak begitu penting, saya lebih suka varian salin, jika itu pilihan, itu jauh lebih rentan kesalahan.
Doc Brown
6
Perlu dicatat bahwa ini benar-benar hanya berlaku untuk API eksternal. Untuk fungsi-fungsi internal, setiap kompiler yang sedikit pun kompeten dalam dekade terakhir akan menulis ulang fungsi yang mengembalikan struct besar untuk mengambil pointer sebagai argumen tambahan dan membangun objek langsung di sana. Argumen immutable vs bisa berubah sudah cukup sering dilakukan, tapi saya pikir kita semua bisa setuju bahwa klaim bahwa struktur data tidak berubah hampir tidak pernah apa yang Anda inginkan tidak benar.
Voo
6
Anda juga bisa menyebutkan kompilasi tembok api sebagai pro untuk pointer. Dalam program besar dengan header yang banyak dibagikan, tipe yang tidak lengkap dengan fungsi mencegah keharusan mengkompilasi ulang setiap kali detail implementasi berubah. Perilaku kompilasi yang lebih baik sebenarnya adalah efek samping dari enkapsulasi yang dicapai ketika antarmuka dan implementasi dipisahkan. Mengembalikan (dan melewati, menetapkan) berdasarkan nilai memerlukan informasi implementasi.
Peter - Reinstate Monica
12

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, fopenmengembalikan hanya satu data (yang dibuka FILE*) dan jika terjadi kesalahan, berikan kode kesalahan melalui errnovariabel pseudo-global. Tapi mungkin akan lebih baik untuk mengembalikan structdua anggota: FILE*pegangan, dan kode kesalahan (yang akan ditetapkan jika pegangan file NULL). Untuk alasan historis, bukan itu masalahnya (dan kesalahan dilaporkan melalui errnoglobal, 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 structdari 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 structbisa lebih mudah dibaca, lebih ramah thread, dan lebih efisien.

Basile Starynkevitch
sumber
Sebenarnya struct kecil dimasukkan ke dalamnyardx:rax . Jadi struct foo { int a,b; };dikembalikan ke dalam rax(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.
Peter Cordes
Terkait: bugs.llvm.org/show_bug.cgi?id=34840 std::optional<int> mengembalikan boolean di bagian atas rax, jadi Anda memerlukan konstanta masker 64-bit untuk mengujinya test. Atau Anda bisa menggunakannya bt. Tapi itu menyebalkan bagi penelepon dan callee dibandingkan dengan menggunakan dl, 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)
Peter Cordes
@PeterCordes: hal-hal terkait Anda adalah C ++, bukan C
Basile Starynkevitch
Ups, benar. Nah hal yang sama akan berlaku persis ke 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.
Peter Cordes
1
Contoh klasikdiv_t div()
chux
6

Anda berada di jalur yang benar

Kedua alasan yang Anda sebutkan valid:

Salah satu alasan saya berpikir bahwa ini akan menjadi keuntungan untuk mengembalikan pointer ke struktur adalah untuk dapat memberi tahu lebih mudah jika fungsi gagal dengan mengembalikan pointer NULL.

Mengembalikan struktur LENGKAP yang NULL akan lebih sulit saya kira atau kurang efisien. Apakah ini alasan yang sah?

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:

struct s1 {
   int integer;
};

struct s1 f(struct s1 input){
   struct s1 returnValue = xinput
   return returnValue;
}

int main(void){
   struct s1 a = { 42 };
   struct s1 b= f(a);

   return 0;
}
Ryan
sumber
Bagaimana mungkin untuk tidak mengetahui berapa banyak memori yang diperlukan oleh variabel tertentu jika Anda sudah menentukan tipe struct?
yoyo_fun
9
@JenniferAnderson C memiliki konsep tipe tidak lengkap: nama tipe dapat dideklarasikan tetapi belum didefinisikan, sehingga ukurannya tidak tersedia. Saya tidak bisa mendeklarasikan variabel tipe itu, tetapi bisa mendeklarasikan pointer ke tipe itu, misalnya struct incomplete* foo(void). Dengan begitu saya bisa mendeklarasikan fungsi dalam header, tetapi hanya mendefinisikan struct dalam file C, sehingga memungkinkan untuk enkapsulasi.
amon
@amon Jadi ini adalah bagaimana mendeklarasikan fungsi header (prototipe / tanda tangan) sebelum menyatakan bagaimana mereka bekerja sebenarnya dilakukan di C? Dan dimungkinkan untuk melakukan hal yang sama pada struktur dan serikat pekerja di C
yoyo_fun
@JenniferAnderson Anda mendeklarasikan prototipe fungsi (fungsi tanpa badan) dalam file header dan kemudian dapat memanggil fungsi-fungsi tersebut dalam kode lain, tanpa mengetahui isi fungsi, karena kompiler hanya perlu mengetahui cara mengatur argumen, dan bagaimana menerima nilai pengembalian. Pada saat Anda menautkan program, Anda sebenarnya harus mengetahui definisi fungsi (yaitu dengan tubuh), tetapi Anda hanya perlu memprosesnya sekali saja. Jika Anda menggunakan tipe non-sederhana, itu juga perlu mengetahui struktur tipe itu, tetapi pointer sering memiliki ukuran yang sama dan tidak masalah untuk penggunaan prototipe.
simpleuser
6

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 memanggil fopen, umumnya program tidak akan peduli dengan isi dari struktur yang dikembalikan - semua yang akan dipedulikan adalah bahwa fungsi-fungsi lain seperti freadakan 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 ke freadharus dapat memperbarui informasi itu. Setelah freadmenerima pointer ke FILEmembuatnya mudah. Jika freadsebaliknya menerima FILE, itu tidak akan memiliki cara untuk memperbarui FILEobjek yang dipegang oleh penelepon.

supercat
sumber
3

Menyembunyikan Informasi

Apa keuntungan dari mengembalikan pointer ke struktur sebagai lawan mengembalikan seluruh struktur dalam pernyataan kembali fungsi?

Yang paling umum adalah penyembunyian informasi . C tidak memiliki, katakanlah, kemampuan untuk membuat bidang structpribadi, 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. Definisi FILEmaka hanya akan terlihat oleh mereka yang melaksanakan operasi yang memerlukan definisi, seperti fopen, 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

Mengembalikan struktur penuh yang NULL akan lebih sulit saya kira atau kurang efisien. Apakah ini alasan yang sah?

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) terkait FILE.

Itu bukan alasan yang valid setidaknya dari sudut pandang kinerja untuk membuat fopenpengembalian pointer karena satu-satunya alasan itu akan kembali NULLadalah 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 memungkinkan NULLdikembalikan pada beberapa kondisi pasca.

Untuk operasi file, overhead relatif cukup sepele dibandingkan dengan operasi file itu sendiri, dan manual perlu fclosetidak dapat dihindari. Jadi tidak seperti kita dapat menyelamatkan klien dari kesulitan membebaskan (menutup) sumber daya dengan mengekspos definisi FILEdan mengembalikannya dengan nilai fopenatau 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 mallocdan 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:

struct Foo
{
   /* priv_* indicates that you shouldn't tamper with these fields! */
   int priv_internal_field;
   int priv_other_one;
};

struct Foo foo_create(void);
void foo_destroy(struct Foo* foo);
void foo_something(struct Foo* foo);

Jika ada masalah kompatibilitas biner di masa depan, maka saya merasa cukup baik untuk hanya menyediakan ruang ekstra untuk keperluan di masa mendatang, seperti:

struct Foo
{
   /* priv_* indicates that you shouldn't tamper with these fields! */
   int priv_internal_field;
   int priv_other_one;

   /* reserved for possible future uses (emergency backup plan).
     currently just set to null. */
   void* priv_reserved;
};

Ruang yang dipesan itu agak boros tetapi bisa menjadi penyelamat jika kita menemukan di masa depan bahwa kita perlu menambahkan lebih banyak data Footanpa 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:

int foo_create(struct Foo* foo);
...
/* In the client code: */
struct Foo foo;
if (foo_create(&foo))
{
    foo_something(&foo);
    foo_destroy(&foo);
}

... untuk memulai Foodari tumpukan tanpa kemungkinan salinan yang berlebihan. Atau klien bahkan memiliki kebebasan untuk mengalokasikan Foopada heap jika mereka ingin karena suatu alasan.


sumber