Mengapa ini menyatakan kompiler khusus peringatan jenis pointer dihukum?

38

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)

StoneThrow
sumber
1
"a 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 dengan
Lightness Races in Orbit
7
Intinya, kompiler yang Anda gunakan berusia 15 dan 17 tahun! Saya tidak akan bergantung pada keduanya.
Tavian Barnes
4
@TavianBarnes Juga, jika Anda harus bergantung pada GCC 3 untuk alasan apa pun, yang terbaik adalah menggunakan versi akhir end-of-lifeed, yang 3.4.6, saya pikir. Mengapa tidak mengambil keuntungan dari semua perbaikan yang tersedia untuk seri itu sebelum dimakamkan.
Kaz
Standar pengkodean C ++ seperti apa yang mengatur semua ruang tersebut?
Peter Mortensen

Jawaban:

33

Nilai tipe void**adalah penunjuk ke objek tipe void*. Objek tipe Foo*bukan objek tipe void*.

Ada konversi implisit antara nilai tipe Foo*dan void*. Konversi ini dapat mengubah representasi nilai. Demikian pula, Anda dapat menulis int n = 3; double x = n;dan ini memiliki perilaku yang ditetapkan dengan baik xuntuk nilai 3.0, tetapi double *p = (double*)&n;memiliki perilaku yang tidak terdefinisi (dan dalam praktiknya tidak akan diatur pke "penunjuk ke 3.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 dan void*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 dari void*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 freefuncmakro:

#define FREE_SINGLE_REFERENCE(p) (free(p), (p) = NULL)

Ini datang dengan batasan makro yang biasa: kurangnya jenis keamanan, pdievaluasi dua kali. Perhatikan bahwa ini hanya memberi Anda keamanan untuk tidak meninggalkan pointer menggantung di sekitar jika pitu adalah pointer tunggal ke objek yang dibebaskan.

Gilles 'SANGAT berhenti menjadi jahat'
sumber
1
Dan bagus untuk mengetahui bahwa meskipun Foo*dan void*memiliki representasi yang sama pada arsitektur Anda, masih belum ditentukan untuk mengetik-pun.
Tavian Barnes
12

A void *diperlakukan secara khusus oleh standar C sebagian karena merujuk pada tipe yang tidak lengkap. Perawatan ini tidak tidak memperpanjang untuk void **seperti halnya titik untuk tipe lengkap, khusus void *.

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:

#define freeFunc(obj) (free(obj), (obj) = NULL)

Yang bisa Anda panggil seperti ini:

freeFunc(f);

Namun ini memiliki batasan, karena makro di atas akan mengevaluasi objdua kali. Jika Anda menggunakan GCC, ini dapat dihindari dengan beberapa ekstensi, khususnya typeofkata kunci dan ekspresi pernyataan:

#define freeFunc(obj) ({ typeof (&(obj)) ptr = &(obj); free(*ptr); *ptr = NULL; })
dbush
sumber
3
+1 untuk memberikan implementasi yang lebih baik dari perilaku yang dimaksud. Satu-satunya masalah yang saya lihat #defineadalah, itu akan mengevaluasi objdua 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 tetapkan objsetelah Anda menggunakan nilainya.
cmaster - mengembalikan monica
2
@cmaster: Jika Anda bersedia untuk menggunakan ekstensi GNU seperti ekspresi pernyataan, maka Anda dapat menggunakan typeofuntuk menghindari mengevaluasi objdua kali: #define freeFunc(obj) ({ typeof(&(obj)) ptr = &(obj); free(*ptr); *ptr = NULL; }).
ruakh
@ruakh Sangat keren :-) Akan lebih bagus jika dbush akan mengeditnya menjadi jawaban, jadi itu tidak akan dihapus secara massal dengan komentar.
cmaster - mengembalikan monica
9

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 freemenerima pointer nol.

