Bir işlev işaretçisini başka bir türe çevirme


90

void (*)(void*)Geri arama olarak kullanılmak üzere bir işlev işaretçisini kabul eden bir işlevim olduğunu varsayalım :

Şimdi, böyle bir işleve sahipsem:

Bunu güvenle yapabilir miyim?

Ben baktım bu soruya ve sana 'uyumlu işlev işaretçileri' artığını söylüyorlar bazı C standartlarına baktım ama ne 'uyumlu fonksiyon işaretçisi' araçlarının bir tanım bulamıyorum.


1
Ben biraz acemiyim ama "void ( ) (void ) işlev işaretçisi" ne anlama geliyor ?. Bir boşluğu * argüman olarak kabul eden ve void döndüren bir işleve işaretçi mi
Digital Gal

2
@ Myke: gibi bir tür imzası olan bir işleve işaretçi olduğu void (*func)(void *)anlamına gelir . Yani evet, haklısın. funcvoid foo(void *arg)
mk12

Yanıtlar:


123

C standardı söz konusu olduğunda, farklı türden bir işlev işaretçisine bir işlev işaretçisi atarsanız ve sonra onu çağırırsanız, bu tanımsız bir davranıştır . Ek J.2'ye bakınız (bilgilendirici):

Aşağıdaki durumlarda davranış tanımsızdır:

  • Bir işaretçi, türü işaret edilen türle (6.3.2.3) uyumlu olmayan bir işlevi çağırmak için kullanılır.

Bölüm 6.3.2.3, paragraf 8 şunu okur:

Bir türdeki bir işleve yönelik bir işaretçi, başka türden bir işleve bir göstericiye dönüştürülebilir ve tekrar geri alınabilir; sonuç orijinal göstericiye eşit olmalıdır. Dönüştürülmüş bir işaretçi, türü işaret edilen yazı ile uyumlu olmayan bir işlevi çağırmak için kullanılırsa, davranış tanımsızdır.

Yani başka bir deyişle, bir işlev işaretçisini farklı bir işlev işaretçisi tipine çevirebilir, onu tekrar geri çevirebilir ve çağırabilirsiniz ve işler çalışacaktır.

Uyumluluğun tanımı biraz karmaşıktır. Bölüm 6.7.5.3, paragraf 15'te bulunabilir:

İki işlev türünün uyumlu olması için, her ikisi de uyumlu dönüş türlerini ( 127) belirtmelidir .

Ayrıca, parametre tipi listeleri, eğer her ikisi de mevcutsa, parametre sayısı ve elips sonlandırıcının kullanımında uyuşmalıdır; karşılık gelen parametrelerin uyumlu tipleri olacaktır. Bir türün bir parametre türü listesi varsa ve diğer tür, bir işlev tanımının parçası olmayan ve boş bir tanımlayıcı listesi içeren bir işlev tanımlayıcı tarafından belirtilmişse, parametre listesi bir üç nokta sonlandırıcısına sahip olmayacak ve her bir parametrenin türü varsayılan bağımsız değişken yükseltmelerinin uygulanmasından kaynaklanan türle uyumlu olmalıdır. Bir türün bir parametre türü listesi varsa ve diğer tür bir (muhtemelen boş) bir tanımlayıcı listesi içeren bir işlev tanımıyla belirtilmişse, her ikisi de parametre sayısı konusunda hemfikir olmalıdır, ve her prototip parametresinin türü, varsayılan argüman yükseltmelerinin karşılık gelen tanımlayıcının türüne uygulanmasından kaynaklanan türle uyumlu olacaktır. (Tür uyumluluğunun ve bir bileşik türün belirlenmesinde, işlev veya dizi türüyle bildirilen her parametre, ayarlanmış türe sahip olarak alınır ve nitelenmiş türle bildirilen her parametre, bildirilen türünün nitelenmemiş sürümüne sahip olarak alınır.)

127) Her iki işlev türü de "eski stil" ise, parametre türleri karşılaştırılmaz.

İki türün uyumlu olup olmadığını belirleme kuralları bölüm 6.2.7'de açıklanmıştır ve oldukça uzun oldukları için burada alıntı yapmayacağım, ancak bunları C99 standardının (PDF) taslağında okuyabilirsiniz .

Buradaki ilgili kural bölüm 6.7.5.1, 2. paragraftadır:

İki işaretçi tipinin uyumlu olması için, her ikisi de aynı nitelikte olmalı ve her ikisi de uyumlu tiplere işaretçi olacaktır.

Bu nedenle, a , a ile void* uyumlu olmadığındanstruct my_struct* , türündeki void (*)(void*)bir işlev işaretçisi, türündeki bir işlev işaretçisi ile uyumlu değildir void (*)(struct my_struct*), bu nedenle işlev işaretçilerinin bu dökümü teknik olarak tanımlanmamış bir davranıştır.

