C ++ 'da güvenli bir arayüz için kalıp nedir


22

Not: Aşağıdakiler C ++ 03 kodudur, ancak önümüzdeki iki yıl içinde C ++ 11'e geçmeyi bekliyoruz, bu yüzden bunu aklımızda tutmalıyız.

C ++ 'ta soyut bir arayüzün nasıl yazılacağı hakkında bir kılavuz (yeni başlayanlar için, diğerleri arasında) yazıyorum. Konuyla ilgili her iki Sutter makalesini de okudum, örnekler ve cevaplar için interneti aradım ve bazı testler yaptım.

Bu kod derlenmemelidir!

void foo(SomeInterface & a, SomeInterface & b)
{
   SomeInterface c ;               // must not be default-constructible
   SomeInterface d(a);             // must not be copy-constructible
   a = b ;                         // must not be assignable
}

Yukarıdaki tüm davranışlar, dilimleme konusundaki problemlerinin kaynağını bulur : Soyut arabirim (veya hiyerarşideki yaprak olmayan sınıf), türetilebilir sınıf olabilirse, inşa edilemez veya kopyalanamaz / atanabilir, EVEN olmamalıdır.

0'ınci Çözüm: temel arayüz

class VirtuallyDestructible
{
   public :
      virtual ~VirtuallyDestructible() {}
} ;

Bu çözüm sade ve biraz naif: Tüm kısıtlamalarımız başarısız oluyor: Varsayılan olarak oluşturulmuş, kopya oluşturulmuş ve kopya atanmış olabilir (Taşıyıcıları ve atamayı taşımaktan bile emin değilim, ancak hala 2 yıla sahibim. dışarı).

  1. Yıkıcıyı saf sanal olarak ilan edemeyiz çünkü satır içi olarak tutmamız gerekir ve derleyicilerimizden bazıları saf sanal yöntemleri satır içi boş gövdeyle sindirmez.
  2. Evet, bu sınıfın tek amacı uygulayıcıları neredeyse yok edilebilecek bir duruma getirmek, ki bu nadir bir durum.
  3. Ek bir sanal saf yöntemimiz olsa bile (vakaların çoğunluğu), bu sınıf yine de kopyalanabilir.

Yani hayır ...

1. Çözüm: boost :: tartışılmaz

class VirtuallyDestructible : boost::noncopyable
{
   public :
      virtual ~VirtuallyDestructible() {}
} ;

Bu çözüm en iyisidir, çünkü açık, net ve C ++ (makro yok)

Sorun, bu özel arabirim için hala çalışmamasıdır çünkü VirtuallyConstructible hala varsayılan olarak yapılandırılabilir .

  1. Yıkıcıyı tamamen sanal ilan edemeyiz çünkü satır içi olarak tutmamız gerekir ve derleyicilerimizden bazıları sindirmez.
  2. Evet, bu sınıfın tek amacı uygulayıcıları neredeyse yok edilebilecek bir duruma getirmek, ki bu nadir bir durum.

Bir başka sorun da , kopyalanamayan arayüzü uygulayan sınıfların, bu yöntemlere sahip olmaları gerekiyorsa (ve bizim kodumuzda hala müşterimiz tarafından erişilebilen değer sınıflarına sahip olmamız durumunda) açıkça kopya kurucuyu ve atama işlecini açıklamak / tanımlamak zorunda olmasıdır. arayüzler).

Bu, gitmek istediğimiz yer olan Sıfır Kuralına aykırıdır: Eğer varsayılan uygulama uygunsa, onu kullanabilmeliyiz.

2. Çözüm: korunmalarını sağlayın!

class MyInterface
{
   public :
      virtual ~MyInterface() {}

   protected :
      // With C++11, these methods would be "= default"
      MyInterface() {}
      MyInterface(const MyInterface & ) {}
      MyInterface & operator = (const MyInterface & ) { return *this ; }
} ;

Bu kalıp sahip olduğumuz teknik kısıtlamaları takip eder (en azından kullanıcı kodunda): MyInterface varsayılan olarak oluşturulamaz, kopya ile oluşturulamaz ve kopya ile atanamaz.

Ayrıca, uygulama sınıfları için Sıfır Kuralını takip etmekte özgür olan, hatta birkaç kurucu / işletmeciyi C ++ 11 / 14'te sorunsuz bir şekilde "= default" olarak ilan edebileceği hiçbir yapay kısıtlama getirmez .

Şimdi, bu oldukça ayrıntılı ve bir alternatif bir makro kullanıyor olabilir:

class MyInterface
{
   public :
      virtual ~MyInterface() {}

