RTTI kullanmak yerine neden 'saf polimorfizm' tercih edilir?


106

Bu tür şeyleri tartışan gördüğüm hemen hemen her C ++ kaynağı, RTTI (çalışma zamanı türü tanımlama) kullanmak yerine polimorfik yaklaşımları tercih etmem gerektiğini söylüyor. Genel olarak, bu tür tavsiyeleri ciddiye alıyorum ve mantığını anlamaya çalışacağım - sonuçta, C ++ muazzam bir canavar ve derinlemesine anlaşılması zor. Ancak, bu özel soru için bir boşluk bırakıyorum ve internetin ne tür tavsiyeler verebileceğini görmek istiyorum. Öncelikle, RTTI'nin neden "zararlı" kabul edildiğini söyleyen yaygın nedenleri listeleyerek şimdiye kadar öğrendiklerimi özetleyeyim:

Bazı derleyiciler bunu kullanmıyor / RTTI her zaman etkin değil

Bu argümana gerçekten inanmıyorum. Bu, C ++ 14 özelliklerini kullanmamam gerektiğini söylemek gibi, çünkü onu desteklemeyen derleyiciler var. Yine de kimse beni C ++ 14 özelliklerini kullanmaktan caydırmaz. Projelerin çoğu, kullandıkları derleyici ve nasıl yapılandırıldığı üzerinde etkiye sahip olacaktır. Gcc kılavuz sayfasından alıntı yapsak bile:

-fno-rtti

C ++ çalışma zamanı tür tanımlama özellikleri (dynamic_cast ve typeid) tarafından kullanılmak üzere sanal işlevlerle her sınıf hakkında bilgi üretmeyi devre dışı bırakın. Dilin bu kısımlarını kullanmazsanız, bu bayrağı kullanarak biraz alan kazanabilirsiniz. İstisna işlemenin aynı bilgileri kullandığını, ancak G ++ 'nın gerektiğinde bunları oluşturduğunu unutmayın. Dinamik yayın operatörü, çalışma zamanı tip bilgisi gerektirmeyen yayınlar için, yani "void *" e veya kesin temel sınıflara yayınlar için hala kullanılabilir.

Ne bu bana söyler olmasıdır eğer ben RTTI kullanarak değilim, bunu devre dışı bırakabilir. Bu, Boost kullanmıyorsanız, ona bağlanmanız gerekmediğini söylemek gibi bir şey. Birinin derlediği vaka için plan yapmama gerek yok -fno-rtti. Ayrıca, bu durumda derleyici yüksek sesle ve net bir şekilde başarısız olacaktır.

Ekstra belleğe mal olur / Yavaş olabilir

Ne zaman RTTI kullanmaya istekli olsam, bu, sınıfımın bir tür tip bilgisine veya özelliğine erişmem gerektiği anlamına geliyor. RTTI kullanmayan bir çözüm uygularsam, bu genellikle bu bilgiyi depolamak için sınıflarıma bazı alanlar eklemem gerektiği anlamına gelir, bu nedenle bellek argümanı bir çeşit geçersizdir (bunun bir örneğini daha aşağıda vereceğim).

Dinamik yayın gerçekten yavaş olabilir. Yine de, genellikle hız açısından kritik durumlarda kullanmaktan kaçınmanın yolları vardır. Ve alternatifi tam olarak görmüyorum. Bu SO yanıtı , türü depolamak için temel sınıfta tanımlanan bir enum kullanılmasını önerir. Bu yalnızca türetilmiş sınıflarınızı önceden biliyorsanız çalışır. Bu oldukça büyük bir "eğer"!

Bu yanıttan, RTTI'nin maliyeti de net değil gibi görünüyor. Farklı insanlar farklı şeyleri ölçer.

Zarif polimorfik tasarımlar RTTI'yi gereksiz kılacaktır

Bu ciddiye aldığım bir tavsiye. Bu durumda, RTTI kullanım durumumu kapsayan iyi RTTI olmayan çözümler bulamıyorum. Bir örnek vereyim:

Bazı tür nesnelerin grafiklerini işlemek için bir kütüphane yazdığımı varsayalım. Kullanıcıların kitaplığımı kullanırken kendi türlerini oluşturmalarına izin vermek istiyorum (bu nedenle enum yöntemi kullanılamaz). Düğümüm için bir temel sınıfım var:

class node_base
{
  public:
    node_base();
    virtual ~node_base();

    std::vector< std::shared_ptr<node_base> > get_adjacent_nodes();
};

Şimdi, düğümlerim farklı türlerde olabilir. Peki ya bunlar:

class red_node : virtual public node_base
{
  public:
    red_node();
    virtual ~red_node();

    void get_redness();
};

class yellow_node : virtual public node_base
{
  public:
    yellow_node();
    virtual ~yellow_node();

    void set_yellowness(int);
};

Cehennem, neden bunlardan biri bile değil:

class orange_node : public red_node, public yellow_node
{
  public:
    orange_node();
    virtual ~orange_node();

    void poke();
    void poke_adjacent_oranges();
};

Son işlev ilginç. İşte yazmanın bir yolu:

void orange_node::poke_adjacent_oranges()
{
    auto adj_nodes = get_adjacent_nodes();
    foreach(auto node, adj_nodes) {
        // In this case, typeid() and static_cast might be faster
        std::shared_ptr<orange_node> o_node = dynamic_cast<orange_node>(node);
        if (o_node) {
             o_node->poke();
        }
    }
}

Bunların hepsi açık ve temiz görünüyor. İhtiyacım olmayan öznitelikleri veya yöntemleri tanımlamama gerek yok, temel düğüm sınıfı zayıf ve anlamsız kalabilir. RTTI olmadan nereden başlamalıyım? Belki temel sınıfa bir node_type özniteliği ekleyebilirim:

class node_base
{
  public:
    node_base();
    virtual ~node_base();

    std::vector< std::shared_ptr<node_base> > get_adjacent_nodes();

  private:
    std::string my_type;
};

