C ++ 'da std :: veya bir işlev işaretçisi mi kullanmalıyım?


142

C ++ 'da bir geri çağırma işlevi uygularken, yine de C stili işlev işaretçisini kullanmalıyım:

void (*callbackFunc)(int);

Yoksa std :: fonksiyonunu kullanmalıyım:

std::function< void(int) > callbackFunc;

9
Geri çağırma işlevi derleme zamanında biliniyorsa, bunun yerine bir şablon düşünün.
Baum mit Augen

4
Bir geri arama işlevi uygularken arayanın gerektirdiği her şeyi yapmanız gerekir. Sorunuz gerçekten bir geri arama arayüzü tasarlamakla ilgiliyse, cevaplamak için yeterince yakın bilgi yoktur. Geri arama alıcınızın ne yapmasını istiyorsunuz? Alıcıya hangi bilgileri vermeniz gerekiyor? Çağrı sonucunda alıcı size hangi bilgileri geri vermelidir?
Pete Becker

Yanıtlar:


171

Kısacası,std::function bir nedeniniz olmadığı sürece kullanın .

İşlev işaretçileri bazı bağlamları yakalayamama dezavantajına sahiptir . Örneğin, bazı bağlam değişkenlerini yakalayan bir geri arama olarak lambda işlevini geçiremezsiniz (ancak yakalamadığında işe yarayacaktır). Bu nedenle, bir nesnenin üye değişkenini (yani statik olmayan) thisçağırmak da mümkün değildir, çünkü nesnenin ( -pointer) yakalanması gerekir. (1)

std::function(çünkü C ++ 11) öncelikle bir işlevi saklamak içindir (onu aktarmak, saklanmasını gerektirmez). Dolayısıyla, geri aramayı örneğin bir üye değişkeninde saklamak istiyorsanız, muhtemelen en iyi seçimdir. Ama aynı zamanda saklamıyorsanız, iyi bir "ilk seçim" olmasına rağmen, çağrıldığında bazı (çok küçük) ek yükü dezavantajına sahip olsa da (çok performans açısından kritik bir durumda bir sorun olabilir, ancak çoğu olmamalıdır). Çok "evrensel": tutarlı ve okunabilir kodlara çok önem veriyorsanız ve yaptığınız her seçimi (yani basit tutmak istiyorsanız) düşünmek istemiyorsanız, std::functionetrafta dolaştığınız her fonksiyon için kullanın .

Üçüncü bir seçenek düşünün: o zaman sağlanan geri arama fonksiyonu vasıtasıyla şey bildiriyor küçük işlevi uygulamak için yaklaşık iseniz, bir düşünün şablon parametresi sonra olabilir, herhangi bir çağrılabilir nesne , yani bir işlev işaretçisi, bir funktoru, bir lambda, a std::function, ... Buradaki dezavantaj, (dış) işlevinizin bir şablon haline gelmesi ve dolayısıyla başlıkta uygulanması gerektiğidir. Öte yandan, (dış) fonksiyonunuzun istemci kodu, geri çağrıya yapılan çağrının tam olarak kullanılabilecek tür bilgisi olacağını "gördüğü" için, geri çağrıya yapılan çağrının satır içine alınabilmesi avantajını elde edersiniz.

Template parametresine sahip sürüm örneği ( C ++ 11 öncesi &yerine yazma &&):

template <typename CallbackFunction>
void myFunction(..., CallbackFunction && callback) {
    ...
    callback(...);
    ...
}

Aşağıdaki tabloda görebileceğiniz gibi, hepsinin avantajları ve dezavantajları vardır:

+-------------------+--------------+---------------+----------------+
|                   | function ptr | std::function | template param |
+===================+==============+===============+================+
| can capture       |    no(1)     |      yes      |       yes      |
| context variables |              |               |                |
+-------------------+--------------+---------------+----------------+
| no call overhead  |     yes      |       no      |       yes      |
| (see comments)    |              |               |                |
+-------------------+--------------+---------------+----------------+
| can be inlined    |      no      |       no      |       yes      |
| (see comments)    |              |               |                |
+-------------------+--------------+---------------+----------------+
| can be stored     |     yes      |      yes      |      no(2)     |
| in class member   |              |               |                |
+-------------------+--------------+---------------+----------------+
| can be implemented|     yes      |      yes      |       no       |
| outside of header |              |               |                |
+-------------------+--------------+---------------+----------------+
| supported without |     yes      |     no(3)     |       yes      |
| C++11 standard    |              |               |                |
+-------------------+--------------+---------------+----------------+
| nicely readable   |      no      |      yes      |      (yes)     |
| (my opinion)      | (ugly type)  |               |                |
+-------------------+--------------+---------------+----------------+