   protected :
      DECLARE_AS_NON_SLICEABLE(MyInterface) ;
} ;

Korumalı makro dışında kalmalıdır (çünkü kapsamı yoktur)

Doğru şekilde "ad alanı" (yani, şirketinizin veya ürününüzün adıyla önceden belirlenmiş), makronun zararsız olması gerekir.

Bunun avantajı, kodun tüm arayüzlere kopyalanmak yerine, tek bir kaynaktan etkilenmesidir. Hareket yapıcı ve hareket ataması gelecekte de aynı şekilde açıkça devre dışı bırakılırsa, bu kodda çok hafif bir değişiklik olur.

Sonuç

  • Arayüzlerde kodun dilimlemeye karşı korunmasını istemek paranoyak mıyım? (Yapmadığıma inanıyorum, ama kimse bilmiyor ...)
  • Yukarıdakiler arasında en iyi çözüm nedir?
  • Başka, daha iyi bir çözüm var mı?

Lütfen bunun yeni başlayanlar için bir rehber niteliğinde olacak bir model olduğunu unutmayın (diğerleri arasında), bu nedenle şöyle bir çözüm var: "Her vaka kendi uygulamasına sahip olmalı" uygulanabilir bir çözüm değildir.

Ödül ve sonuçlar

Ben lütuf layık coredump çünkü sorulara cevap harcanan ve cevapları alaka.

Soruna olan çözümüm muhtemelen şöyle bir şeye gider:

class MyInterface
{
   DECLARE_CLASS_AS_INTERFACE(MyInterface) ;

   public :
      // the virtual methods
} ;

... aşağıdaki makroyla:

#define DECLARE_CLASS_AS_INTERFACE(ClassName)                                \
   public :                                                                  \
      virtual ~ClassName() {}                                                \
   protected :                                                               \
      ClassName() {}                                                         \
      ClassName(const ClassName & ) {}                                       \
      ClassName & operator = (const ClassName & ) { return *this ; }         \
   private :

Bu, aşağıdaki nedenlerle sorunum için uygun bir çözüm:

  • Bu sınıf somutlaştırılamaz (inşaatçılar korunur)
  • Bu sınıf neredeyse imha edilebilir
  • Bu sınıf, miras sınıflarına aşırı sınırlamalar getirmeden miras alınabilir (örneğin miras sınıfı varsayılan olarak kopyalanabilir)
  • Makro kullanımı, "bildirim" arayüzünün kolayca tanınabilir (ve aranabilir) anlamına gelir ve kodu tek bir yerde değiştirilir, değiştirilmesini kolaylaştırır (uygun bir önek adı istenmeyen ad çakışmalarını kaldıracaktır)

Diğer cevapların değerli bilgiler verdiğini unutmayın. Bunu yapan herkese teşekkür ederim.

Sanırım bu soruya yine başka bir ödül getirebilirim ve bir tane görürsem, sadece bu cevaba vermek için bir ödül açacağım kadar aydınlatıcı cevaplara değer veriyorum.


5
Saf sanal fonksiyonları arayüzde kullanamaz mısın? virtual void bar() = 0;Örneğin? Bu, ara yüzünüzün tanıtılmasını engeller.
Morwenn

@Morwenn: Soruda da belirtildiği gibi, bu vakaların% 99'unu çözecektir (mümkünse% 100'ü hedefliyorum). Kayıp% 1'i görmezden gelmeyi seçsek bile, bu aynı zamanda atama dilimlemesini de çözmez. Yani, hayır, bu iyi bir çözüm değil.
paercebal

@Morwenn: Cidden? ... :-D ... Bu soruyu önce StackOverflow'a yazdım ve göndermeden hemen önce fikrimi değiştirdim. Onu burada silmem ve SO'ya göndermem gerektiğine inanıyor musunuz?
paercebal

Eğer haklıysam, ihtiyacın olan tek şey virtual ~VirtuallyDestructible() = 0ve arabirim sınıflarının sanal mirası (sadece soyut üyelerle). Bunu, VirtuallyDestructible, belki de ihmal edebilirsin.
Dieter Lücking

5
@ paercebal: Derleyici saf sanal sınıflarda boğulursa, o zaman çöpe aittir. Gerçek bir arayüz tanımı gereği saf sanal.
Kimse

Yanıtlar:


13