Pemrogram
sumber
2
Menarik bahwa mungkin saja 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.
StoneThrow
1
@Seethrow contoh standar adalah pointer lemak yang digunakan untuk mengatasi byte dalam arsitektur addressable kata. Tapi saya pikir kata mesin addressable saat ini menggunakan alternatif sizeof char == sizeof kata .
Pemrogram
2
Perhatikan bahwa jenis harus dipasangkan untuk sizeof ...
Antti Haapala
@StoneThrow: Terlepas dari ukuran penunjuk, analisis alias berbasis tipe membuatnya tidak aman; ini membantu kompiler mengoptimalkan dengan mengasumsikan bahwa toko melalui a float*tidak akan memodifikasi int32_tobjek, jadi mis. seorang kompiler int32_t*tidak perlu int32_t *restrict ptrberasumsi itu tidak menunjuk ke memori yang sama. Sama untuk toko melalui void**yang diasumsikan tidak mengubah Foo*objek.
Peter Cordes
4

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:

Objek harus memiliki nilai tersimpan diakses hanya oleh ekspresi lvalue yang memiliki salah satu dari jenis berikut:

  • jenis yang kompatibel dengan jenis objek yang efektif,

  • versi berkualitas dari jenis yang kompatibel dengan jenis objek yang efektif,

  • tipe yang merupakan tipe bertanda tangan atau tidak bertanda tangan yang sesuai dengan jenis objek yang efektif,

  • tipe yang merupakan tipe bertanda tangan atau tidak bertanda tangan yang sesuai dengan versi terkualifikasi dari tipe efektif objek,

  • suatu jenis agregat atau serikat yang mencakup salah satu dari jenis-jenis yang disebutkan di atas di antara para anggotanya (termasuk, secara rekursif, seorang anggota dari sub-agregat atau serikat yang terkandung), atau

  • tipe karakter.

Di Anda *obj = NULL; pernyataan , objek memiliki tipe efektif Foo*tetapi diakses oleh ekspresi nilai *objdengan tipe void*.

Dalam 6.7.5.1 paragraf 2, kita punya

Untuk dua tipe pointer agar kompatibel, keduanya harus memiliki kualifikasi yang identik dan keduanya harus menjadi pointer ke tipe yang kompatibel.

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:

Pointer ke voidharus memiliki persyaratan representasi dan penyelarasan yang sama dengan pointer ke tipe karakter. Demikian pula, pointer ke versi yang memenuhi syarat atau tidak memenuhi syarat dari jenis yang kompatibel harus memiliki persyaratan representasi dan perataan yang sama. Semua pointer ke tipe struktur harus memiliki persyaratan representasi dan perataan yang sama satu sama lain. Semua pointer ke tipe-tipe serikat pekerja harus memiliki persyaratan representasi dan penyelarasan yang sama satu sama lain. Pointer ke tipe lain tidak perlu memiliki representasi atau persyaratan penyelarasan yang sama.

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.

aschepler
sumber
2

Di atas apa yang dikatakan jawaban lain, ini adalah pola anti-klasik C, dan yang harus dibakar dengan api. Itu muncul di:

  1. Fungsi bebas-dan-nol-keluar seperti yang Anda temukan peringatannya.
  2. Fungsi alokasi yang menghindari idiom standar C untuk kembali 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_freefungsi ffmpeg / libavcodec . Saya percaya itu akhirnya diperbaiki dengan makro atau trik lain, tapi saya tidak yakin.

Untuk (2), keduanya cudaMallocdan posix_memalignmerupakan 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.

R .. GitHub BERHENTI MEMBANTU ICE
sumber
Apakah Anda memiliki tautan yang menjelaskan lebih lanjut mengapa (1) anti-pola? Saya pikir saya tidak terbiasa dengan situasi / argumen ini dan ingin belajar lebih banyak.
StoneThrow
1
@StoneThrow: Ini benar-benar sederhana - tujuannya adalah untuk mencegah penyalahgunaan dengan meniadakan objek yang menyimpan pointer ke memori yang sedang dibebaskan, tetapi satu-satunya cara sebenarnya dapat melakukan itu adalah jika penelepon benar-benar menyimpan pointer di objek ketik 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 tipe void *dan meneruskan alamat itu ke fungsi membebaskan, dan kemudian hanya ...
R .. GitHub BERHENTI MEMBANTU ICE
1
... nulls objek temp daripada penyimpanan aktual di mana penelepon memiliki pointer. Tentu saja yang sebenarnya terjadi adalah pengguna fungsi tersebut akhirnya melakukan (void **)gips, menghasilkan perilaku yang tidak terdefinisi.
R .. GitHub BERHENTI MEMBANTU ICE
2

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.

supercat
sumber