Uygulamada, yine de, bazı durumlarda işlev işaretlerini atarak güvenli bir şekilde kurtulabilirsiniz. X86 çağırma kuralında, bağımsız değişkenler yığın üzerinde itilir ve tüm işaretçiler aynı boyuttadır (x86'da 4 bayt veya x86_64'te 8 bayt). Bir işlev işaretçisi çağırmak, yığındaki bağımsız değişkenleri itmek ve işlev işaretçisi hedefine dolaylı bir sıçrama yapmak için aşağıya doğru kaynar ve açık bir şekilde makine kodu düzeyinde türler kavramı yoktur.

Kesinlikle yapamayacağınız şeyler:

  • Farklı arama kurallarının işlev işaretçileri arasında geçiş yapın. Yığını alt üst edeceksiniz ve en iyi ihtimalle, çarpacaksınız, en kötü ihtimalle, devasa bir güvenlik deliği ile sessizce başarılı olacaksınız. Windows programlamada, genellikle işlev işaretçileri dolaştırırsınız. Win32 tüm geri arama fonksiyonları kullanmak beklediğini stdcallçağırma kuralını (ki makro CALLBACK, PASCALve WINAPItüm genişletmek). Standart C çağırma kuralını ( cdecl) kullanan bir işlev göstericisini iletirseniz, kötülük ortaya çıkar.
  • C ++ 'da, sınıf üyesi işlev işaretçileri ve normal işlev işaretçileri arasında dönüşüm yapın. Bu genellikle C ++ yeni başlayanları tetikler. Sınıf üyesi işlevlerinin gizli bir thisparametresi vardır ve bir üye işlevini normal bir işleve atarsanız, thiskullanılacak nesne yoktur ve yine, çok fazla kötülük ortaya çıkar.

Bazen işe yarayabilecek ancak aynı zamanda tanımlanmamış davranış olan başka bir kötü fikir:

  • İşlev işaretçileri ve düzenli işaretçiler arasında Döküm (bir döküm örneğin void (*)(void)bir karşı void*). İşlev işaretçileri, bazı mimarilerde fazladan bağlamsal bilgiler içerebileceğinden, normal işaretçilerle aynı boyutta olmayabilir. Bu muhtemelen x86'da düzgün çalışacaktır, ancak bunun tanımsız bir davranış olduğunu unutmayın.

19
Bütün mesele, void*başka herhangi bir işaretçi ile uyumlu olmaları değil mi? A'yı a'ya çevirmede herhangi bir sorun olmamalı , aslında yayınlamak struct my_struct*zorunda void*bile değilsiniz, derleyici bunu kabul etmelidir. Örneğin, struct my_struct*a alan bir işleve a void*iletirseniz, çevrim gerekmez. Burada bunları uyumsuz kılan neyi özlüyorum?
brianmearns

2
Bu yanıt, "Bu muhtemelen x86'da sorunsuz çalışacaktır ..." konusuna gönderme yapar: Bunun çalışmayacağı herhangi bir platform var mı? Bu başarısız olduğunda tecrübesi olan var mı? qsort () for C, mümkünse bir işlev gösterici atamak için güzel bir yer gibi görünüyor.
kevinarpe

4
@KCArpe: Bu makaledeki "Üye İşlev İşaretçilerinin Gerçekleştirilmesi" başlığı altındaki grafiğe göre , 16 bitlik OpenWatcom derleyicisi bazen belirli yapılandırmalarda veri işaretçisi türünden (2 bayt) daha büyük bir işlev işaretçisi türü (4 bayt) kullanır. Bununla birlikte, POSIX uyumlu sistemler void*, işlev işaretçi türleriyle aynı gösterimi kullanmalıdır , spesifikasyona bakın .
Adam Rosenfield

3
@Adam'dan gelen bağlantı artık, ilgili bölüm 2.12.3'ün kaldırıldığı POSIX standardının 2016 baskısına atıfta bulunuyor. Yine de 2008 baskısında bulabilirsiniz .
Martin Trenkmann

7
@brianmearns Hayır, void *herhangi bir diğer (işlevsiz) gösterici ile çok kesin bir şekilde tanımlanmış yollarla (bu durumda C standardının "uyumlu" kelimesiyle ne anlama geldiğiyle ilgisi yoktur ) yalnızca "uyumludur" . C, a'nın a'dan void *daha büyük veya daha küçük olmasına struct my_struct *veya bitlerin farklı sırada veya olumsuzlanmış veya her neyse olmasına izin verir . Yani void f(void *)ve void f(struct my_struct *)olabilir ABI-uyumsuz . C, gerekirse sizin için işaretçileri dönüştürür, ancak muhtemelen farklı bir argüman türünü almak için işaretli bir işlevi bazen dönüştüremez ve dönüştüremez.
mtraceur