C ++ ile arayüz oluşturmanın kurallı yolu, saf bir sanal yıkıcı vermektir. Bu sağlar

  • C ++ bir soyut sınıf örneği oluşturmanıza izin vermediğinden, arabirim sınıfının hiçbir örneği oluşturulamaz. Bu, yapılamaz gereklilikleri dikkate alır (hem varsayılan hem de kopya).
  • deleteArabirime bir işaretçi çağırmak doğru olanı yapar: bu örnek için en türetilmiş sınıfın yıkıcısını çağırır.

Sadece saf bir sanal yıkıcıya sahip olmak, arayüze referans olarak atanmayı engellemez. Bunun da başarısız olması gerekiyorsa, arayüzünüze korumalı bir atama operatörü eklemelisiniz.

Herhangi bir C ++ derleyicisi bunun gibi bir sınıf / arayüz kullanabilmelidir (hepsi bir başlık dosyasında):

class MyInterface {
public:
  virtual ~MyInterface() = 0;
protected:
  MyInterface& operator=(const MyInterface&) { return *this; } // or = default for C++14
};

inline MyInterface::~MyInterface() {}

Bunun üzerine boğulan bir derleyiciniz varsa (bu, C ++ 98 öncesi olması gerektiği anlamına gelir), o zaman seçenek 2 (korumalı yapıcılara sahip olmak) iyi bir ikinci en iyisidir.

boost::noncopyableBu görev için kullanılması tavsiye edilmez, çünkü hiyerarşideki tüm sınıfların kopyalanamaz olması gerektiği mesajını gönderir ve bu şekilde kullanma niyetinize aşina olmayan daha deneyimli geliştiriciler için kafa karışıklığı yaratabilir.


If you need [prevent assignment] to fail as well, then you must add a protected assignment operator to your interface.: Bu benim problemimin kökü. Atamayı desteklemek için bir arayüze ihtiyaç duyduğum durumlar gerçekten nadir olmalı. Öte yandan, bir arayüzü referans olarak iletmek istediğim durumlar (NULL kabul edilemez olan durumlar) ve bu nedenle, bir işlemden kaçınmak veya bu derlemeyi dilimlemekten çok daha fazlasını istemek istiyorum.
paercebal

Görevlendirme operatörü asla aranmamalı, neden bir tanım veriyorsunuz? Bir kenara, neden yapmıyorsun private? Ayrıca, varsayılan ve copy-ctor ile uğraşmak isteyebilirsiniz.
Deduplicator

5

Paranoyak mıyım?

  • Arayüzlerde kodun dilimlemeye karşı korunmasını istemek paranoyak mıyım? (Yapmadığıma inanıyorum, ama kimse bilmiyor ...)

Bu bir risk yönetimi sorunu değil mi?

  • Dilimleme ile ilgili bir hatanın ortaya çıkmasından korkuyor musunuz?
  • farkedilmeden gidebileceğini ve kurtarılamaz hatalara neden olabileceğini düşünüyor musunuz?
  • Dilimlemekten kaçınmak için ne dereceye kadar gitmek istiyorsunuz?

En iyi çözüm

  • Yukarıdakiler arasında en iyi çözüm nedir?

İkinci çözümün ("onları korumalı yap") iyi görünüyor, ama bir C ++ uzmanı olmadığımı hatırla.
En azından, geçersiz kullanımlar derleyicim (g ++) tarafından yanlış olarak bildirilmiş gibi görünüyor.

Şimdi makrolara ihtiyacınız var mı? "Evet" derdim, çünkü yazdığınız kılavuzun amacının ne olduğunu söylemeseniz de, bunun ürün kodunuzda belirli bir dizi en iyi uygulamayı zorlamak olduğunu tahmin ediyorum.

Bu amaçla, makrolar insanların modeli ne zaman etkili bir şekilde uyguladıklarını saptamaya yardımcı olabilir: temel bir taahhüt filtresi size makronun kullanılıp kullanılmadığını söyleyebilir:

  • kullanılırsa, o zaman modelin uygulanması muhtemeldir ve daha da önemlisi, doğru şekilde uygulanır (sadece bir protectedanahtar kelime olup olmadığını kontrol edin ),
  • kullanılmazsa, neden olmadığını araştırmayı deneyebilirsiniz.

Makrolar olmadan, desenin gerekli olup olmadığını ve her durumda iyi uygulanıp uygulanmadığını incelemelisiniz.

Daha iyi çözüm

  • Başka, daha iyi bir çözüm var mı?

C ++ 'da dilimleme, dilin özelliğinden başka bir şey değildir. Bir kılavuz yazdığınızdan (özellikle yeni başlayanlar için), sadece "kodlama kurallarını" değil, öğretmeye odaklanmalısınız. Dilimlemenin nasıl ve neden gerçekleştiğini, örnekler ve alıştırmalar ile birlikte gerçekten nasıl açıkladığınızdan emin olmalısınız (tekerleği yeniden icat etmeyin, kitaplardan ve öğreticilerden ilham alın).

