size_t vs uintptr_t

246

Standar C menjamin itu size_tadalah tipe yang dapat menampung indeks array apa pun. Ini berarti bahwa, secara logis, size_tharus dapat menahan semua jenis pointer. Saya telah membaca di beberapa situs yang saya temukan di Google bahwa ini legal dan / atau harus selalu berfungsi:

void *v = malloc(10);
size_t s = (size_t) v;

Jadi di C99, standar memperkenalkan intptr_tdan uintptr_tjenis, yang ditandatangani dan jenis yang tidak ditandatangani dijamin untuk dapat menahan pointer:

uintptr_t p = (size_t) v;

Jadi apa perbedaan antara menggunakan size_tdan uintptr_t? Keduanya tidak ditandatangani, dan keduanya harus dapat menahan jenis pointer apa pun, sehingga keduanya tampak identik secara fungsional. Apakah ada alasan kuat yang nyata untuk menggunakan uintptr_t(atau lebih baik, a void *) daripada a size_t, selain kejelasan? Dalam struktur buram, di mana lapangan hanya akan ditangani oleh fungsi internal, adakah alasan untuk tidak melakukan ini?

Dengan cara yang sama, ptrdiff_ttelah menjadi tipe bertanda yang mampu menahan perbedaan pointer, dan karenanya mampu menahan sebagian besar pointer, jadi bagaimana bedanya intptr_t?

Bukankah semua tipe ini pada dasarnya melayani versi yang berbeda dari fungsi yang sama? Jika tidak, mengapa? Apa yang tidak bisa saya lakukan dengan salah satu dari mereka yang saya tidak bisa lakukan dengan yang lain? Jika demikian, mengapa C99 menambahkan dua jenis dasarnya tidak berguna untuk bahasa?

Saya bersedia untuk mengabaikan fungsi pointer, karena mereka tidak berlaku untuk masalah saat ini, tetapi jangan ragu untuk menyebutkannya, karena saya memiliki kecurigaan menyelinap mereka akan menjadi pusat jawaban "benar".

Chris Lutz
sumber

Jawaban:

236

size_tadalah tipe yang bisa menampung indeks array apa pun. Ini berarti bahwa, secara logis, size_t harus dapat menampung semua jenis pointer

Belum tentu! Kembalilah ke masa arsitektur 16-bit tersegmentasi misalnya: sebuah array mungkin terbatas pada satu segmen (jadi 16-bit size_takan melakukannya) TETAPI Anda dapat memiliki beberapa segmen (jadi intptr_tdibutuhkan jenis 32-bit untuk memilih segmen serta offset di dalamnya). Saya tahu hal-hal ini terdengar aneh pada hari-hari ini dengan arsitektur yang tidak beraturan yang dapat dialamatkan secara seragam, tetapi standar HARUS melayani variasi yang lebih luas daripada "apa yang normal pada tahun 2009", Anda tahu! -)

Alex Martelli
sumber
6
Ini, bersama dengan banyak orang lain yang melompat ke kesimpulan yang sama, menjelaskan perbedaan antara size_tdan uintptr_ttetapi bagaimana ptrdiff_tdan intptr_t- tidakkah keduanya dapat menyimpan rentang nilai yang sama di hampir semua platform? Mengapa memiliki tipe integer berukuran pointer dan unsigned, terutama jika ptrdiff_tsudah melayani tujuan dari tipe integer ukuran pointer yang ditandatangani.
Chris Lutz
8
Frasa kunci ada "di hampir semua platform", @Chris. Implementasi gratis untuk membatasi pointer ke kisaran 0xf000-0xffff - ini membutuhkan intptr_t 16bit tetapi hanya ptrdiff_t 12/13-bit.
paxdiablo
29
@ Chris, hanya untuk pointer di dalam array yang sama itu didefinisikan dengan baik untuk mengambil perbedaannya. Jadi, pada arsitektur 16-bit tersegmentasi yang sama persis (array harus hidup di dalam satu segmen tetapi dua array yang berbeda dapat berada di segmen yang berbeda) pointer harus 4 byte tetapi perbedaan pointer bisa 2 byte!
Alex Martelli
6
@AlexMartelli: Kecuali bahwa perbedaan pointer bisa positif atau negatif. Standar tersebut membutuhkan size_tsetidaknya 16 bit, tetapi ptrdiff_tharus setidaknya 17 bit (yang dalam praktiknya berarti mungkin akan setidaknya 32 bit).
Keith Thompson
3
Nevermind segmented architecture, bagaimana dengan arsitektur modern seperti x86-64? Implementasi awal arsitektur ini hanya memberi Anda ruang addressable 48-bit, tetapi pointer itu sendiri adalah tipe data 64-bit. Blok memori bersebelahan terbesar yang bisa Anda tangani secara wajar adalah 48-bit, jadi saya harus membayangkan SIZE_MAXseharusnya tidak menjadi 2 ** 64. Ini menggunakan pengalamatan datar, ingatlah; tidak ada segmentasi yang diperlukan untuk memiliki ketidakcocokan antara SIZE_MAXdan rentang pointer data.
Andon M. Coleman
89