Std :: string bir tür için iyi bir fikir mi? Belki hayır, ama başka ne kullanabilirim? Bir numara oluşturun ve henüz kimsenin kullanmadığını umuyor musunuz? Ayrıca, orange_node'um durumunda, red_node ve yellow_node'daki yöntemleri kullanmak istersem ne olur? Düğüm başına birden çok tür depolamam gerekir mi? Bu karmaşık görünüyor.

Sonuç

Bu örnekler aşırı derecede karmaşık veya sıradışı görünmüyor (Düğümlerin yazılım aracılığıyla kontrol edilen ve ne olduklarına bağlı olarak çok farklı şeyler yapan gerçek donanımı temsil ettiği günlük işimde benzer bir şey üzerinde çalışıyorum). Yine de bunu şablonlarla veya diğer yöntemlerle yapmanın net bir yolunu bilmiyordum. Lütfen örneğimi savunmak yerine sorunu anlamaya çalıştığımı unutmayın. Yukarıda bağlantılandırdığım SO yanıtı ve Wikibooks'taki bu sayfa gibi sayfaları okumam, RTTI'yi kötüye kullandığımı gösteriyor gibi görünüyor, ancak nedenini öğrenmek istiyorum.

Öyleyse, orijinal soruma geri dönelim: Neden 'saf polimorfizm' RTTI kullanmaya tercih edilir?


9
Kurutma portakal örneğinizi çözmek için "kaçırdığınız" (bir dil özelliği olarak), çoklu gönderim ("çoklu yöntemler") olacaktır. Bu nedenle, bunu taklit etmenin bir alternatif olabileceğini düşünmek. Bu nedenle genellikle ziyaretçi kalıbı kullanılır.
Daniel Jour

1
Bir dizeyi tür olarak kullanmak çok yardımcı olmaz. Bir "tip" sınıfının bir örneğine bir işaretçi kullanmak bunu daha hızlı hale getirir. Ancak, temelde RTTI'nin yapacağı şeyi manuel olarak yapıyorsunuz.
Daniel Jour

4
@MargaretBloom Hayır, olmayacak, RTTI, Runtime Type Information ( Çalışma Zamanı Tipi Bilgileri) anlamına gelirken CRTP yalnızca şablonlar içindir - statik tipler, bu yüzden.
edmz

2
@ mbr0wn: tüm mühendislik süreçleri bazı kurallara bağlıdır; programlama bir istisna değildir. Kurallar iki gruba ayrılabilir: esnek kurallar (GEREKLİ) ve katı kurallar (ZORUNLU). (Ayrıca tabiri caizse bir tavsiye / seçenek bölümü (COULD) de var.) C / C ++ standardının (ya da gerçek için başka bir mühendislik standardının) bunları nasıl tanımladığını okuyun. Sanırım sorununuz, zor bir kural olarak "RTTI kullanma" yı ("RTTI KULLANMAYINIZ") yanlış anlamanızdan kaynaklanıyor. Aslında yumuşak bir kuraldır ("RTTI KULLANMAMALISINIZ"), yani mümkün olduğunda ondan kaçınmalısınız - ve bunu yapmaktan kaçınamadığınızda kullanın

3
Pek çok cevabın, örneğinizin önerdiği fikrinin node_basebir kütüphanenin parçası olduğunu ve kullanıcıların kendi düğüm türlerini oluşturacağını not etmediğine dikkat ediyorum . O zaman başka bir çözüme izin vermek için değişiklik yapamazlarnode_base , bu yüzden belki RTTI onların en iyi seçeneği olabilir. Öte yandan, böyle bir kitaplık tasarlamanın başka yolları da vardır, böylece yeni düğüm türleri RTTI kullanmaya gerek kalmadan (ve yeni düğüm türlerini tasarlamanın başka yollarını da) çok daha zarif bir şekilde sığabilir.
Matthew Walton

Yanıtlar:


69

Bir arayüz, koddaki belirli bir durumda etkileşim kurmak için neleri bilmesi gerektiğini açıklar. Arayüzü "tüm tür hiyerarşiniz" ile genişlettiğinizde, arayüz "yüzey alanınız" çok büyük hale gelir ve bu da onun hakkında akıl yürütmeyi zorlaştırır .

Örnek olarak, "bitişik portakalları dürtün", bir 3. taraf olarak, turuncu olmayı taklit edemeyeceğim anlamına geliyor! Özel olarak turuncu bir tür ilan ettiniz, ardından kodunuzun o türle etkileşimde özel davranmasını sağlamak için RTTI'yi kullandınız. "Turuncu olmak" istiyorsam , özel bahçenizde olmalıyım .

Artık "orangity" ile çiftleşen herkes, tanımlanmış bir arayüz yerine tüm portakal türünüzle ve örtük olarak tüm özel bahçenizle eşleşir.

İlk bakışta bu, tüm istemcileri değiştirmek zorunda kalmadan (ekleme am_I_orange) sınırlı arayüzü genişletmenin harika bir yolu gibi görünse de , bunun yerine kod tabanını kemikleştirme ve daha fazla genişletmeyi engelleme eğilimi vardır . Özel renklilik, sistemin işleyişine özgü hale gelir ve portakal için farklı şekilde uygulanan ve belki bir bağımlılığı ortadan kaldıran veya başka bir sorunu zarif bir şekilde çözen bir "mandalina" ikamesi oluşturmanızı engeller.

Bu, arayüzünüzün probleminizi çözmek için yeterli olması gerektiği anlamına gelir. Bu perspektiften, neden sadece portakalları dürtmeniz gerekiyor ve öyleyse neden portakallık arayüzde mevcut değildi? Geçici olarak eklenebilecek bazı belirsiz etiketlere ihtiyacınız varsa, bunu türünüze ekleyebilirsiniz:

class node_base {
  public:
    bool has_tag(tag_name);

Bu, arayüzünüzün dar bir şekilde belirtilenden geniş etiket tabanlıya kadar benzer şekilde genişlemesini sağlar. Bunu RTTI ve uygulama ayrıntıları aracılığıyla yapmak yerine (diğer bir deyişle, "nasıl uygulandınız? Turuncu türle? Tamam, geçersiniz."), Bunu tamamen farklı bir uygulama yoluyla kolayca taklit edilen bir şeyle yapar.

Bu bile, dinamik yöntemlere uzatılabilir eğer bunu gerekir. "Baz, Tom ve Alice argümanlarıyla Foo'lu olmayı destekliyor musun? Tamam, seni kandırıyorum." Büyük anlamda, bu, diğer nesnenin bildiğiniz bir tür olduğu gerçeğini anlamak için dinamik bir oyuncu kadrosundan daha az müdahaleci.

Artık mandalina nesneleri turuncu etikete sahip olabilir ve uygulama ayrıştırılırken birlikte oynayabilir.

Yine de büyük bir karışıklığa yol açabilir, ancak en azından bir mesaj ve veri karmaşasıdır, uygulama hiyerarşileri değil.

Soyutlama, ilgisizlikleri ayırma ve gizleme oyunudur. Kodun yerel olarak akıl yürütmesini kolaylaştırır. RTTI, soyutlamadan doğrudan uygulama ayrıntılarına doğru bir delik açıyor. Bu, bir sorunu çözmeyi kolaylaştırabilir, ancak sizi gerçekten kolayca belirli bir uygulamaya kilitlemenin maliyeti vardır.


14
En son paragraf için +1; Sadece sizinle aynı fikirde olduğum için değil, buradaki çivi çakma olduğu için.

7
Bir nesnenin bu işlevi desteklediğini bildiğinde, belirli bir işlevselliğe nasıl ulaşılır? Ya bu, atmayı içerir ya da her olası üye işlevine sahip Tanrı sınıfı vardır. İlk olasılık ya denetlenmemiş çevrimdir, bu durumda etiketleme sadece kişinin kendi çok hatalı dinamik tip kontrol şemasıdır ya da işaretlidir dynamic_cast(RTTI), bu durumda etiketler gereksizdir. İkinci olasılık, bir Tanrı sınıfı, tiksindiricidir. Özetle, bu yanıtın Java programcılarına kulağa hoş geldiğini düşündüğüm birçok kelime var, ancak gerçek içerik anlamsız.
Şerefe ve hth. - Alf

2
@Falco: Bu (bir varyantı) bahsettiğim ilk olasılık, etikete göre kontrol edilmeyen yayın. Burada etiketleme, kişinin kendi çok kırılgan ve çok yanılabilir dinamik tip kontrol şemasıdır. Herhangi bir küçük istemci kodu yanlış davranışı ve C ++ 'da biri UB bölgesinde kapalıdır. Java'da olabileceğiniz gibi istisnalar almazsınız, ancak çökmeler ve / veya yanlış sonuçlar gibi Tanımsız Davranışlar elde edersiniz. Son derece güvenilmez ve tehlikeli olmasının yanı sıra, daha mantıklı C ++ koduna kıyasla son derece verimsizdir. IOW., Bu çok kötü; son derece çok.
Şerefe ve hth. - Alf

1
Uhm. :) Bağımsız değişken türleri?
Şerefe ve hth. - Alf

2
@JojOatXGME: Çünkü "polimorfizm", çeşitli türlerle çalışabilmek anlamına geliyor. Belirli bir tür olup olmadığını kontrol etmeniz gerekiyorsa, başlangıçta işaretçiyi / başvuruyu almak için kullandığınız zaten var olan tür kontrolünün ötesinde, o zaman polimorfizmin arkasına bakıyorsunuz demektir. Çeşitli türlerle çalışmıyorsunuz; belirli bir türle çalışıyorsun . Evet, Java'da bunu yapan "(büyük) projeler" var. Ama bu Java ; dil yalnızca dinamik çok biçimliliğe izin verir. C ++ da statik polimorfizme sahiptir. Ayrıca, sırf "büyük" birinin yapması onu iyi bir fikir yapmaz.
Nicol Bolas

31

Bu ya da bu özelliğe karşı ahlaki ikna etmenin çoğu, bu özelliğin bir dizi yanlış anlaşılmış kullanımı olduğu gözleminden kaynaklanmaktadır .

Nerede ahlâkçılar başarısız olurken onlar, TÜM kullanımları yanlış anlaşılacak bir edilir tahmin olmasıdır aslında özellikleri bir sebebi vardır.

Hepsi düşünüyorum: Onlar "tesisatçı kompleksi" derdi var musluklar vardır arızalı onlar tamir etmek denir tüm musluklar çünkü. Gerçek şu ki, çoğu musluk iyi çalışıyor: onlar için bir tesisatçı çağırmazsınız!

Programcıların, belirli bir özelliği kullanmaktan kaçınmak için, aslında bu özelliği özel olarak yeniden uygulayarak pek çok standart kod yazmaları çılgınca olabilir. (Hiç RTTI veya sanal çağrılar kullanmayan, ancak hangi gerçek türetilmiş tür olduklarını izlemek için bir değere sahip olan sınıflarla tanıştınız mı? Bu , kılık değiştirmiş RTTI yeniden keşfinden başka bir şey değil .)

Polimorfizm düşünmek genel bir yol yoktur: IF(selection) CALL(something) WITH(parameters). (Üzgünüm ama programlama, soyutlamayı göz ardı ederken, tamamen bununla ilgilidir)

Tasarım zamanı (kavramlar) derleme zamanı (şablon kesinti tabanlı), çalışma zamanı (miras ve sanal işlev tabanlı) veya veri odaklı (RTTI ve anahtarlama) polimorfizminin kullanımı, kararların ne kadarının bilindiğine bağlıdır. üretimin her aşamasında ve her bağlamda ne kadar değişken oldukları.

Fikir şudur:

ne kadar çok tahmin ederseniz, hataları yakalama ve son kullanıcıyı etkileyen hataları önleme şansı o kadar artar.

Her şey sabitse (veriler dahil) her şeyi şablon meta programlamasıyla yapabilirsiniz. Gerçekleştirilmiş sabitler üzerinde derleme gerçekleştikten sonra, tüm program sadece sonucu ortaya çıkaran bir dönüş ifadesine dönüşür .

