İşlev imzalarında neden std :: enable_if kullanmam gerekiyor?


165

Scott Meyers bir sonraki kitabı EC ++ 11'in içeriğini ve statüsünü yayınladı . Kitaptaki bir öğenin " std::enable_ifİşlev imzalarından kaçın" olabileceğini yazdı .

std::enable_if işlevleri veya sınıfları aşırı yük çözünürlüğünden koşullu olarak kaldırmak için işlev bağımsız değişkeni, dönüş türü veya sınıf şablonu veya işlev şablonu parametresi olarak kullanılabilir.

Gelen bu soruya her üç çözüm gösterilmiştir.

Fonksiyon parametresi olarak:

template<typename T>
struct Check1
{
   template<typename U = T>
   U read(typename std::enable_if<
          std::is_same<U, int>::value >::type* = 0) { return 42; }

   template<typename U = T>
   U read(typename std::enable_if<
          std::is_same<U, double>::value >::type* = 0) { return 3.14; }   
};

Şablon parametresi olarak:

template<typename T>
struct Check2
{
   template<typename U = T, typename std::enable_if<
            std::is_same<U, int>::value, int>::type = 0>
   U read() { return 42; }

   template<typename U = T, typename std::enable_if<
            std::is_same<U, double>::value, int>::type = 0>
   U read() { return 3.14; }   
};

Dönüş türü olarak:

template<typename T>
struct Check3
{
   template<typename U = T>
   typename std::enable_if<std::is_same<U, int>::value, U>::type read() {
      return 42;
   }

   template<typename U = T>
   typename std::enable_if<std::is_same<U, double>::value, U>::type read() {
      return 3.14;
   }   
};
  • Hangi çözüm tercih edilmeli ve neden başkalarından kaçınmalıyım?
  • Hangi durumlarda " std::enable_ifİşlev imzalarından kaçın " , dönüş türü olarak kullanımla ilgilidir (normal işlev imzasının bir parçası değil, şablon uzmanlıklarının bir parçasıdır)?
  • Üye ve üye olmayan işlev şablonları için herhangi bir fark var mı?

Çünkü aşırı yükleme genellikle aynı derecede güzeldir. Herhangi bir şey varsa, (özel) sınıf şablonları kullanan bir uygulamaya temsilci seçin.
sehe