Örneğin, bir alıştırmanın başlığı " C ++ 'da güvenli bir arayüz için kalıp nedir ?" Olabilir.

Bu nedenle, en iyi hareketiniz, C ++ geliştiricilerin dilimleme olduğunda neler olup bittiğini anlamalarını sağlamaktır. Yaparlarsa, kodda belirli bir kalıbı resmi olarak uygulamadan bile korktuğun kadar hata yapamayacaklarına ikna oldum (ama yine de uygulayabilirsin, derleyici uyarılarının iyi olduğu).

Derleyici hakkında

Diyorsun :

Bu ürün için derleyici seçimi konusunda gücüm yok,

Çoğu zaman insanlar, "[X] yapmaya hakkım yok" , "[Y] yapmam gerekmiyor" diyecekler , çünkü bunun mümkün olmadığını sanıyorlar çünkü denedim ya da sordum.

Teknik konularla ilgili görüş bildirmek muhtemelen iş tanımınızın bir parçasıdır; Derleyicinin sorunlu alanınız için mükemmel (veya benzersiz) bir seçim olduğunu düşünüyorsanız, kullanın. Ama ayrıca “satır içi uygulamalara sahip saf sanal yıkıcılar gördüğüm en kötü boğulma noktası değil” demiştiniz ; Anladığım kadarıyla, derleyici o kadar özel ki, bilgili C ++ geliştiricileri bile kullanmakta zorlanıyor: eski / şirket içi derleyiciniz artık teknik borçlu ve bu konuyu diğer geliştiriciler ve yöneticilerle tartışmaya hakkınız var (görev?) .

Derleyiciyi tutma maliyetini ve başka birini kullanma maliyetini değerlendirmeye çalışın:

  1. Mevcut derleyici size kimsenin veremediği neyi getiriyor?
  2. Ürün kodunuz başka bir derleyici kullanarak kolayca derlenebilir mi? Neden olmasın ?

Durumunuzu bilmiyorum ve aslında belirli bir derleyiciye bağlanmak için geçerli nedenleriniz olabilir.
Ancak bu sadece düz bir atalet durumunda, siz veya iş arkadaşlarınız üretkenlik veya teknik borç problemlerini bildirmezse durum hiç değişmeyecektir.


Am I paranoid...: "Arabirimlerinizin doğru kullanımı kolay ve yanlış kullanılması zor". Birisi statik yöntemlerimden birinin yanlışlıkla yanlış kullanıldığını bildirdiğinde bu prensibi tattım. Üretilen hata ilgisiz görünüyordu ve kaynağın bulunması birkaç saat sürdü. Bu "arayüz hatası", başka bir arayüz referansı atamakla aynıdır. Yani, evet, bu tür hatalardan kaçınmak istiyorum. Ayrıca, C ++ 'da felsefe, derleme zamanında olabildiğince yakalamak ve dil bize o gücü veriyor, onunla devam ediyoruz.
paercebal

Best solution: Katılıyorum. . . Better solution: Bu harika bir cevap. Üzerinde çalışacağım ... Şimdi, hakkında Pure virtual classes: Bu nedir? Bir C ++ soyut arayüzü? (devlet olmadan sınıf ve sadece saf sanal yöntemler?). Bu "saf sanal sınıf" beni dilimlemeye karşı nasıl korudu? (saf sanal yöntemler başlatmayı derleme yapmaz, ancak kopya atama ve hareket atama da IIRC yapar).
paercebal

About the compiler: Aynı fikirdeyiz, ancak derleyicilerimiz sorumluluk alanımın dışında (beni derli toplu yorumlardan uzak tutmuyor ... :-p ...). Ayrıntıları ifşa etmeyeceğim (Keşke yapabilseydim) ancak iç nedenlerle (test takımları gibi) ve dış nedenlerle (örneğin, kütüphanelerimizle bağlantı kuran müşteri) bağlantılı. Sonunda, derleyici sürümünün değiştirilmesi (veya hatta yamalanması) önemsiz bir işlem DEĞİLDİR. Tek bir kırık derleyiciyi yeni bir gcc ile değiştireyim.
paercebal