Derleme zamanında bilinen birkaç durum varsa, ancak bunların üzerinde hareket etmeleri gereken gerçek verileri bilmiyorsanız, derleme zamanı polimorfizmi (özellikle CRTP veya benzeri) bir çözüm olabilir.

Vakaların seçimi verilere bağlıysa (derleme zamanı bilinen değerlere değil) ve anahtarlama tek boyutluysa (ne yapılması gerektiği yalnızca bir değere indirgenebilir), o zaman sanal işlev tabanlı gönderim (veya genel olarak "işlev işaretçisi tabloları) ") gereklidir.

Anahtarlama çok boyutluysa, C ++ 'da yerel birden çok çalışma zamanı gönderimi olmadığından, şunlardan birini yapmanız gerekir:

  • Goedelization ile tek boyuta küçültün : elmaslar ve yığılmış paralelkenarlar ile sanal tabanların ve çoklu kalıtımın olduğu yer burasıdır , ancak bu, bilinmesi ve nispeten küçük olması için olası kombinasyon sayısının olmasını gerektirir.
  • Boyutları birbirine zincirleyin (bileşik ziyaretçi modelinde olduğu gibi, ancak bu, tüm sınıfların diğer kardeşlerinden haberdar olmasını gerektirir, bu nedenle tasarlandığı yerden "ölçeklenemez")
  • Çağrıları birden çok değere göre gönderin. RTTI tam olarak bunun için.

Yalnızca geçiş değil, eylemler bile derleme zamanı bilinmiyorsa, o zaman komut dosyası oluşturma ve ayrıştırma gereklidir: verilerin kendileri, bunlar üzerinde yapılacak eylemi tanımlamalıdır.

Şimdi, numaralandırdığım vakaların her biri, onu takip eden özel bir durum olarak görülebildiğinden, her sorunu en alttaki çözümü kötüye kullanarak çözebilirsin, aynı zamanda en iyilerle karşılanabilir sorunlar için.

Ahlaklaşmanın aslında kaçınmaya zorladığı şey budur . Ancak bu, çoğu alanda yaşayan sorunların olmadığı anlamına gelmez!

RTTI'ye sadece vurmak için vurmak, sadece vurmak için vurmak gibidir goto. Papağanlar için şeyler, programcılar için değil.


Her yaklaşımın uygulanabilir olduğu seviyelerin iyi bir hesabı. Yine de "Goedelization" ı duymadım - başka bir adla da biliniyor mu? Belki bir bağlantı veya daha fazla açıklama ekleyebilir misiniz? Teşekkürler :)
j_random_hacker

1
@j_random_hacker: Ben de Godelization'ın bu kullanımını merak ediyorum. Normalde biri ilk olarak Godelization'ı düşünür, bir dizeden bir tam sayıya eşleme ve ikincisi, bu tekniği resmi dillerde kendine atıfta bulunan ifadeler üretmek için kullanır. Sanal gönderim bağlamında bu terime aşina değilim ve daha fazlasını öğrenmek isterim.
Eric Lippert

1
Aslında terimi kötüye kullanıyorum : Goedle'a göre, her tam sayı bir n-ple'ye (asal çarpanlarının kuvvetleri) karşılık geldiğinden ve her n-ple bir tam sayıya karşılık geldiğinden, her n-boyutlu indeksleme problemi olabilir tek boyutlu olana indirgenmiştir . Bu, bunu yapmanın tek ve tek yolu olduğu anlamına gelmez: sadece "mümkün" demenin bir yolu. Tek ihtiyacınız olan bir "böl ve yönet" mekanizmasıdır. sanal işlevler "bölmek" ve çoklu kalıtım "fethetmek" tir.
Emilio Garavaglia

... Sonlu bir alan (bir aralık) içinde gerçekleşen her şey doğrusal kombinasyonlar daha etkilidir (klasik i = r * C + c, bir matrisin hücresindeki bir dizide indeksi elde eder). Bu durumda, bölme kimliği "ziyaretçi" ve fetih "bileşik" dir. Doğrusal cebir söz konusu olduğu için, bu durumda teknik "köşegenleştirme" ye karşılık gelir
Emilio Garavaglia

Bunların hepsini teknik olarak düşünmeyin. Bunlar sadece benzetmelerdir
Emilio Garavaglia

23

Küçük bir örnekte oldukça düzgün görünüyor, ancak gerçek hayatta kısa süre sonra birbirini dürtebilen uzun bir dizi türle karşılaşacaksınız, bazıları belki de yalnızca tek bir yönde.

Peki dark_orange_nodeya black_and_orange_striped_nodeya ya ya dotted_node? Farklı renklerde noktalar olabilir mi? Ya çoğu nokta turuncuysa, o zaman itilebilir mi?

Ve her yeni bir kural eklemeniz gerektiğinde, tüm poke_adjacentişlevleri tekrar gözden geçirmeniz ve daha fazla if-ifadesi eklemeniz gerekecektir .


Her zaman olduğu gibi, genel örnekler oluşturmak zordur, size bunu vereceğim.

Ancak bu özel örneği yapacak olsaydım, poke()tüm sınıflara bir üye ekler ve void poke() {}ilgilenmiyorlarsa bazılarının call ( ) ' yı görmezden gelmesine izin verirdim .

Elbette bu, e'leri karşılaştırmaktan daha ucuz olurdu typeid.


3
"Kesinlikle" diyorsun, ama seni bu kadar emin kılan nedir? Gerçekten anlamaya çalıştığım şey bu. Diyelim ki orange_node'u pokable_node olarak yeniden adlandırdım ve poke () olarak adlandırabileceğim tek şey onlar. Bu, arayüzümün, örneğin bir istisna atan bir poke () yöntemi uygulaması gerekeceği anlamına gelir ("bu düğüm pokable değildir"). Bu daha pahalı görünüyor .
mbr0wn

2
Neden bir istisna yapmak zorunda kalsın? Arayüzün "poke edilebilir" olup olmadığına dikkat ettiyseniz, sadece "isPokeable" fonksiyonunu ekleyin ve dürtme fonksiyonunu çağırmadan önce onu çağırın. Ya da sadece söylediği şeyi yapın ve "dürtünemez sınıflarda hiçbir şey yapma".
Brandon

