Pertanyaan Apakah ini jenis fungsi yang kompatibel di C?


Pertimbangkan program C berikut:

int f() { return 9; }
int main() {
  int (*h1)(int);
  h1 = f; // why is this allowed?                                               
  return h1(7);
}

Menurut Standar C11, Sec. 6.5.16.1, dalam penugasan sederhana, "salah satu dari yang berikut harus dipertahankan", dan satu-satunya yang relevan dalam daftar adalah sebagai berikut:

operan kiri memiliki tipe penunjuk atom, berkualitas, atau tidak memenuhi syarat, dan (mempertimbangkan jenis operan kiri setelah konversi nilai), kedua operand adalah pointer ke versi yang memenuhi syarat atau tidak memenuhi syarat dari jenis yang kompatibel, dan jenis yang ditunjukkan oleh kiri memiliki semua kualifikasi dari tipe yang ditunjuk oleh yang benar;

Selain itu, ini adalah "kendala", yang berarti, implementasi yang sesuai harus melaporkan pesan diagnostik jika dilanggar.

Menurut saya, kendala ini dilanggar dalam penugasan dalam program di atas. Kedua sisi penugasan adalah penunjuk fungsi. Jadi pertanyaannya adalah, apakah kedua jenis fungsi itu kompatibel? Ini dijawab dalam Sec. 6.7.6.3:

Untuk dua jenis fungsi agar kompatibel, keduanya harus menentukan jenis pengembalian yang kompatibel.146) Selain itu, daftar jenis parameter, jika keduanya ada, harus sesuai dengan jumlah parameter dan dalam penggunaan terminator elipsis; parameter yang sesuai harus memiliki tipe yang kompatibel. Jika satu jenis memiliki daftar jenis parameter dan jenis lainnya ditentukan oleh deklarator fungsi yang bukan bagian dari definisi fungsi dan yang berisi daftar identifier kosong, daftar parameter tidak akan memiliki terminator elipsis dan jenis setiap parameter harus kompatibel dengan jenis yang dihasilkan dari penerapan promosi argumen default. Jika satu jenis memiliki daftar jenis parameter dan jenis lainnya ditentukan oleh definisi fungsi yang berisi daftar pengidentifikasi (mungkin kosong), keduanya harus menyetujui jumlah parameter, dan jenis setiap parameter prototipe harus kompatibel dengan jenis yang dihasilkan dari penerapan promosi argumen default ke jenis pengidentifikasi yang sesuai.

Dalam hal ini, salah satu jenis, yaitu h1, memiliki daftar jenis parameter; yang lain, f, tidak. Oleh karena itu kalimat terakhir dalam kutipan di atas berlaku: khususnya, "keduanya harus setuju dalam jumlah parameter". Jelas h1 mengambil satu parameter. Bagaimana dengan f? Poin berikut terjadi sebelum di atas:

Daftar kosong dalam deklarator fungsi yang merupakan bagian dari definisi fungsi yang menetapkan bahwa fungsi tidak memiliki parameter.

Jadi jelas f mengambil 0 parameter. Jadi dua jenis tidak setuju dalam jumlah parameter, dua jenis fungsi tidak kompatibel, dan penugasan melanggar kendala, dan diagnosis harus dikeluarkan.

Namun, baik gcc 4.8 dan Clang tidak mengeluarkan peringatan ketika menyusun program:

tmp$ gcc-mp-4.8 -std=c11 -Wall tmp4.c 
tmp$ cc -std=c11 -Wall tmp4.c 
tmp$

By the way, kedua kompilator melakukan peringatan masalah jika f dinyatakan "int f (void) ...", tetapi ini tidak perlu berdasarkan pembacaan saya atas Standar di atas.

Pertanyaan-pertanyaan:

Q1: Apakah penugasan "h1 = f;" dalam program di atas melanggar batasan "kedua operan adalah penunjuk ke versi yang memenuhi syarat atau tidak memenuhi syarat dari jenis yang kompatibel"? Secara khusus:

Q2: Tipe h1 dalam ekspresi "h1 = f" adalah pointer-to-T1 untuk beberapa tipe fungsi T1. Apa sebenarnya T1?

Q3: Tipe f dalam ekspresi "h1 = f" adalah pointer-to-T2 untuk beberapa tipe fungsi T2. Apa sebenarnya T2?

Q4: Apakah tipe T1 dan T2 kompatibel? (Silakan mengutip bagian Standar yang sesuai atau dokumen lain untuk mendukung jawabannya.)

Q1 ', Q2', Q3 ', Q4': Sekarang misalkan deklarasi f diubah menjadi "int f (void) {return 9;}". Jawab pertanyaan 1-4 lagi untuk program ini.


32
2017-07-14 19:04


asal


Jawaban:


Dua laporan cacat ini mengatasi masalah Anda:

Cacat laporan 316 mengatakanpenekanan tambang maju):

Aturan untuk kompatibilitas tipe fungsi di 6.7.5.3 # 15 tidak   menentukan kapan suatu tipe fungsi "ditentukan oleh definisi fungsi   yang berisi daftar pengenal (mungkin kosong) ", [...]

dan itu memiliki contoh serupa dengan yang Anda berikan:

void f(a)int a;{}
void (*h)(int, int, int) = f;

dan selanjutnya mengatakan:

Saya percaya maksud dari standar itu adalah a jenis ditentukan oleh   definisi fungsi hanya untuk keperluan memeriksa kompatibilitas   beberapa deklarasi dari fungsi yang sama; ketika seperti di sini nama   fungsi muncul dalam ekspresi, jenisnya ditentukan oleh   kembali jenis dan tidak mengandung jejak dari tipe parameter. Namun,   interpretasi implementasi bervariasi.

Pertanyaan 2: Apakah unit terjemahan di atas valid?

dan jawabannya dari panitia adalah:

Komite percaya bahwa jawaban untuk Q1 & 2 adalah ya

Ini antara C99 dan C11 tetapi panitia menambahkan:

Kami tidak punya niat untuk memperbaiki aturan gaya lama. Namun, itu   pengamatan yang dibuat dalam dokumen ini tampaknya secara umum benar.

dan sejauh yang saya tahu C99 dan C11 tidak sangat berbeda dalam bagian yang telah Anda kutip dalam pertanyaan. Jika kita melihat lebih jauh laporan cacat 317 kita dapat melihat bahwa itu mengatakan:

Saya percaya maksud dari C adalah definisi fungsi gaya lama dengan   kurung kosong jangan berikan fungsi tipe termasuk a   prototipe untuk sisa unit terjemahan. Sebagai contoh:

void f(){} 
void g(){if(0)f(1);}

Pertanyaan 1: Apakah definisi fungsi semacam itu memberikan fungsi suatu tipe   termasuk prototipe untuk sisa unit terjemahan?

Pertanyaan 2: Apakah unit terjemahan di atas valid?

dan tanggapan komite adalah:

Jawaban atas pertanyaan nomor 1 adalah TIDAK, dan pertanyaan # 2 adalah YA. Ada   tidak ada pelanggaran kendala, namun jika pemanggilan fungsi dieksekusi   itu akan memiliki perilaku yang tidak terdefinisi. Lihat 6.5.2.2; p6.

Hal ini tampaknya bergantung pada fakta bahwa tidak ditentukan apakah definisi fungsi mendefinisikan tipe atau prototipe dan oleh karena itu berarti tidak ada persyaratan pemeriksaan kompatibilitas. Ini awalnya maksud dengan definisi fungsi gaya lama dan komite tidak akan memperjelas lebih lanjut mungkin karena sudah ditinggalkan.

Komite menunjukkan bahwa hanya karena unit penerjemahan valid bukan berarti tidak ada perilaku yang tidak terdefinisi.


8
2017-07-16 03:22



Secara historis, compiler C umumnya menangani argumen yang lewat dengan cara yang menjamin bahwa argumen tambahan akan diabaikan, dan juga hanya mensyaratkan bahwa program-program melewati argumen untuk parameter yang sebenarnya bekas, sehingga memungkinkan misalnya

int foo(a,b) int a,b;
{
  if (a)
    printf("%d",b);
  else
    printf("Unspecified");
}

untuk dapat dengan aman dipanggil melalui keduanya foo(1,123); atau foo(0);, tanpa harus menentukan argumen kedua dalam kasus terakhir. Bahkan pada platform (misalnya. Macintosh klasik) yang konvensi pemanggilan normalnya tidak akan mendukung jaminan semacam itu, C compiler umumnya gagal menggunakan konvensi pemanggilan yang akan mendukungnya.

Standar membuat jelas bahwa compiler tidak wajib untuk mendukung penggunaan tersebut, tetapi membutuhkan implementasi untuk melarang mereka tidak hanya melanggar kode yang sudah ada, tetapi juga akan membuat tidak mungkin bagi mereka untuk menghasilkan kode yang seefisien seperti apa yang mungkin dalam C standar (sejak kode aplikasi harus diubah untuk melewati argumen yang tidak berguna, yang kemudian harus menyusun kode untuk compiler). Membuat penggunaan seperti Perilaku Undefined menghilangkan implementasi dari setiap kewajiban untuk mendukungnya, sementara masih memungkinkan implementasi untuk mendukungnya jika nyaman.


2
2017-10-26 23:26



Bukan jawaban langsung untuk pertanyaan Anda, tetapi compiler hanya menghasilkan perakitan untuk mendorong nilai ke stack sebelum memanggil fungsi.