Üye fonksiyonları, aşırı yük setinin mevcut aşırı yükten sonra bildirilen aşırı yükleri içermesi bakımından farklılık gösterir . Bu özellikle değişken gecikmeli dönüş tipi (dönüş tipinin başka bir aşırı yüklenmeden
çıkacağı

1
Eh, sadece subjektif sık sık oldukça yararlı olurken ben sevmiyorum söylemek zorunda std::enable_ifbenim işlevi imzaları (özellikle çirkin ek yığılmayı için nullptrher zaman bir şey bir için (ne olduğunu gibi garip kesmek göründüğünden fonksiyon argümanı versiyonu) static ifkudreti çok daha güzel ve temiz yapın) interresting dil özelliğini kullanmak için kara büyü kullanarak şablonu. Bu yüzden mümkün olduğunda etiket göndermeyi tercih ederim (iyi, hala ek garip argümanlarınız var, ancak genel arayüzde değil, aynı zamanda daha az çirkin ve şifreli ).
Christian Rau

2
Ben gelmez Ne sormak istiyorum =0içinde typename std::enable_if<std::is_same<U, int>::value, int>::type = 0başarmak? Bunu anlamak için doğru kaynakları bulamadım. Önce ilk kısmı biliyorum =0üyesi türüne sahip intolmadığını Uve intaynıdır. Çok teşekkürler!
astroboylrx

4
@astroboylrx Komik, sadece bunu not ederek bir yorum yazacaktım. Temel olarak, bu = 0, bunun varsayılan, tür olmayan bir şablon parametresi olduğunu gösterir. Bu şekilde yapılır, çünkü varsayılan tip şablonu parametreleri imzanın bir parçası değildir, bu nedenle bunlara aşırı yüklenemezsiniz.
Nir Friedman

Yanıtlar:


107

Kesmeyi şablon parametrelerine koyun .

enable_ifŞablon parametresine yaklaşım diğerlerine en az iki avantajı vardır:

  • okunabilirlik : enable_if kullanımı ve return / argüman türleri, bir dizi tipin disambiguator ve iç içe tip erişiminin karışık bir kümesiyle birleştirilmez; disambiguator ve iç içe tipin dağınıklığı takma ad şablonlarıyla azaltılabilse de, yine de ilgisiz iki şeyi bir araya getirecektir. Enable_if kullanımı, dönüş türleriyle değil şablon parametreleriyle ilgilidir. Bunların şablon parametrelerinde bulunması, önemli olana daha yakın oldukları anlamına gelir;

  • evrensel uygulanabilirlik : kurucuların dönüş türleri yoktur ve bazı işleçlerin ek argümanları olamaz, bu nedenle diğer iki seçeneğin hiçbiri her yere uygulanamaz. Enable_if öğesini bir şablon parametresine koymak her yerde çalışır çünkü zaten SFINAE'yi şablonlarda kullanabilirsiniz.

Benim için okunabilirlik yönü bu seçimdeki en büyük motive edici faktör.


4
BuradakiFUNCTION_REQUIRES makroyu kullanmak, okumayı çok daha güzel hale getirir ve C ++ 03 derleyicilerinde de çalışır ve dönüş türünde kullanılmaya dayanır . Ayrıca, işlev şablonu parametrelerini kullanmak aşırı yükleme için sorunlara neden olur, çünkü şimdi işlev imzası benzersiz değildir ve belirsiz aşırı yükleme hatalarına neden olur. enable_ifenable_if
Paul Fultz II

3
Bu eski bir sorudur, ancak hala okuyan herkes için: @Paul tarafından ortaya konan sorunun çözümü enable_if, aşırı yüklemeye izin veren varsayılan olmayan türden bir şablon parametresiyle kullanmaktır. Yani enable_if_t<condition, int> = 0yerine typename = enable_if_t<condition>.
Nir Friedman

neredeyse statik-if için geri dönüş
davidbak

@ R.MartinhoFernandes Yorumunuzdaki flamingdangerzonebağlantı şimdi bir casus yazılım yükleme sayfasına götürüyor gibi görünüyor. Moderatörlerin dikkatini çekmek için işaretledim.
nispio

58

std::enable_ifşablon argümanı kesinti sırasında " Substition Failure Hata Değil " (SFINAE) prensibine dayanır . Bu çok kırılgan bir dil özelliğidir ve doğru olması için çok dikkatli olmanız gerekir.

  1. içindeki durumunuz iç enable_ifiçe geçmiş bir şablon veya tür tanımı içeriyorsa (ipucu: ::jetonları arayın ), bu iç içe tempatles veya türlerin çözünürlüğü genellikle kesinti yapılmayan bir bağlamdır . Bu tür bir çıkarılmamış bağlamdaki herhangi bir ikame hatası bir hatadır .
  2. birden fazla enable_ifaşırı yükteki çeşitli koşullar herhangi bir çakışma içeremez çünkü aşırı yük çözünürlüğü belirsiz olabilir. İyi bir derleyici uyarıları alsanız da, bu bir yazar olarak kendinizi kontrol etmeniz gereken bir şeydir.
  3. enable_ifaşırı yük çözünürlüğü sırasında diğer kapsamlardan getirilen diğer işlevlerin varlığına bağlı olarak (örneğin ADL aracılığıyla) şaşırtıcı etkileşimlere sahip olabilen canlı işlevler kümesini manipüle eder. Bu onu çok sağlam değil.

Kısacası, işe yaradığında işe yarıyor, ama yapmadığı zaman hata ayıklamak çok zor olabilir. Çok iyi bir alternatif kullanmaktır etiket gönderilmesini (genellikle bir uygulama işlevine temsilci, yani detailiçinde kullandığınız aynı derleme zamanı durumuna göre bir kukla argüman alır ad veya bir yardımcı sınıf olarak) enable_if.

template<typename T>
T fun(T arg) 
{ 
    return detail::fun(arg, typename some_template_trait<T>::type() ); 
}

namespace detail {
    template<typename T>
    fun(T arg, std::false_type /* dummy */) { }

    template<typename T>
    fun(T arg, std::true_type /* dummy */) {}
}

Etiket gönderme, aşırı yük kümesini değiştirmez, ancak derleme zamanı ifadesiyle (örneğin bir tür özelliğinde) uygun bağımsız değişkenleri sağlayarak tam olarak istediğiniz işlevi seçmenize yardımcı olur. Deneyimlerime göre, bu hata ayıklamak ve doğru almak çok daha kolay. Eğer sofistike tip özelliklerin arzulanan bir kütüphane yazarı iseniz, bir enable_ifşekilde ihtiyacınız olabilir , ancak derleme zamanı koşullarının en düzenli kullanımı için önerilmez.


22
Ancak etiket gönderme işleminin bir dezavantajı vardır: bir işlevin varlığını algılayan bir özelliğiniz varsa ve bu işlev etiket gönderme yaklaşımıyla uygulanırsa, her zaman bu üyeyi mevcut olarak rapor eder ve olası bir ikame hatası yerine hataya neden olur. . SFINAE esas olarak aşırı yükleri aday kümelerinden kaldırma tekniğidir ve etiket dağıtımı iki (veya daha fazla) aşırı yük arasında seçim yapma tekniğidir. İşlevsellikte bazı çakışmalar var, ancak eşdeğer değiller.
R. Martinho Fernandes

@ R.MartinhoFernandes kısa bir örnek verebilir ve nasıl enable_ifdoğru olacağını gösterebilir misiniz ?
TemplateRex

1
@ R.MartinhoFernandes Bence bu noktaları açıklayan ayrı bir cevap OP'ye değer katabilir. :-) BTW, özellikleri yazmak gibi is_f_ablebir şey olduğunu düşünüyorum bir kütüphane yazarları için bir görev olduğunu düşünüyorum tabii ki SFINAE kullanabilirsiniz onlara bir avantaj verir, ama "düzenli" kullanıcılar için ve bir özellik verilen is_f_able, ben etiket gönderme daha kolay olduğunu düşünüyorum.
TemplateRex

1
@hansmaad Sorunuzu ele alan kısa bir yanıt gönderdim ve bunun yerine bir blog yayınında "SFINAE'ye veya SFINAE'ye değil" konusunu ele alacağım (bu soruda biraz konu dışı). Bunu bitirmek için zaman alır almaz, yani.
R. Martinho Fernandes

8
SFINAE "kırılgan" mı? Ne?
Orbit'te

5

Hangi çözüm tercih edilmeli ve neden başkalarından kaçınmalıyım?

  • Template parametresi

    • Yapıcılarda kullanılabilir.
    • Kullanıcı tanımlı dönüşüm operatöründe kullanılabilir.
    • C ++ 11 veya üstünü gerektirir.
    • IMO, daha okunabilir.
    • Kolayca yanlış kullanılabilir ve aşırı yüklerle ilgili hatalar üretir:

      template<typename T, typename = std::enable_if_t<std::is_same<T, int>::value>>
      void f() {/*...*/}
      
      template<typename T, typename = std::enable_if_t<std::is_same<T, float>::value>>
      void f() {/*...*/} // Redefinition: both are just template<typename, typename> f()

    Bildirimi typename = std::enable_if_t<cond>yerine doğru birstd::enable_if_t<cond, int>::type = 0

  • dönüş türü:

    • Yapıcıda kullanılamaz. (dönüş türü yok)
    • Kullanıcı tanımlı dönüşüm işlecinde kullanılamaz. (çıkartılamaz)
    • C ++ 11 öncesi kullanmak olabilir.
    • Daha okunabilir ikinci IMO.
  • Son olarak, fonksiyon parametresinde:

    • C ++ 11 öncesi kullanmak olabilir.
    • Yapıcılarda kullanılabilir.
    • Kullanıcı tanımlı dönüşüm işlecinde kullanılamaz. (parametre yok)
    • Bu argümanların sabit sayıda yöntemlerde kullanılamaz (birli / ikili operatörler +, -, *, ...)
    • Kalıtımda güvenle kullanılabilir (aşağıya bakınız).
    • İşlev imzasını değiştirme (temel olarak son bağımsız değişken olarak fazladan bir seçeneğiniz vardır void* = nullptr) (böylece işlev işaretçisi farklı olur, vb.)

Üye ve üye olmayan işlev şablonları için herhangi bir fark var mı?

Miras ile ince farklılıklar vardır ve using:

Göre using-declarator(vurgu maden):

namespace.udecl

Using-declarator tarafından tanıtılan bildirim kümesi, tanımlayıcıdaki ad için tanımlanmış olarak gizli işlevler hariç olmak üzere nitelikli ad araması ([basic.lookup.qual], [class.member.lookup]) yaparak bulunur. altında.

...

Using-declarator, bir temel sınıftan bildirimleri türetilmiş bir sınıfa getirdiğinde, türetilmiş sınıftaki üye işlevleri ve üye işlev şablonları , aynı işlev , parametre türü-listesi, cv- ile üye işlevlerini ve üye işlev şablonlarını geçersiz kılar ve gizler. yeterlilik ve bir temel sınıftaki ( varsa) ref-niteleyicisi . Bu tür gizli veya geçersiz kılınan bildirimler, kullanım bildiricisi tarafından sunulan bildiriler kümesinin dışında tutulur.

Bu nedenle, hem şablon bağımsız değişkeni hem de dönüş türü için, yöntemler aşağıdaki senaryo gizlidir:

struct Base
{
    template <std::size_t I, std::enable_if_t<I == 0>* = nullptr>
    void f() {}

    template <std::size_t I>
    std::enable_if_t<I == 0> g() {}
};

struct S : Base
{
    using Base::f; // Useless, f<0> is still hidden
    using Base::g; // Useless, g<0> is still hidden

    template <std::size_t I, std::enable_if_t<I == 1>* = nullptr>
    void f() {}

    template <std::size_t I>
    std::enable_if_t<I == 1> g() {}
};

Demo (gcc yanlış temel işlevi bulur).

Tartışma ile benzer senaryo çalışır:

struct Base
{
    template <std::size_t I>
    void h(std::enable_if_t<I == 0>* = nullptr) {}
};

struct S : Base
{
    using Base::h; // Base::h<0> is visible

    template <std::size_t I>
    void h(std::enable_if_t<I == 1>* = nullptr) {}
};

gösteri

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.