1
@ mbr0wn: Daha iyi soru, neden pokable ve nonpokable node'ların aynı temel sınıfı paylaşmasını istediğinizdir.
Nicol Bolas

2
@NicolBolas Neden dost canlısı ve düşman canavarların aynı temel sınıfı, odaklanabilir ve odaklanamayan UI öğelerini veya sayısal tuş takımı ve klavyeli klavyeleri olan klavyeleri paylaşmasını istersiniz?
user253751

1
@ mbr0wn Bu davranış kalıbı gibi geliyor. Baz arayüzü iki yöntemi vardır supportsBehaviourve invokeBehaviourher sınıf davranışları Listesini sahip olabilir. Davranışlardan biri Poke olabilir ve dürtüklenmek isteyen tüm sınıflar tarafından desteklenen Davranışlar listesine eklenebilir.
Falco

20

Bazı derleyiciler bunu kullanmıyor / RTTI her zaman etkin değil

Bu tür argümanları yanlış anladığınıza inanıyorum.

RTTI'nin kullanılmayacağı birkaç C ++ kodlama yeri vardır. Derleyici anahtarlarının RTTI'yi zorla devre dışı bırakmak için kullanıldığı yerlerde. Böyle bir paradigma içinde kod yazıyorsanız ... o zaman neredeyse kesinlikle bu kısıtlamadan zaten haberdar olmuşsunuzdur.

Bu nedenle sorun kütüphanelerde . Eğer RTTI bağlıdır kütüphane yazıyorsanız belirtecektim, ardından kütüphane olamaz RTTI kapatmak kullanıcılar tarafından kullanılabilir. Kitaplığınızın bu kişiler tarafından kullanılmasını istiyorsanız, kitaplığınız RTTI kullanabilen kişiler tarafından da kullanılsa bile, RTTI kullanamaz. Aynı derecede önemlisi, RTTI kullanamıyorsanız, kütüphaneler için biraz daha fazla alışveriş yapmanız gerekir, çünkü RTTI kullanımı sizin için bir anlaşma kırıcıdır.

Ekstra belleğe mal olur / Yavaş olabilir

Sıcak döngülerde yapmadığınız birçok şey vardır. Bellek ayırmıyorsunuz. Bağlantılı listelerde yineleme yapmazsınız. Ve benzeri. RTTI kesinlikle şu "bunu burada yapma" şeylerinden biri olabilir.

Ancak, tüm RTTI örneklerinizi göz önünde bulundurun. Her durumda, belirsiz türde bir veya daha fazla nesneye sahipsiniz ve bunlardan bazıları için mümkün olmayabilecek bazı işlemler yapmak istiyorsunuz.

Bu, tasarım düzeyinde çalışmanız gereken bir şey . "STL" paradigmasına uyan bellek ayırmayan kaplar yazabilirsiniz. Bağlantılı liste veri yapılarından kaçınabilir veya bunların kullanımını sınırlayabilirsiniz. Yapı dizilerini dizi yapılarına veya herhangi bir şekilde yeniden düzenleyebilirsiniz. Bazı şeyleri değiştirir ama onu bölümlere ayırabilirsiniz.

Karmaşık bir RTTI işlemini normal bir sanal işlev çağrısına dönüştürmek mi? Bu bir tasarım sorunu. Bunu değiştirmeniz gerekiyorsa, türetilmiş her sınıfta değişiklik yapılmasını gerektiren bir şeydir . Çok sayıda kodun çeşitli sınıflarla nasıl etkileşime girdiğini değiştirir. Böyle bir değişikliğin kapsamı, kodun performans açısından kritik bölümlerinin çok ötesine uzanır.

Öyleyse ... neden başlangıçta yanlış yazdınız?

İhtiyacım olmayan öznitelikleri veya yöntemleri tanımlamama gerek yok, temel düğüm sınıfı zayıf ve anlamsız kalabilir.

Hangi sona?

Temel sınıfın "zayıf ve kaba" olduğunu söylüyorsunuz. Ama gerçekten ... yok . Aslında hiçbir şey yapmaz .

Sadece örneği inceleyelim: node_base. Bu ne? Başka şeylere bitişik olan bir şey gibi görünüyor. Bu bir Java arabirimidir (ön jenerik Java): Yalnızca kullanıcıların gerçek türe dönüştürebilecekleri bir şey olması için var olan bir sınıf . Belki bitişiklik gibi bazı temel özellikler eklersiniz (Java ekler ToString), ama hepsi bu.

"Yalın ve ortalama" ile "şeffaf" arasında bir fark vardır.

Yakk'in dediği gibi, bu tür programlama stilleri birlikte çalışabilirlik konusunda kendilerini sınırlar, çünkü tüm işlevsellik türetilmiş bir sınıftaysa, bu türetilmiş sınıfa erişimi olmayan bu sistemin dışındaki kullanıcılar sistemle birlikte çalışamaz. Sanal işlevleri geçersiz kılamaz ve yeni davranışlar ekleyemezler. Hatta olamaz diyoruz bu işlevleri.

Ama aynı zamanda yaptıkları şey, sistem içinde bile yeni şeyler yapmayı büyük bir acı haline getirmektir. poke_adjacent_orangesİşlevinizi düşünün . Birisi lime_nodetıpkı orange_nodes gibi dürtülen bir tip isterse ne olur ? Eh, türetmek edemez lime_nodegelen orange_node; bu hiç mantıklı değil.

Bunun yerine, yeni bir lime_nodetüretilmiş eklemeliyiz node_base. Sonra ismini değiştirmek poke_adjacent_orangesiçin poke_adjacent_pokables. Ve sonra, orange_nodeve lime_node; Hangi oyuncu kadrosu çalışıyorsa, dürttüğümüz oydur.

Ancak, lime_nodebu kadar ihtiyacı kendi poke_adjacent_pokables . Ve bu fonksiyonun aynı döküm kontrollerini yapması gerekiyor.