Mengenai pernyataan Anda:

"Standar C menjamin bahwa itu size_tadalah tipe yang dapat menyimpan indeks array apa pun. Ini berarti bahwa, secara logis, size_tharus dapat menahan tipe pointer apa pun."

Ini sebenarnya sebuah kesalahan (kesalahpahaman yang dihasilkan dari alasan yang salah) (a) . Anda mungkin berpikir yang terakhir mengikuti dari yang pertama tetapi sebenarnya tidak demikian.

Pointer dan indeks array bukan hal yang sama. Sangat masuk akal untuk membayangkan implementasi yang sesuai yang membatasi array menjadi 65536 elemen tetapi memungkinkan pointer untuk menunjuk nilai apa pun ke dalam ruang alamat 128-bit yang besar.

C99 menyatakan bahwa batas atas suatu size_tvariabel didefinisikan oleh SIZE_MAXdan ini bisa serendah 65535 (lihat C99 TR3, 7.18.3, tidak berubah dalam C11). Pointer akan sangat terbatas jika mereka terbatas pada kisaran ini dalam sistem modern.

Dalam praktiknya, Anda mungkin akan menemukan bahwa asumsi Anda berlaku, tetapi itu bukan karena standar menjaminnya. Karena sebenarnya tidak menjamin itu.


(a) Ngomong-ngomong, ini bukan bentuk serangan pribadi, hanya menyatakan mengapa pernyataan Anda salah dalam konteks pemikiran kritis. Misalnya, alasan berikut juga tidak valid:

Semua anak anjing lucu. Benda ini lucu. Karena itu makhluk ini pasti anak anjing.

Kelucuan atau sebaliknya dari anak-anak anjing tidak ada hubungannya di sini, semua yang saya nyatakan adalah bahwa kedua fakta tidak mengarah pada kesimpulan, karena dua kalimat pertama memungkinkan adanya hal-hal lucu yang bukan anak - anak anjing.

Ini mirip dengan pernyataan pertama Anda yang belum tentu mengharuskan yang kedua.

paxdiablo
sumber
Daripada mengetik ulang apa yang saya katakan di komentar untuk Alex Martelli, saya hanya akan mengucapkan terima kasih atas klarifikasi, tetapi mengulangi bagian kedua dari pertanyaan saya (bagian ptrdiff_tvs. intptr_t).
Chris Lutz
5
@Van, seperti kebanyakan komunikasi, perlu ada pemahaman bersama tentang barang-barang dasar tertentu. Jika Anda melihat jawaban ini sebagai "mengolok-olok", saya jamin itu salah paham tentang maksud saya. Dengan asumsi bahwa Anda merujuk pada komentar 'fallacy logis' saya (saya tidak dapat melihat kemungkinan lain), itu dimaksudkan sebagai pernyataan faktual, bukan pernyataan yang dibuat dengan mengorbankan OP. Jika Anda ingin menyarankan beberapa perbaikan konkret untuk meminimalkan kemungkinan kesalahpahaman (bukan hanya keluhan umum), saya akan dengan senang hati mempertimbangkannya.
paxdiablo
1
@ivan_pozdeev - itu adalah suntingan yang menjengkelkan dan drastis, dan saya tidak melihat bukti bahwa paxdiablo "mengolok-olok" siapa pun. Jika saya OP, saya akan memutar ini kembali ....
ex nihilo
1
@Ivan, tidak benar-benar senang dengan suntingan yang Anda usulkan, telah dibatalkan dan juga mencoba untuk menghapus pelanggaran yang tidak diinginkan. Jika Anda memiliki perubahan lain untuk ditawarkan, saya sarankan memulai obrolan sehingga kami bisa berdiskusi.
paxdiablo
1
@paxdiablo oke, saya kira "ini sebenarnya adalah kesalahan" kurang menggurui.
ivan_pozdeev
36