32

Geçenlerde GLib'deki bazı kodlarla ilgili olarak aynı sorunu sordum. (GLib, GNOME projesi için bir çekirdek kitaplıktır ve C'de yazılmıştır) Bana tüm slots'n'signals çerçevesinin ona bağlı olduğu söylendi.

Kod boyunca, (1) 'den (2)' ye kadar çok sayıda çevrim örneği vardır:

  1. typedef int (*CompareFunc) (const void *a, const void *b)
  2. typedef int (*CompareDataFunc) (const void *b, const void *b, void *user_data)

Aşağıdaki gibi çağrılarla zincirleme geçiş yapmak yaygındır:

Burada kendiniz görün g_array_sort(): http://git.gnome.org/browse/glib/tree/glib/garray.c

Yukarıdaki cevaplar ayrıntılı ve muhtemelen doğrudur - eğer standartlar komitesinde oturursanız. Adam ve Johannes, iyi araştırılmış yanıtları için övgüyü hak ediyor. Ancak, vahşi doğada, bu kodun gayet iyi çalıştığını göreceksiniz. Kontrollü? Evet. Şunu düşünün: GLib, çok çeşitli derleyiciler / bağlayıcılar / kernel yükleyiciler (GCC / CLang / MSVC) ile çok sayıda platformda (Linux / Solaris / Windows / OS X) derler / çalışır / test eder. Standartlar lanetleniyor sanırım.

Bu cevapları düşünmek için biraz zaman harcadım. İşte benim sonucum:

  1. Bir geri arama kitaplığı yazıyorsanız, bu sorun olmayabilir. Uyarı sahibi - kendi sorumluluğunuzdadır kullanın.
  2. Aksi takdirde, yapma.

Bu yanıtı yazdıktan sonra daha derin düşündüğümde, C derleyicilerinin kodu aynı numarayı kullanırsa şaşırmam. Ve (çoğu / tümü?) Modern C derleyicileri önyüklendiğinden, bu hilenin güvenli olduğu anlamına gelir.

Daha önemli bir soru araştırma: Can Birisi bir platform / derleyici / bağlayıcı / yükleyici bu hile yapar bulmak değil işi? Bunun için büyük kek puanları. Bahse girerim, hoşuna gitmeyen bazı yerleşik işlemciler / sistemler vardır. Bununla birlikte, masaüstü bilgi işlem (ve muhtemelen mobil / tablet) için bu numara muhtemelen hala çalışıyor.


10
Kesinlikle çalışmadığı bir yer Emscripten LLVM to Javascript derleyicisidir. Ayrıntılar için github.com/kripken/emscripten/wiki/Asm-pointer-casts adresine bakın.
Ben Lings

2
Yaklaşık upated referans Emscripten .
ysdx

4
Paylaşılan @BenLings bağlantısı yakın gelecekte kesilecektir. Resmi olarak kripken.github.io/emscripten-site/docs/porting/guidelines/… adresine
Alex Reinking

10

Mesele gerçekten yapıp yapamayacağınız değil. Önemsiz çözüm

İyi bir derleyici, gerçekten gerekliyse sadece my_callback_helper için kod üretecektir, bu durumda yaptığına sevinirsiniz.


Sorun şu ki, bu genel bir çözüm değil. İşlev bilgisi ile duruma göre yapılması gerekir. Zaten yanlış türde bir işleve sahipseniz, sıkışmışsınızdır.
BeeOnRope

Bunu birlikte test ettiğim tüm derleyiciler my_callback_helper, her zaman satır içi olmadığı sürece kod üretecek . Yapma eğiliminde olduğu tek şey olduğu için bu kesinlikle gerekli değildir jmp my_callback_function. Derleyici muhtemelen işlevler için adreslerin farklı olduğundan emin olmak ister, ancak ne yazık ki bunu işlev C99 ile işaretlendiğinde bile yapar inline(yani "adres umurunda değil").
yyny