Ve üçüncü bir tür eklersek, sadece kendi işlevini eklememeli, diğer iki sınıftaki işlevleri de değiştirmeliyiz.

Açıkçası, şimdi poke_adjacent_pokablesücretsiz bir işlev yapıyorsunuz , böylece hepsi için çalışıyor. Ama birisi dördüncü bir tür ekler ve onu bu işleve eklemeyi unutursa ne olur sence?

Merhaba sessiz kırılma . Program az çok iyi çalışıyor gibi görünüyor, ancak öyle değil. Olsaydı pokebir olmuş gerçek bir sanal fonksiyon, derleyici size saf sanal işlevi geçersiz vermedi zaman başarısız olurdu node_base.

Sizin yönteminizle, böyle bir derleyici denetimine sahip değilsiniz. Elbette, derleyici saf olmayan sanalları kontrol etmeyecektir, ancak en azından korumanın mümkün olduğu durumlarda korumaya sahipsiniz (yani: varsayılan işlem yoktur).

RTTI ile şeffaf temel sınıfların kullanılması bir bakım kabusuna yol açar. Aslında, RTTI'nin çoğu kullanımı bakım baş ağrısına yol açar. Bu, RTTI'nin yararlı ( boost::anyörneğin, iş yapmak için hayati önem taşır ). Ancak çok özel ihtiyaçlar için çok özel bir araçtır .

Bu şekilde, aynı şekilde "zararlı" dır. goto . Bu, ortadan kaldırılmaması gereken yararlı bir araçtır. Ancak kodunuzda kullanımı nadir olmalıdır .


Öyleyse, şeffaf temel sınıfları ve dinamik döküm kullanamıyorsanız, şişman arayüzlerden nasıl kaçınırsınız? Bir tür üzerinde çağırmak isteyebileceğiniz her işlevi köpürmeden temel sınıfa yükseltmekten nasıl kaçınırsınız?

Cevap, temel sınıfın ne için olduğuna bağlıdır.

Şeffaf temel sınıflar gibi node_base , problem için yanlış aracı kullanıyorlar. Bağlı listeler en iyi şekilde şablonlarla işlenir. Düğüm türü ve bitişikliği, bir şablon türü tarafından sağlanacaktır. Listeye polimorfik bir tür eklemek isterseniz, yapabilirsiniz. Sadece şablon argümanında BaseClass*olduğu gibi kullanın T. Veya tercih ettiğiniz akıllı işaretçi.

Ancak başka senaryolar da var. Biri, çok şey yapan, ancak bazı isteğe bağlı parçaları olan bir türdür. Belirli bir örnek belirli işlevleri uygulayabilirken bir diğeri yapmayabilir. Bununla birlikte, bu tür türlerin tasarımı genellikle uygun bir cevap sunar.

"Entite" sınıfı bunun mükemmel bir örneğidir. Bu sınıf oyun geliştiricilere çoktan bela oldu. Kavramsal olarak, neredeyse bir düzine, tamamen farklı sistemin kesişme noktasında yaşayan devasa bir arayüze sahiptir. Ve farklı varlıkların farklı özellikleri vardır. Bazı varlıkların herhangi bir görsel temsili yoktur, bu nedenle işleme işlevleri hiçbir şey yapmaz. Ve bunların hepsi çalışma zamanında belirlenir.

Bunun modern çözümü, bileşen tarzı bir sistemdir. Entitysadece aralarında biraz tutkal bulunan bir dizi bileşenden oluşan bir kaptır. Bazı bileşenler isteğe bağlıdır; Görsel temsili olmayan bir varlık "grafik" bileşenine sahip değildir. AI olmayan bir varlığın "denetleyici" bileşeni yoktur. Ve benzeri.

Böyle bir sistemdeki varlıklar, arabirimlerinin çoğu bileşenlere doğrudan erişilerek sağlanan bileşenlere yalnızca işaretçilerdir.

Böyle bir bileşen sistemi geliştirmek, tasarım aşamasında, belirli işlevlerin kavramsal olarak bir arada gruplandırıldığının farkına varmayı gerektirir, öyle ki birini uygulayan tüm tipler hepsini uygulayacaktır. Bu, sınıfı olası temel sınıftan çıkarmanıza ve onu ayrı bir bileşen haline getirmenize olanak tanır.

Bu aynı zamanda Tek Sorumluluk İlkesinin izlenmesine de yardımcı olur. Böyle bir bileşen sınıf, yalnızca bileşenlerin sahibi olma sorumluluğuna sahiptir.


Matthew Walton'dan:

Pek çok cevabın, örneğinizin node_base'in bir kitaplığın parçası olduğunu ve kullanıcıların kendi düğüm türlerini oluşturacağını önerdiği fikrine dikkat etmediğini unutmayın. O zaman başka bir çözüme izin vermek için node_base'i değiştiremezler, bu yüzden belki RTTI onların en iyi seçeneği olabilir.

Tamam, bunu keşfedelim.

Bunun mantıklı olması için, sahip olmanız gereken, bir L kütüphanesinin bir kap veya başka yapılandırılmış veri sahibi sağladığı bir durumdur. Kullanıcı bu konteynere veri ekleyebilir, içeriğini yineleyebilir, vb. Ancak, kütüphane bu verilerle gerçekten hiçbir şey yapmaz; sadece varlığını yönetir.

Ama varlığını yıkımı kadar yönetmiyor bile . Bunun nedeni, RTTI'yi bu tür amaçlar için kullanmanız bekleniyorsa, L'nin cahil olduğu sınıflar oluşturuyorsunuz. Bu , kodunuzun nesneyi tahsis ettiği ve onu yönetim için L'ye verdiği anlamına gelir .

Şimdi, bunun gibi bir şeyin meşru bir tasarım olduğu durumlar var. Olay sinyallemesi / mesaj geçişi, iş parçacığı güvenli iş kuyrukları, vb. Buradaki genel model şudur: Birisi, herhangi bir tür için uygun olan iki kod parçası arasında bir hizmet gerçekleştiriyor, ancak hizmetin ilgili belirli türlerin farkında olması gerekmiyor .