Saya akan membiarkan semua jawaban lain berdiri sendiri mengenai alasan dengan batasan segmen, arsitektur eksotis, dan sebagainya.

Bukankah perbedaan sederhana dalam alasan cukup untuk menggunakan jenis yang tepat untuk hal yang tepat?

Jika Anda menyimpan ukuran, gunakan size_t. Jika Anda menyimpan pointer, gunakan intptr_t. Seseorang yang membaca kode Anda akan langsung tahu bahwa "aha, ini adalah ukuran sesuatu, mungkin dalam byte", dan "oh, inilah nilai pointer yang disimpan sebagai integer, untuk beberapa alasan".

Kalau tidak, Anda bisa menggunakan unsigned long(atau, di zaman modern di sini unsigned long long) , untuk semuanya. Ukuran bukanlah segalanya, ketikkan nama mengandung arti yang berguna karena membantu menjelaskan program.

beristirahat
sumber
Saya setuju, tapi saya sedang mempertimbangkan sesuatu dari hack / trik (yang tentu saja akan saya dokumentasikan) yang melibatkan penyimpanan tipe pointer di suatu size_tbidang.
Chris Lutz
@MarkAdler Standard tidak memerlukan pointer untuk diwakili sebagai integer sama sekali: Semua tipe pointer dapat dikonversi ke tipe integer. Kecuali seperti yang ditentukan sebelumnya, hasilnya ditentukan implementasi. Jika hasilnya tidak dapat direpresentasikan dalam tipe integer, perilaku tidak terdefinisi. Hasilnya tidak harus dalam kisaran nilai dari semua tipe integer. Dengan demikian, hanya void*, intptr_tdan uintptr_tdijamin dapat mewakili pointer apa pun ke data.
Andrew Svietlichnyy
12

Mungkin saja ukuran array terbesar lebih kecil dari sebuah pointer. Pikirkan arsitektur tersegmentasi - pointer mungkin 32-bit, tetapi satu segmen mungkin hanya mampu mengatasi 64KB (misalnya arsitektur real-mode 8086).

Meskipun ini tidak umum digunakan dalam mesin desktop lagi, standar C dimaksudkan untuk mendukung bahkan arsitektur kecil khusus. Masih ada embedded system yang sedang dikembangkan dengan CPU 8 atau 16 bit misalnya.

Michael Burr
sumber
Tetapi Anda dapat mengindeks pointer seperti array, jadi haruskah Anda size_tbisa mengatasinya? Atau apakah array dinamis di beberapa segmen yang jauh masih terbatas pada pengindeksan dalam segmen mereka?
Chris Lutz
Pointer pengindeksan hanya didukung secara teknis sesuai ukuran array yang mereka tunjuk - jadi jika sebuah array terbatas pada ukuran 64KB, hanya itu yang perlu didukung oleh aritmatika pointer. Namun, kompiler MS-DOS mendukung model memori 'besar', di mana pointer jauh (pointer tersegmentasi 32-bit) dimanipulasi sehingga mereka dapat menangani seluruh memori sebagai satu array - tetapi aritemik yang dilakukan untuk pointer di belakang layar adalah cukup jelek - ketika offset bertambah melewati nilai 16 (atau sesuatu), offset itu dibungkus kembali ke 0 dan bagian segmen bertambah.
Michael Burr
7
Baca en.wikipedia.org/wiki/C_memory_model#Memory_segmentation dan menangis untuk programmer MS-DOS yang mati sehingga kita bisa bebas.
Justicle
Lebih buruk lagi adalah bahwa fungsi stdlib tidak mengurus kata kunci BESAR. 16bit MS-C untuk semua strfungsi dan Borland bahkan untuk memfungsi ( memset, memcpy, memmove). Ini berarti Anda dapat menimpa sebagian memori ketika offset meluap, itu menyenangkan untuk debug pada platform tertanam kami.
Patrick Schlüter
@Justicle: Arsitektur segmen 8086 tidak didukung dengan baik di C, tapi saya tahu tidak ada arsitektur lain yang lebih efisien dalam kasus di mana ruang alamat 1MB cukup tetapi 64K tidak akan. Beberapa JVM modern benar-benar menggunakan pengalamatan seperti mode real x86, menggunakan pengalihan referensi objek 32-bit menyisakan 3 bit untuk menghasilkan alamat basis objek dalam ruang alamat 32GB.
supercat
5