Sebagai contoh (menggunakan VS-2013 compiler):

mov         esi,esp
push        7
call        dword ptr [h1]

Jika Anda menambahkan variabel lokal dalam fungsi ini, maka Anda dapat menggunakan alamatnya untuk menemukan nilai yang Anda lewati setiap kali Anda memanggil fungsi tersebut.

Sebagai contoh (menggunakan VS-2013 compiler):

int f()
{
    int a = 0;
    int* p1 = &a + 4; // *p1 == 1
    int* p2 = &a + 5; // *p2 == 2
    int* p3 = &a + 6; // *p3 == 3
    return a;
}

int main()
{
    int(*h1)(int);
    h1 = f;
    return h1(1,2,3);
}

Jadi pada dasarnya, memanggil fungsi dengan argumen tambahan benar-benar aman, karena mereka hanya didorong ke stack sebelum program-counter diatur ke alamat fungsi (dalam kode-bagian dari gambar yang dapat dieksekusi).

Tentu saja, seseorang dapat mengklaim bahwa hal itu mungkin mengakibatkan tumpukan-overflow, tetapi itu dapat terjadi dalam hal apapun (bahkan jika jumlah argumen yang dilewatkan sama dengan jumlah argumen yang dideklarasikan).


1
2017-07-15 07:26



Untuk fungsi tanpa parameter yang dinyatakan tidak ada parameter / tipe parameter yang disimpulkan oleh kompilator. Kode berikut pada dasarnya sama:

int f()
{
    return 9;
}

int main()
{
    return f(7, 8, 9);
}

Saya percaya ini ada hubungannya dengan cara argumen variabel variabel yang mendasari didukung, dan bahwa () pada dasarnya identik dengan (...). Melihat lebih dekat pada kode objek yang dihasilkan menunjukkan bahwa argumen ke f () masih terdorong ke register yang digunakan untuk memanggil fungsi, tetapi karena mereka direferensikan dalam definisi fungsi, mereka tidak digunakan di dalam fungsi. Jika Anda ingin mendeklarasikan parameter yang tidak mendukung argumen itu sedikit lebih tepat untuk menulisnya seperti:

int f(void)
{
    return 9;
}

int main()
{
    return f(7, 8, 9);
}

Kode ini akan gagal dikompilasi dalam GCC untuk kesalahan berikut:

In function 'main':
error: too many arguments to function 'f'

-1
2017-07-14 22:32



coba gunakan __stdcall sebelum deklarasi fungsi - dan itu tidak akan dikompilasi.
Alasannya adalah bahwa fungsi panggilan adalah __cdecl secara default. Itu berarti (di samping fitur-fitur lain) bahwa pemanggil membersihkan tumpukan demi panggilan. Jadi, fungsi pemanggil dapat mendorong semua tumpukan yang diinginkan, karena ia tahu apa yang mendorongnya dan akan membersihkan tumpukan dengan cara yang benar.
__stdcall berarti (selain hal-hal lain) bahwa callee akan membersihkan tumpukan. Jadi jumlah argumen harus sesuai.
... tanda mengatakan kepada kompiler bahwa jumlah argumen bervariasi. Jika dinyatakan sebagai __stdcall, maka itu akan secara otomatis digantikan dengan __cdecl, dan Anda masih dapat menggunakan argumen sebanyak yang Anda inginkan.

Itulah mengapa kompilator memperingatkan, tetapi tidak berhenti.

Contoh
Kesalahan: tumpukan rusak.

#include <stdio.h>

void __stdcall allmyvars(int num) {
    int *p = &num + 1;
    while (num--) {
        printf("%d ", *p);
        p++;
    }  
}

void main() {
    allmyvars(4, 1, 2, 3, 4);
}

Bekerja

#include <stdio.h>

void allmyvars(int num) {
    int *p = &num + 1;
    while (num--) {
        printf("%d ", *p);
        p++;
    }  
}

void main() {
    allmyvars(4, 1, 2, 3, 4);
}

Untuk contoh ini Anda memiliki perilaku normal, yang tidak saling berhubungan dengan standar. Anda menyatakan pointer ke fungsi, setelah itu menetapkan pointer ini dan itu mengarah ke konversi tipe implisit. Saya menulis mengapa itu berhasil. Di c Anda juga bisa menulis

int main() {
  int *p;
  p = (int (*)(void))f; // why is this allowed?      
  ((int (*)())p)();
  return ((int (*)())p)(7);
}

Dan itu masih bagian dari standar, tetapi bagian lain dari standar tentu saja. Dan tidak ada yang terjadi, bahkan jika Anda menetapkan pointer ke fungsi untuk penunjuk ke int.


-4
2017-07-15 05:40