C'de bu kalıp yazılmıştır void*ve kullanımı kırılmamak için büyük özen gerektirir. C ++ 'da bu kalıp yazılır std::experimental::any(yakında yazılacak std::any).

Bunun çalışması gereken yol , L'nin gerçek verilerinizi temsil eden bir node_basesınıf sağlamasıdır any. Mesajı, iş parçacığı kuyruğu çalışma öğesini veya yaptığınız her şeyi aldığınızda, bunu anyhem gönderenin hem de alıcının bildiği uygun türüne çevirirsiniz.

Bunun yerine türetmek arasında orange_nodegelen node_data, sadece bir sopa orangeiçini node_databireyin anyüyesi alanına. Son kullanıcı bunu çıkarır ve any_castdönüştürmek için kullanır orange. Oyuncu kadrosu başarısız olursa, o zaman değildi orange.

Şimdi, uygulamasına hiç aşina iseniz, anymuhtemelen "bir dakika bekleyin: iş yapmak için any dahili olarak RTTI kullanır " diyeceksiniz any_cast. Buna cevap vereceğim "... evet".

Soyutlamanın amacı budur . Ayrıntıların derinliklerinde birisi RTTI kullanıyor. Ancak, çalışmanız gereken düzeyde, doğrudan RTTI yapmanız gereken bir şey değildir.

Size istediğiniz işlevselliği sağlayan türler kullanmalısınız. Sonuçta, gerçekten RTTI istemiyorsunuz. İstediğiniz, belirli bir türdeki bir değeri depolayabilen, istenen hedef dışındaki herkesten gizleyebilen, ardından depolanan değerin gerçekte bu türden olduğunu doğrulayarak o türe dönüştürülebilen bir veri yapısıdır.

Buna denir any. O kullanır RTTI fakat kullanarak anydaha düzgün istenen anlambilim uyar beri, doğrudan RTTI kullanarak çok daha üstündür.


10

Bir işlevi çağırırsanız, kural olarak, hangi kesin adımların atılacağını gerçekten umursamıyorsunuz, yalnızca belirli kısıtlamalar dahilinde daha üst düzey bir hedefe ulaşılacağı (ve işlevin bunu nasıl gerçekleştirdiği gerçekten kendi sorunudur).

Belirli bir işi yapabilen özel nesnelerin ön seçimini yapmak için RTTI kullandığınızda, aynı setteki diğerleri yapamazsa, bu rahat dünya görüşünü bozuyorsunuz. Birdenbire, arayan kişinin, sadece yardımcılarına devam etmelerini söylemek yerine kimin neyi yapabileceğini bilmesi gerekiyor. Bazı insanlar bundan rahatsız oluyor ve bunun RTTI'nin biraz kirli görülmesinin nedeninin büyük bir kısmı olduğundan şüpheleniyorum.

Performans sorunu mu var? Belki, ama bunu hiç deneyimlemedim ve yirmi yıl öncesinden gelen bilgelik olabilir veya iki yerine üç montaj talimatının kullanılmasının kabul edilemez şişkinlik olduğuna dürüstçe inanan insanlardan.