Saya akan membayangkan (dan ini berlaku untuk semua jenis nama) bahwa lebih baik menyampaikan niat Anda dalam kode.

Sebagai contoh, meskipun unsigned shortdan wchar_tmemiliki ukuran yang sama pada Windows (saya pikir), menggunakan wchar_tbukannya unsigned shortmenunjukkan niat bahwa Anda akan menggunakannya untuk menyimpan karakter yang luas, daripada hanya beberapa nomor acak.

dreamlax
sumber
Tapi ada perbedaan di sini - pada sistem saya, wchar_tjauh lebih besar daripada unsigned shortmenggunakan satu untuk yang lain akan keliru dan menciptakan masalah portabilitas yang serius (dan modern), sedangkan portabilitas menyangkut size_tdan uintptr_ttampaknya terletak di tanah yang jauh 1980-sesuatu (tusukan acak dalam kegelapan pada tanggal, di sana)
Chris Lutz
Sentuh! Tapi sekali lagi, size_tdan uintptr_tmasih tersirat menggunakan nama mereka.
dreamlax
Mereka melakukannya, dan saya ingin tahu apakah ada motivasi untuk hal ini di luar sekadar kejelasan. Dan ternyata ada.
Chris Lutz
3

Melihat ke belakang dan ke depan, dan mengingat bahwa berbagai arsitektur aneh tersebar di lanskap, saya cukup yakin mereka mencoba untuk membungkus semua sistem yang ada dan juga menyediakan untuk semua sistem yang mungkin di masa depan.

Begitu yakin, cara penyelesaiannya, sejauh ini kami membutuhkan tidak banyak jenis.

Tetapi bahkan di LP64, sebuah paradigma yang agak umum, kami membutuhkan size_t dan ssize_t untuk antarmuka panggilan sistem. Orang bisa membayangkan warisan yang lebih terbatas atau sistem masa depan, di mana menggunakan tipe 64-bit penuh itu mahal dan mereka mungkin ingin menyodok pada I / O ops lebih besar dari 4GB tetapi masih memiliki pointer 64-bit.

Saya pikir Anda harus bertanya-tanya: apa yang mungkin telah dikembangkan, apa yang akan terjadi di masa depan. (Mungkin 128-bit pointer sistem terdistribusi internet-lebar, tetapi tidak lebih dari 64 bit dalam panggilan sistem, atau bahkan mungkin batas 32-bit "warisan". :-) Gambar yang sistem warisan mungkin mendapatkan kompiler C baru .. .

Juga, lihat apa yang ada di sekitar itu. Selain model memori real-mode zillion 286, bagaimana dengan mainframe pointer 60-bit word / 18-bit CDC? Bagaimana dengan seri Cray? Jangankan ILP64, LP64, LLP64 yang normal. (Saya selalu berpikir microsoft berpura-pura dengan LLP64, seharusnya P64.) Saya pasti bisa membayangkan sebuah komite mencoba untuk menutupi semua pangkalan ...

DigitalRoss
sumber
-9
int main(){
  int a[4]={0,1,5,3};
  int a0 = a[0];
  int a1 = *(a+1);
  int a2 = *(2+a);
  int a3 = 3[a];
  return a2;
}

Menyiratkan bahwa intptr_t harus selalu menggantikan size_t dan sebaliknya.

Chris Becke
sumber
10
Semua pertunjukkan ini adalah quirk sintaksis tertentu dari C. Array indexing didefinisikan dalam istilah x [y] yang setara dengan * (x + y), dan karena + 3 dan 3 + a identik dalam jenis dan nilai, Anda dapat gunakan 3 [a] atau [3].
Fred Nurk