(1) Bu sınırlamanın üstesinden gelmek için geçici çözümler mevcuttur, örneğin ek verileri (dış) fonksiyonunuza başka parametreler olarak iletmek: myFunction(..., callback, data)çağrılacaktır callback(data). C ++ 'da (ve bu arada WIN32 API'de yoğun bir şekilde kullanılırken) mümkün olan ancak C ++' da daha iyi seçeneklere sahip olduğumuzdan kaçınılması gereken C tarzı "argümanlarla geri çağrı".

(2) Bir sınıf şablonundan bahsetmediğimiz sürece, yani işlevi sakladığınız sınıf bir şablondur. Ancak bu, istemci tarafında, işlevin türünün, gerçek kullanım durumları için neredeyse hiçbir zaman bir seçenek olmayan geri çağrıyı depolayan nesnenin türüne karar verdiği anlamına gelir.

(3) C ++ 11 öncesi için boost::function


9
işlev işaretçileri şablon parametrelerine kıyasla çağrı yüküne sahiptir. Yürütülen kod, değerin değil parametrenin türüyle tanımlandığı için şablon parametreleri, on altı düzeyden aşağıya geçseniz bile satır içi işlemeyi kolaylaştırır. Ve şablon dönüş türlerinde saklanan şablon işlevi nesneleri, yaygın ve kullanışlı bir kalıptır (iyi bir kopya oluşturucu ile, std::functionhemen dışında saklamanız gerekiyorsa , tür silinmiş olana dönüştürülebilen etkin şablon işlevini yaratabilirsiniz. bağlam denir).
Yakk - Adam Nevraumont

1
@tohecz Şimdi C ++ 11 gerektirip gerektirmediğinden bahsediyorum.
leemes

1
@Yakk Oh, elbette, unutmuşum! Ekledim, teşekkürler.
leemes

1
@MooingDuck Tabii ki uygulamaya bağlı. Ancak doğru hatırlıyorsam, tür silme nasıl çalıştığı nedeniyle bir dolaylı daha var mı? Ama şimdi tekrar düşündüğüme göre, işlev işaretçileri veya yakalamadan daha az lambdas atarsanız durum böyle değil ... (tipik bir optimizasyon olarak)
leemes

1
@leemes: Doğru, işlev işaretçileri veya captureless lambdas için, bu gerektiğini c-fonk-ptr aynı yükü olması. Hangi hala bir boru hattı durak + önemsiz bir şekilde inline değil.
Mooing Ördek

25

void (*callbackFunc)(int); C tarzı geri arama işlevi olabilir, ancak kötü tasarımın korkunç derecede kullanılamaz bir işlevidir.

İyi tasarlanmış bir C tarzı geri arama, şuna benzer void (*callbackFunc)(void*, int);- geri çağrıyı void*yapan kodun, fonksiyonun ötesinde durumunu korumasına izin vermek için bir vardır . Bunu yapmamak, arayanı devleti küresel olarak depolamaya zorlar, bu da kaba değildir.

std::function< int(int) >sonuç int(*)(void*, int)olarak çoğu uygulamada çağrıdan biraz daha pahalı olur . Ancak bazı derleyicilerin satır içi olması daha zordur. Orada std::functionrakip fonksiyon işaretçisi invokation giderleri kütüphaneler içine kendi yolunu yapabilir ( 'mümkün olan en hızlı delegelere' vb bakınız) o klon uygulamalar.

Şimdi, bir geri arama sistemi istemcilerinin, geri arama oluşturulduğunda ve kaldırıldığında kaynakları kurmaları ve atmaları ve geri arama ömrünün farkında olmaları gerekir. void(*callback)(void*, int)bunu sağlamaz.

Bazen bu, kod yapısı (geri aramanın ömrü sınırlıdır) veya diğer mekanizmalar (geri aramaları ve benzerlerini silme) yoluyla kullanılabilir.

std::function sınırlı ömür boyu yönetim için bir araç sağlar (nesnenin son kopyası unutulduğunda kaybolur).

Genel olarak, std::functionperformans kaygıları ortaya çıkmadıkça bir . Eğer öyleyse, ilk önce yapısal değişiklikleri arardım (piksel başına geri arama yerine, bana geçtiğiniz lambda tabanlı bir tarama hattı işlemcisi üretmeye ne dersiniz? ). Sonra, devam ederse, delegatemümkün olan en hızlı delegelere dayalı bir yazı yazardım ve performans sorununun giderilip giderilmediğini görüyorum.

