Saya telah membaca berbagai posting di Stack Overflow RE: kesalahan pointer-jenis dihukum-derefercing. Pemahaman saya adalah bahwa kesalahan pada dasarnya adalah peringatan kompiler dari bahaya mengakses suatu objek melalui pointer dari tipe yang berbeda (meskipun pengecualian tampaknya dibuat untuk char*
), yang merupakan peringatan yang dapat dimengerti dan masuk akal.
Pertanyaan saya khusus untuk kode di bawah ini: mengapa tidak memasukkan alamat penunjuk ke void**
kualifikasi untuk peringatan ini (dipromosikan ke kesalahan melalui -Werror
)?
Selain itu, kode ini dikompilasi untuk beberapa arsitektur target, hanya satu yang menghasilkan peringatan / kesalahan - mungkinkah ini menyiratkan bahwa itu adalah defisiensi khusus versi kompiler?
// main.c
#include <stdlib.h>
typedef struct Foo
{
int i;
} Foo;
void freeFunc( void** obj )
{
if ( obj && * obj )
{
free( *obj );
*obj = NULL;
}
}
int main( int argc, char* argv[] )
{
Foo* f = calloc( 1, sizeof( Foo ) );
freeFunc( (void**)(&f) );
return 0;
}
Jika pemahaman saya, yang dinyatakan di atas, benar, a void**
, karena masih hanya sebuah pointer, ini harus menjadi casting yang aman.
Apakah ada solusi untuk tidak menggunakan nilai yang akan menenangkan peringatan / kesalahan khusus kompiler ini? Yaitu saya mengerti itu dan mengapa ini akan menyelesaikan masalah, tetapi saya ingin menghindari pendekatan ini karena saya ingin mengambil keuntungan dari freeFunc()
NULL ing out-arg yang dimaksudkan:
void* tmp = f;
freeFunc( &tmp );
f = NULL;
Penyusun masalah (salah satunya):
user@8d63f499ed92:/build$ /usr/local/crosstool/x86-fc3/bin/i686-fc3-linux-gnu-gcc --version && /usr/local/crosstool/x86-fc3/bin/i686-fc3-linux-gnu-gcc -Wall -O2 -Werror ./main.c
i686-fc3-linux-gnu-gcc (GCC) 3.4.5
Copyright (C) 2004 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
./main.c: In function `main':
./main.c:21: warning: dereferencing type-punned pointer will break strict-aliasing rules
user@8d63f499ed92:/build$
Kompiler tidak mengeluh (salah satu dari banyak):
user@8d63f499ed92:/build$ /usr/local/crosstool/x86-rh73/bin/i686-rh73-linux-gnu-gcc --version && /usr/local/crosstool/x86-rh73/bin/i686-rh73-linux-gnu-gcc -Wall -O2 -Werror ./main.c
i686-rh73-linux-gnu-gcc (GCC) 3.2.3
Copyright (C) 2002 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
user@8d63f499ed92:/build$
Pembaruan: Saya menemukan bahwa peringatan tampaknya dihasilkan secara khusus ketika dikompilasi dengan -O2
(masih dengan "problem compiler" yang dicatat saja)
void**
, karena masih hanya sebuah pointer, ini seharusnya menjadi casting yang aman." Woah ada lendir! Sepertinya Anda memiliki beberapa asumsi mendasar yang sedang terjadi. Cobalah untuk berpikir lebih sedikit dalam hal byte dan tuas dan lebih banyak dalam hal abstraksi, karena itulah yang sebenarnya Anda pemrograman denganJawaban:
Nilai tipe
void**
adalah penunjuk ke objek tipevoid*
. Objek tipeFoo*
bukan objek tipevoid*
.Ada konversi implisit antara nilai tipe
Foo*
danvoid*
. Konversi ini dapat mengubah representasi nilai. Demikian pula, Anda dapat menulisint n = 3; double x = n;
dan ini memiliki perilaku yang ditetapkan dengan baikx
untuk nilai3.0
, tetapidouble *p = (double*)&n;
memiliki perilaku yang tidak terdefinisi (dan dalam praktiknya tidak akan diaturp
ke "penunjuk ke3.0
" pada arsitektur umum apa pun).Arsitektur di mana berbagai jenis pointer ke objek memiliki representasi yang berbeda jarang terjadi saat ini, tetapi mereka diizinkan oleh standar C. Ada (jarang) mesin tua dengan pointer kata yang merupakan alamat sebuah kata dalam memori dan byte pointer yang merupakan alamat kata bersama dengan byte offset dalam kata ini;
Foo*
akan menjadi penunjuk kata danvoid*
akan menjadi penunjuk byte pada arsitektur tersebut. Ada (jarang) mesin dengan pointer gemuk yang berisi informasi tidak hanya tentang alamat objek, tetapi juga tentang jenisnya, ukurannya dan daftar kontrol aksesnya; pointer ke tipe tertentu mungkin memiliki representasi yang berbeda darivoid*
yang membutuhkan informasi tipe tambahan saat runtime.Mesin seperti itu jarang terjadi, tetapi diizinkan oleh standar C. Dan beberapa kompiler C memanfaatkan izin untuk memperlakukan pointer jenis-dihukum sebagai berbeda untuk mengoptimalkan kode. Risiko aliasing pointer adalah keterbatasan utama pada kemampuan kompiler untuk mengoptimalkan kode, sehingga kompiler cenderung mengambil keuntungan dari izin tersebut.
Kompiler bebas memberi tahu Anda bahwa Anda melakukan kesalahan, atau diam-diam melakukan apa yang tidak Anda inginkan, atau diam-diam melakukan apa yang Anda inginkan. Perilaku tidak terdefinisi memungkinkan semua ini.
Anda dapat membuat
freefunc
makro:Ini datang dengan batasan makro yang biasa: kurangnya jenis keamanan,
p
dievaluasi dua kali. Perhatikan bahwa ini hanya memberi Anda keamanan untuk tidak meninggalkan pointer menggantung di sekitar jikap
itu adalah pointer tunggal ke objek yang dibebaskan.sumber
Foo*
danvoid*
memiliki representasi yang sama pada arsitektur Anda, masih belum ditentukan untuk mengetik-pun.A
void *
diperlakukan secara khusus oleh standar C sebagian karena merujuk pada tipe yang tidak lengkap. Perawatan ini tidak tidak memperpanjang untukvoid **
seperti halnya titik untuk tipe lengkap, khususvoid *
.Aturan aliasing yang ketat mengatakan Anda tidak dapat mengonversi penunjuk dari satu jenis ke penunjuk dari jenis lain dan kemudian menskorser penunjuk itu karena dengan melakukan itu berarti menafsirkan ulang byte dari satu jenis dengan yang lain. Satu-satunya pengecualian adalah ketika mengkonversi ke tipe karakter yang memungkinkan Anda membaca representasi objek.
Anda bisa mengatasi batasan ini dengan menggunakan makro seperti fungsi alih-alih fungsi:
Yang bisa Anda panggil seperti ini:
Namun ini memiliki batasan, karena makro di atas akan mengevaluasi
obj
dua kali. Jika Anda menggunakan GCC, ini dapat dihindari dengan beberapa ekstensi, khususnyatypeof
kata kunci dan ekspresi pernyataan:sumber
#define
adalah, itu akan mengevaluasiobj
dua kali. Saya tidak tahu cara yang baik untuk menghindari evaluasi kedua itu. Bahkan ekspresi pernyataan (ekstensi GNU) tidak akan melakukan trik seperti yang perlu Anda tetapkanobj
setelah Anda menggunakan nilainya.typeof
untuk menghindari mengevaluasiobj
dua kali:#define freeFunc(obj) ({ typeof(&(obj)) ptr = &(obj); free(*ptr); *ptr = NULL; })
.Dereferencing jenis pointer yang dihukum adalah UB dan Anda tidak dapat mengandalkan apa yang akan terjadi.
Kompiler yang berbeda menghasilkan peringatan yang berbeda, dan untuk tujuan ini, versi berbeda dari kompiler yang sama dapat dianggap sebagai kompiler yang berbeda. Ini sepertinya penjelasan yang lebih baik untuk varian yang Anda lihat daripada ketergantungan pada arsitektur.
Kasus yang dapat membantu Anda memahami mengapa jenis hukuman dalam kasus ini bisa buruk adalah bahwa fungsi Anda tidak akan berfungsi pada arsitektur yang mana
sizeof(Foo*) != sizeof(void*)
. Itu diotorisasi oleh standar meskipun saya tidak tahu ada yang sekarang yang ini benar.Solusinya adalah menggunakan makro alih-alih fungsi.
Perhatikan bahwa
free
menerima pointer nol.sumber
sizeof Foo* != sizeof void*
. Saya tidak pernah menemukan ukuran pointer "in the wild" bergantung pada tipe, jadi selama bertahun-tahun, saya menganggapnya aksiomatik bahwa ukuran pointer semuanya sama pada arsitektur yang diberikan.float*
tidak akan memodifikasiint32_t
objek, jadi mis. seorang kompilerint32_t*
tidak perluint32_t *restrict ptr
berasumsi itu tidak menunjuk ke memori yang sama. Sama untuk toko melaluivoid**
yang diasumsikan tidak mengubahFoo*
objek.Kode ini tidak valid per Standar C, jadi mungkin berfungsi dalam beberapa kasus, tetapi tidak harus portabel.
"Aturan aliasing ketat" untuk mengakses nilai melalui pointer yang telah dilemparkan ke tipe pointer yang berbeda ditemukan dalam 6.5 paragraf 7:
Di Anda
*obj = NULL;
pernyataan , objek memiliki tipe efektifFoo*
tetapi diakses oleh ekspresi nilai*obj
dengan tipevoid*
.Dalam 6.7.5.1 paragraf 2, kita punya
Jadi
void*
danFoo*
bukan tipe yang kompatibel atau tipe yang kompatibel dengan kualifikasi yang ditambahkan, dan tentu saja tidak cocok dengan opsi lain dari aturan aliasing yang ketat.Meskipun bukan alasan teknis kode tidak valid, itu juga relevan untuk dicatat bagian 6.2.5 paragraf 26:
Adapun perbedaan dalam peringatan, ini bukan kasus di mana Standar membutuhkan pesan diagnostik, jadi itu hanya masalah seberapa baik kompiler atau versinya dalam memperhatikan masalah potensial dan menunjukkannya dengan cara yang bermanfaat. Anda perhatikan pengaturan pengoptimalan dapat membuat perbedaan. Ini sering karena lebih banyak informasi yang dihasilkan secara internal tentang bagaimana berbagai bagian dari program benar-benar cocok dalam praktek, dan oleh karena itu informasi tambahan juga tersedia untuk pemeriksaan peringatan.
sumber
Di atas apa yang dikatakan jawaban lain, ini adalah pola anti-klasik C, dan yang harus dibakar dengan api. Itu muncul di:
void *
(yang tidak mengalami masalah ini karena melibatkan konversi nilai alih-alih jenis hukuman ), alih-alih mengembalikan bendera kesalahan dan menyimpan hasilnya melalui pointer-ke-pointer.Untuk contoh lain dari (1), ada kasus terkenal lama dalam
av_free
fungsi ffmpeg / libavcodec . Saya percaya itu akhirnya diperbaiki dengan makro atau trik lain, tapi saya tidak yakin.Untuk (2), keduanya
cudaMalloc
danposix_memalign
merupakan contoh.Dalam kedua kasus, antarmuka secara inheren tidak memerlukan penggunaan yang tidak benar, tetapi sangat mendorongnya, dan mengakui penggunaan yang benar hanya dengan objek tipe temporer tambahan
void *
yang mengalahkan tujuan dari fungsi bebas-dan-nol-keluar, dan membuat alokasi menjadi canggung.sumber
void *
dan casting / konversi setiap kali ingin dereferensi itu. Ini sangat tidak mungkin. Jika pemanggil menyimpan beberapa jenis pointer lain, satu-satunya cara untuk memanggil fungsi tanpa memanggil UB adalah menyalin pointer ke objek temp tipevoid *
dan meneruskan alamat itu ke fungsi membebaskan, dan kemudian hanya ...(void **)
gips, menghasilkan perilaku yang tidak terdefinisi.Meskipun C dirancang untuk mesin yang menggunakan representasi yang sama untuk semua pointer, penulis Standar ingin membuat bahasa dapat digunakan pada mesin yang menggunakan representasi berbeda untuk pointer ke berbagai jenis objek. Oleh karena itu, mereka tidak mengharuskan mesin yang menggunakan representasi pointer berbeda untuk berbagai jenis pointer mendukung tipe "pointer to any pointer", walaupun banyak mesin dapat melakukannya dengan biaya nol.
Sebelum Standar ditulis, implementasi untuk platform yang menggunakan representasi yang sama untuk semua jenis pointer akan dengan suara bulat memungkinkan
void**
untuk digunakan, setidaknya dengan casting yang sesuai, sebagai "pointer ke pointer". Para penulis Standar hampir pasti mengakui bahwa ini akan berguna pada platform yang mendukungnya, tetapi karena tidak dapat didukung secara universal, mereka menolak untuk mengamanatkannya. Sebaliknya, mereka berharap bahwa implementasi kualitas akan memproses konstruksi seperti apa yang akan dijelaskan oleh Rationale sebagai "ekstensi populer", dalam kasus di mana hal tersebut masuk akal.sumber