Bunun doğru olduğundan emin değilim. Yukarıdaki başka bir yanıttan başka bir yorum (@mtraceur tarafından), a'nın a'dan void *farklı boyutta bile olabileceğini söylüyor struct *(bunun yanlış olduğunu düşünüyorum, çünkü aksi takdirde mallocbozulur, ancak bu yorumun 5 olumlu oyu var, bu yüzden biraz kredi veriyorum. @Mtraceur haklıysa, yazdığınız çözüm doğru olmaz.
2020

@cesss: Boyutun farklı olması hiç önemli değil. Dönüş ve dönüş void*hala çalışmak zorundadır. Kısacası, void*daha fazla bit olabilir, ancak bir döküm halinde struct*karşı void*sıfır olabilir bu ekstra bit ve dökme geri sadece tekrar o sıfır atabilirsiniz.
MSalters

@MSalters: Gerçekten bir void *(teoride) a'dan çok farklı olabileceğini bilmiyordum struct *. C'de bir vtable uyguluyorum ve thissanal işlevlere ilk argüman olarak bir C ++ - ish işaretçisi kullanıyorum . Açıkçası, this"mevcut" (türetilmiş) yapıya bir işaretçi olmalıdır. Dolayısıyla, sanal işlevler, uygulandıkları void *this
yapıya

6

Dönüş türü ve parametre türleri uyumluysa uyumlu bir işlev türüne sahipsiniz - temelde (gerçekte daha karmaşıktır :)). Uyumluluk, "aynı tür" ile aynıdır, farklı türlere sahip olmaya izin vermek için daha gevşek, ancak yine de "bu türler neredeyse aynı" deme biçimine sahiptir. Örneğin, C89'da, iki yapı birbirinin aynısı olsaydı uyumluydu, ancak sadece adları farklıydı. C99 bunu değiştirmiş görünüyor. Mantıksal belgeden alıntı (şiddetle tavsiye edilen okuma, btw!):

İki farklı çeviri birimindeki yapı, birleşim veya numaralandırma türü bildirimleri, bu bildirimlerin metni aynı içerme dosyasından gelse bile, çeviri birimlerinin kendileri ayrık olduğundan, resmi olarak aynı türü bildirmez. Bu nedenle Standart, bu tür iki beyanın yeterince benzer olması durumunda uyumlu olması için bu türler için ek uyumluluk kuralları belirler.

Bununla birlikte - evet kesinlikle bu tanımlanmamış bir davranıştır, çünkü do_stuff işleviniz veya bir başkası işlevinizi void*parametresi olan bir işlev işaretçisiyle çağırır , ancak işlevinizin uyumsuz bir parametresi vardır. Ancak yine de, tüm derleyicilerin onu inlemeden derleyip çalıştırmasını bekliyorum. Ancak, başka bir işleve sahip olarak void*(ve bunu geri arama işlevi olarak kaydederek) daha temiz bir iş yapabilirsiniz, bu da yalnızca gerçek işlevinizi çağıracaktır.


4

C kodu, işaretçi türlerini hiç umursamayan talimatları derlediğinden, bahsettiğiniz kodu kullanmak oldukça iyidir. Do_stuff'ı geri çağırma işlevinizle çalıştırdığınızda ve başka bir şeye işaret ettiğinizde, ardından argüman olarak my_struct yapısını çalıştırdığınızda sorunlarla karşılaşırsınız.

Umarım neyin işe yaramayacağını göstererek daha net hale getirebilirim:

veya...

Temel olarak, veriler çalışma zamanında anlamlı olmaya devam ettiği sürece, istediğiniz her şeye işaretçi çevirebilirsiniz.


0

İşlev çağrılarının C / C ++ 'da çalışma şeklini düşünürseniz, yığındaki belirli öğeleri iterler, yeni kod konumuna atlarlar, çalıştırırlar ve dönüşte yığını açarlar. İşlev işaretçileriniz aynı dönüş türüne ve aynı sayıda / boyutta bağımsız değişkene sahip işlevleri tanımlıyorsa, sorun yaşamamalısınız.

Bu nedenle, bunu güvenle yapabilmeniz gerektiğini düşünüyorum.


2
yalnızca structişaretçiler ve voidişaretçiler uyumlu bit temsillerine sahip olduğu sürece güvendesiniz ; bu garanti değil
Christoph

1
Derleyiciler ayrıca yazmaçlarda argümanlar iletebilirler. Ve değişkenler, girişler veya işaretçiler için farklı yazmaçlar kullanmak hiç duyulmamış bir şey değil.
MSalters

0

Void işaretçileri diğer işaretçi türleriyle uyumludur. Malloc ve mem işlevlerinin ( memcpy, memcmp) nasıl çalıştığının bel kemiğidir . Tipik olarak, C'de (C ++ yerine) NULLolarak tanımlanan bir makrodur ((void *)0).

C99'daki 6.3.2.3'e (Madde 1) bakın:

Boşluğa bir işaretçi, bir işaretçiye veya bir işaretçiden herhangi bir eksik veya nesne türüne dönüştürülebilir.


Bu, Adam Rosenfield'ın cevabıyla çelişiyor , son paragrafa ve yorumlara bakın
kullanıcı

1
Bu cevap açıkça yanlış. İşlev işaretçileri dışında herhangi bir işaretçi bir boşluk işaretçisine dönüştürülebilir .
marton78
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.