Öyleyse bununla nasıl başa çıkılacağı ... Durumunuza bağlı olarak, herhangi bir düğüme özgü özelliğin ayrı nesnelerde bir araya getirilmesi mantıklı olabilir (yani, 'turuncu' API'nin tamamı ayrı bir nesne olabilir). Kök nesne daha sonra 'turuncu' API'yi döndürmek için sanal bir işleve sahip olabilir ve turuncu olmayan nesneler için varsayılan olarak nullptr döndürür.

Bu durumunuza bağlı olarak aşırılık olsa da, belirli bir düğümün belirli bir API'yi destekleyip desteklemediğini kök düzeyinde sorgulamanıza ve destekliyorsa, bu API'ye özgü işlevleri yürütmenize olanak tanır.


6
Re: performans maliyeti - Dynamic_cast <> değerini, bir numaralandırmayı kontrol etmekten yaklaşık 1000 kat daha yavaş olan bir 3GHz işlemci üzerindeki uygulamamızda yaklaşık 2µs maliyetle ölçtüm. (Uygulamamızın 11,1 ms'lik bir ana döngü son tarihi vardır, bu nedenle mikro saniyeleri çok önemsiyoruz.)
Crashworks

6
Performans, uygulamalar arasında çok farklılık gösterir. GCC, hızlı bir typeinfo işaretçi karşılaştırması kullanır. MSVC, hızlı olmayan dizi karşılaştırmalarını kullanır. Ancak, MSVC'nin yöntemi, GCC'nin işaretçi yönteminin statik kitaplıktaki bir sınıfın paylaşılan kitaplıktaki bir sınıftan farklı olduğuna inandığı farklı kitaplık, statik veya DLL sürümlerine bağlı kodla çalışacaktır.
Zan Lynx

1
@Crashworks Burada tam bir kayda sahip olmak için: hangi derleyici (ve hangi sürüm) bu?
H. Guijt

Gözlemlediğiniz sonuçları hangi derleyicinin ürettiği hakkında bilgi talebini destekleyen @Crashworks; Teşekkürler.
underscore_d

@underscore_d: MSVC.
Crashworks

9

C ++, statik tür denetimi fikri üzerine kurulmuştur.

[1] RTTI, yani dynamic_castve type_iddinamik tür denetimidir.

Yani, esasen statik tip kontrolünün neden dinamik tip kontrolüne tercih edildiğini soruyorsunuz. Ve basit cevap, statik tip kontrolünün dinamik tip kontrolüne tercih edilip edilmeyeceğine bağlıdır . Çok fazla. Ancak C ++, statik tür denetimi fikri etrafında tasarlanmış programlama dillerinden biridir. Ve bu, örneğin geliştirme sürecinin, özellikle testin, tipik olarak statik tip kontrolüne uyarlandığı ve ardından buna en iyi uyduğu anlamına gelir.


Yeniden

" Bunu şablonlarla veya diğer yöntemlerle yapmanın net bir yolunu bilemezdim

bu süreci-heterojen-grafik düğümleri-statik tip kontrolüyle ve ziyaretçi kalıbı yoluyla hiçbir şekilde çevrim yapmadan yapabilirsiniz, örneğin:

#include <iostream>
#include <set>
#include <initializer_list>

namespace graph {
    using std::set;

    class Red_thing;
    class Yellow_thing;
    class Orange_thing;

    struct Callback
    {
        virtual void handle( Red_thing& ) {}
        virtual void handle( Yellow_thing& ) {}
        virtual void handle( Orange_thing& ) {}
    };

    class Node
    {
    private:
        set<Node*> connected_;

    public:
        virtual void call( Callback& cb ) = 0;

        void connect_to( Node* p_other )
        {
            connected_.insert( p_other );
        }

        void call_on_connected( Callback& cb )
        {
            for( auto const p : connected_ ) { p->call( cb ); }
        }

        virtual ~Node(){}
    };

    class Red_thing
        : public virtual Node
    {
    public:
        void call( Callback& cb ) override { cb.handle( *this ); }

        auto redness() -> int { return 255; }
    };

    class Yellow_thing
        : public virtual Node
    {
    public:
        void call( Callback& cb ) override { cb.handle( *this ); }
    };

    class Orange_thing
        : public Red_thing
        , public Yellow_thing
    {
    public:
        void call( Callback& cb ) override { cb.handle( *this ); }

        void poke() { std::cout << "Poked!\n"; }

        void poke_connected_orange_things()
        {
            struct Poker: Callback
            {
                void handle( Orange_thing& obj ) override
                {
                    obj.poke();
                }
            } poker;

            call_on_connected( poker );
        }
    };
}  // namespace graph

auto main() -> int
{
    using namespace graph;

    Red_thing   r;
    Yellow_thing    y1, y2;
    Orange_thing    o1, o2, o3;

    for( Node* p : std::initializer_list<Node*>{ &y1, &y2, &r, &o2, &o3 } )
    {
        o1.connect_to( p );
    }
    o1.poke_connected_orange_things();
}

Bu, düğüm türleri kümesinin bilindiğini varsayar.

Değilse, ziyaretçi modeli (birçok varyasyonu vardır) birkaç merkezi yayınla veya yalnızca tek biriyle ifade edilebilir.


Şablon tabanlı bir yaklaşım için Boost Graph kitaplığına bakın. Aşina olmadığımı söylemek üzücü, kullanmadım. Bu yüzden tam olarak ne yaptığından ve nasıl ve ne ölçüde RTTI yerine statik tip kontrolünü kullandığından emin değilim, ancak Boost genel olarak şablon tabanlı olduğundan, temel fikir statik tip kontrol olduğundan, bunu bulacağınızı düşünüyorum. Grafik alt kitaplığı da statik tip kontrolüne dayanmaktadır.


[1] Çalışma Süresi Tipi Bilgileri .


1
Unutulmaması gereken bir "komik şey", ziyaretçi kalıbı için gerekli olan kod miktarını azaltabilir (türler eklerken değişir), bir hiyerarşiye "tırmanmak" için RTTI kullanmaktır. Bunu "döngüsel olmayan ziyaretçi kalıbı" olarak biliyorum.
Daniel Jour

3

Elbette, polimorfizmin yardımcı olamayacağı bir senaryo var: isimler. typeidbu adın kodlanma şekli uygulama tanımlı olsa da, türün adına erişmenizi sağlar. Ama genellikle iki typeid-yi karşılaştırabildiğiniz için bu bir sorun değildir :

if ( typeid(5) == "int" )
    // may be false

if ( typeid(5) == typeid(int) )
   // always true

Aynısı karmalar için de geçerlidir.

[...] RTTI "zararlı" kabul edilir

zararlı RTTI bazı dezavantajları vardır, ancak: kesinlikle boşuna olduğunu mu çok avantajları vardır.

Gerçekten RTTI kullanmak zorunda değilsiniz. RTTI, OOP sorunlarını çözmek için bir araçtır : başka bir paradigma kullanırsanız, bunlar muhtemelen ortadan kalkacaktır. C'de RTTI yok, ancak yine de çalışıyor. Bunun yerine C ++, OOP'yi tamamen destekler ve çalışma zamanı bilgisi gerektirebilecek bazı sorunların üstesinden gelmek için size birden fazla araç sunar: bunlardan biri gerçekten de RTTI'dır, ancak bir bedeli vardır. Buna gücünüz yetmiyorsa, yalnızca güvenli bir performans analizinden sonra belirtmeniz daha iyi olur, hala eski okul varvoid* : ücretsiz. Maliyetsiz. Ama hiçbir tür güvenlik elde edemezsiniz. Yani her şey ticaretle ilgili.


  • Bazı derleyiciler kullanmaz / RTTI her zaman etkin değildir
    değildir Bu argümanı gerçekten almıyorum. Bu, C ++ 14 özelliklerini kullanmamam gerektiğini söylemek gibi, çünkü onu desteklemeyen derleyiciler var. Yine de kimse beni C ++ 14 özelliklerini kullanmaktan caydırmaz.

Uyumlu C ++ kodu yazarsanız (muhtemelen kesinlikle), uygulamadan bağımsız olarak aynı davranışı bekleyebilirsiniz. Standart uyumlu uygulamalar standart C ++ özelliklerini desteklemelidir.

Ancak bazı ortamlarda C ++ tanımlarının («bağımsız» olanlar), RTTI'nin sağlanmasına gerek olmadığını ve istisnaların da olmadığını unutmayın. virtual vb. . RTTI'nin doğru çalışması için, ABI ve gerçek tür bilgileri gibi düşük düzeyli ayrıntılarla ilgilenen bir temel katmana ihtiyacı vardır.


Bu durumda RTTI konusunda Yakk'e katılıyorum. Evet, kullanılabilir; ama mantıksal olarak doğru mu? Dilin bu kontrolü atlamanıza izin vermesi, yapılması gerektiği anlamına gelmez.

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.