Çoğunlukla yalnızca eski API'lar için işlev işaretçileri veya farklı derleyiciler oluşturulan kodlar arasında iletişim kurmak için C arabirimleri oluşturmak için kullanılır. Ayrıca atlama tabloları, tip silme, vb. Uygularken bunları iç uygulama ayrıntıları olarak kullandım: hem üretip hem de tüketirken ve dışarıdan herhangi bir istemci kodunun kullanması için dışarıda açığa çıkmadığımda ve işlev işaretçileri ihtiyacım olan her şeyi yapıyor .

Uygun geri çağrı ömür boyu yönetim altyapısı olduğu varsayılarak, std::function<int(int)>bir int(void*,int)stil geri aramasına dönüşen sarmalayıcılar yazabileceğinizi unutmayın . Bu yüzden herhangi bir C tarzı geri arama ömür boyu yönetim sistemi için bir duman testi olarak, bir sarma sarma std::functionmakul iyi çalışır emin olun .


1
Bu void*nereden geldi? Durumu neden işlevin ötesinde tutmak istersiniz? Bir işlev, ihtiyaç duyduğu tüm kodu, tüm işlevleri içermelidir, sadece istenen bağımsız değişkenleri iletir ve bir şeyi değiştirir ve döndürürsünüz. Bazı harici durumlara ihtiyacınız varsa neden bir functionPtr veya geri arama bu bagajı taşıyacaktır? Ben geri arama gereksiz karmaşık olduğunu düşünüyorum.
Nikos

@ nik-lz Size bir yorumda C'deki geri aramaların kullanımını ve geçmişini nasıl öğreteceğimden emin değilim. Veya işlevsel programlamanın aksine prosedür felsefesi. Yani, doldurulmadan bırakacaksın.
Yakk - Adam Nevraumont

Unuttum this. Bir üye işlev çağrılması durumunda hesaplamak zorunda olduğu için, bu yüzden thisnesnenin adresini işaret etmek için işaretçiye ihtiyacımız var mı? Eğer yanılıyorsam, bu konuda daha fazla bilgi bulabileceğim bir bağlantı verebilir misiniz, çünkü bu konuda fazla bir şey bulamıyorum. Şimdiden teşekkürler.
Nikos

@ Nik-Lz üye işlevleri işlev değildir. İşlevlerin (çalışma zamanı) durumu yoktur. Geri çağrılar void*, çalışma zamanı durumunun iletilmesine izin vermek için bir alır . A void*ve void*bağımsız değişkeni olan bir işlev işaretçisi bir nesneye üye işlev çağrısını taklit edebilir. Maalesef, "C geri arama mekanizmaları 101 tasarlama" yolunda ilerleyen bir kaynak bilmiyorum.
Yakk - Adam Nevraumont

Evet, bundan bahsettim. Çalışma zamanı durumu temelde çağrılan nesnenin adresidir (çünkü çalıştırmalar arasında değişir). Hala ilgili this. Demek istediğim şey o. Tamam, yine de teşekkürler.
Nikos

17

std::functionİsteğe bağlı çağrılabilir nesneleri saklamak için kullanın . Kullanıcının geri arama için gereken bağlamı sağlamasına olanak tanır; düz işlev işaretçisi bunu yapmaz.

Herhangi bir nedenden ötürü düz işlev işaretçileri kullanmanız gerekiyorsa (belki de C uyumlu bir API istediğiniz için), bir void * user_contextargüman eklemeniz gerekir; böylece, doğrudan işlevi.


Buradaki p türü nedir? std :: işlev türü olacak mı? void f () {}; otomatik p = f; (p);
sree

14

Bundan kaçınmanın tek nedeni std::function, C ++ 11'de tanıtılan bu şablon için destek bulunmayan eski derleyicilerin desteğidir.

C ++ 11 öncesi dili desteklemek bir zorunluluk değilse, kullanmak std::function, arayanlarınıza geri çağrıyı uygulamada daha fazla seçenek sunarak "düz" işlev işaretçileriyle karşılaştırıldığında daha iyi bir seçenek haline getirir. API'nızın kullanıcılarına daha fazla seçenek sunarken, geri aramayı gerçekleştiren kodunuz için uygulamalarının özelliklerini de soyutlar.


1

std::function VMT bazı durumlarda performans üzerinde bazı etkileri olan koda VMT getirebilir.


3
Bu VMT'nin ne olduğunu açıklayabilir misiniz?
Gupta
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.