@ paercebal Yorumlarınız için teşekkürler; saf sanal sınıflar hakkında haklısınız, tüm kısıtlamalarınızı çözmüyor (bu bölümü kaldıracağım). "Arayüz hatası" kısmını ve derleme zamanında hataları yakalamanın ne kadar yararlı olduğunu anladım: paranoyak olup olmadığınızı sordunuz ve akılcı yaklaşım statik kontroller ihtiyacınızı meydana gelen hata olasılığıyla dengelemek olduğunu düşünüyorum. Derleyici şey ile iyi şanslar :)
coredump

1
Makroların hayranı değilim, özellikle kılavuzlar (ayrıca) gençlere yöneliktir. Çok sık, bunları “kör” uygulayabilmek için bu tür “kullanışlı” araçlar verilen ve gerçekte neler olduğunu asla anlamayan insanlar gördüm. Makronun yaptığı şeyin en karmaşık şey olması gerektiğine inanıyorlar çünkü patronları kendileri için çok zor olacağını düşünüyorlardı. Makro yalnızca şirketinizde bulunduğundan, web sitesinde bir arama bile yapamaz, oysa belgeli bir rehber için hangi üyenin ilan ettiğini ve neden yapabildiklerini açıklar.
5gon12eder

2

Dilimleme sorunu, kullanıcılara çalışma zamanı polimorfik bir arabirim gösterdiğinizde ortaya çıkan sorunlardan sadece biri değil. Boş işaretçileri, hafıza yönetimini, paylaşılan verileri düşünün. Bunların hiçbiri her durumda kolayca çözülemez (akıllı işaretçiler harikadır, ancak gümüş mermi bile değildir). Aslında, gönderiminizden dilimleme sorununu çözmeye çalışıyor gibi görünmüyorsunuz , ancak kullanıcıların kopyalarını çıkarmasına izin vermeyerek, bunu gözden kaçırıyorsunuz. Dilimleme problemine bir çözüm sunmak için tek yapmanız gereken, sanal bir klon üyesi işlevi eklemektir. Bir çalışma zamanı polimorfik ara yüzünü ortaya çıkarmayla ilgili daha derin bir sorun, kullanıcıları değer anlambiliminden ziyade zor olan referans anlambilimiyle uğraşmaya zorlamanızdır.

C ++ 'da bu sorunlardan kaçınmanın en iyi yolu tip silme kullanmaktır . Bu, normal bir sınıf arayüzünün arkasındaki çalışma zamanı polimorfik arayüzünü sakladığınız bir tekniktir. Bu normal sınıf arayüzü daha sonra değer semantiğine sahiptir ve ekranların arkasındaki tüm polimorfik 'karışıklıkları' önemser. std::functionsilme türünün en önemli örneğidir.

Kullanıcılarınıza miras bırakmanın neden kötü olduğunu ve bu tür sunumların Sean Parent'in bu sunumlarını görmesini nasıl düzeltebileceğini açıklamak için:

Kalıtım, Kötülüğün Temel Sınıfıdır (kısa versiyon)

Değer Anlambilimi ve Kavram-Temelli Polimorfizm (uzun versiyon; takip etmesi kolay, ses mükemmel değil)


0

Paranoyak değilsin. C ++ programcısı olarak ilk profesyonel görevim dilimleme ve çökme ile sonuçlandı. Başkalarını tanıyorum. Bunun için iyi bir çözüm yok.

Derleyici kısıtlamalarınız göz önüne alındığında, seçenek 2 en iyisidir. Yeni programcılarınızın tuhaf ve gizemli görecekleri bir makro yapmak yerine, kodu otomatik olarak oluşturmak için bir komut dosyası veya araç öneririm. Yeni çalışanlarınız bir IDE kullanacaksa, arayüz adını isteyecek bir “Yeni MYCOMPANY Arayüzü” aracı yaratabilir ve aradığınız yapıyı oluşturabilirsiniz.

Programcılar komut satırı kullanıyorsa, kodu oluşturmak için NewMyCompanyInterface betiğini oluşturmak için kullanabileceğiniz herhangi bir komut dosyası dilini kullanın.

Geçmişte bu yaklaşımı genel kod kalıpları (arayüzler, durum makineleri vb.) İçin kullandım. İşin güzel yanı, yeni programcıların çıktıyı okuyabilmeleri ve kolayca anlayabilmeleri, üretilemeyecek bir şeye ihtiyaç duyduklarında gerekli kodu yeniden üretebilmeleridir.

Makrolar ve diğer meta-programlama yaklaşımları olanları şaşırtmaya meyillidir ve yeni programcılar 'perdenin arkasında' neler olduğunu öğrenmezler. Deseni kırmak zorunda kaldıklarında, eskisi gibi kaybolurlar.

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.