C ++ özel üye işlevleri kapsüllemek için arkadaş sınıfları kullanma - iyi uygulama veya kötüye?


12

Bu yüzden, böyle bir şey yaparak başlıklara özel işlevler koymaktan kaçınmanın mümkün olduğunu fark ettim:

// In file pred_list.h:
    class PredicateList
    {
        int somePrivateField;
        friend class PredicateList_HelperFunctions;
    public:
        bool match();
    } 

// In file pred_list.cpp:
    class PredicateList_HelperFunctions
    {
        static bool fullMatch(PredicateList& p)
        {
            return p.somePrivateField == 5; // or whatever
        }
    }

    bool PredicateList::match()
    {
        return PredicateList_HelperFunctions::fullMatch(*this);
    }

Özel işlev üstbilgide asla bildirilmez ve üstbilgiyi içe aktaran sınıfın tüketicilerinin bunun var olduğunu bilmesine gerek yoktur. Yardımcı işlev bir şablonsa (alternatif, tam kodu başlığa koyuyorsa) bu gereklidir, bu da bunu "keşfettim". Bir özel üye işlevi eklerseniz / kaldırır / değiştirirseniz, başlığı içeren her dosyayı yeniden derlemenize gerek kalmadan başka bir güzel ters. Tüm özel işlevler .cpp dosyasındadır.

Yani...

  1. Bu, adı olan iyi bilinen bir tasarım deseni mi?
  2. Bana göre (Java / C # arka planından geliyor ve kendi zamanımda C ++ öğreniyorum), bu çok iyi bir şey gibi görünüyor, çünkü .cpp bir uygulamayı tanımlarken (ve geliştirilmiş derleme süresi güzel bir bonus). Bununla birlikte, bu şekilde kullanılması amaçlanmamış bir dil özelliğini kötüye kullanıyor gibi kokuyor. Peki, hangisi? Bu profesyonel bir C ++ projesinde görmek için kaşlarını çatmak bir şey mi?
  3. Düşünmediğim bir tuzak var mı?

Uygulamayı kütüphane kenarında gizlemenin çok daha sağlam bir yolu olan Pimpl'in farkındayım. Bu, Pimpl'in performans sorunlarına neden olacağı veya çalışmadığı iç sınıflarla kullanım içindir, çünkü sınıfın bir değer olarak ele alınması gerekir.


DÜZENLEME 2: Dragon Energy'nin aşağıdaki mükemmel cevabı, friendanahtar kelimeyi hiç kullanmayan aşağıdaki çözümü önerdi :

// In file pred_list.h:
    class PredicateList
    {
        int somePrivateField;
        class Private;
    public:
        bool match();
    } 

// In file pred_list.cpp:
    class PredicateList::Private
    {
    public:
        static bool fullMatch(PredicateList& p)
        {
            return p.somePrivateField == 5; // or whatever
        }
    }

    bool PredicateList::match()
    {
        return PredicateList::Private::fullMatch(*this);
    }

Bu , aynı ayrılma prensibini korurken ( friendgibi şeytanlaştırılmış gibi görünüyor) şok faktörünü önler goto.


2
" Bir tüketici kendi PredicateList_HelperFunctions sınıfını tanımlayabilir ve özel alanlara erişmesine izin verebilir. " Bu bir ODR ihlali olmaz mı? Hem siz hem de tüketici aynı sınıfı tanımlamak zorunda kalacaksınız. Bu tanımlar eşit değilse, kod kötü biçimlendirilir.
Nicol Bolas

Yanıtlar:


13

Zaten tanıdığınız en azını söylemek biraz ezoteriktir ki, kodunuzu ilk kez karşılaştığınız zaman ne yaptığınızı ve bu yardımcı sınıfların tarzınızı almaya başlayana kadar nerede uygulandığını merak ettiğimde başımı kaşıma neden olabilir. / alışkanlıkları (hangi noktada buna tamamen alışırım).

Başlıklardaki bilgi miktarını azalttığınızı seviyorum. Özellikle çok büyük kod tabanlarında, derleme zamanı bağımlılıklarını azaltmak ve sonuçta inşa sürelerini azaltmak için pratik etkileri olabilir.

Bağırsak tepkime, uygulama ayrıntılarını bu şekilde gizlemeye ihtiyaç duyduğunuzda, kaynak dosyada dahili bağlantı ile bağımsız işlevlere geçen parametreyi tercih etmenizdir. Genellikle, belirli bir sınıfı uygulamak için sınıfın tüm iç bileşenlerine erişmeden yararlı işlevler (veya tüm sınıflar) uygulayabilir ve bunun yerine işlevin (veya yapıcı) bir yöntemin uygulanmasından ilgili olanları iletebilirsiniz. Ve doğal olarak bu, sınıfınız ve "yardımcılar" arasındaki bağlantıyı azaltma bonusuna sahiptir. Ayrıca, birden fazla sınıf uygulaması için daha genel bir amaca hizmet etmeye başladıklarını fark ederseniz, başka türlü "yardımcı" olabilecekleri genelleme eğilimi vardır.

Ben de kod "yardımcıları" bir sürü gördüğümde bazen biraz saçma. Bu her zaman doğru değildir, ancak bazen, kod çoğaltmasını ortadan kaldırmak için sadece işlevlerini willy-nilly olarak ayrıştıran bir geliştiricinin semptomatik olabilirler. diğer bazı işlevleri uygulamak için gereken kod. Sadece biraz daha ufacık düşünülmüş, bazen bir sınıfın uygulanmasının nasıl daha fazla fonksiyona ayrıştırıldığı ve nesnelerinizin tüm örneklerini iç kısımlara tam erişim ile geçmesine nasıl yardımcı olacağı konusunda çok daha fazla netliğe yol açabilir. tasarım düşünce tarzını teşvik etmek. Bunu yaptığınızı önermiyorum, elbette (hiçbir fikrim yok),

Bu uygunsuz olursa, pimpl olan ikinci, daha deyimsel bir çözüm düşünürdüm (bununla ilgili sorunlardan bahsettiğinizi anlıyorum, ancak minimum çaba göstermeyenlerden kaçınmak için bir çözümü genelleştirebileceğinizi düşünüyorum). Bu, özel verileri de dahil olmak üzere, sınıfınızın uygulanması gereken birçok bilgiyi toptancıdan uzaklaştırabilir. Pimpl'in performans sorunları, tam anlamıyla kullanıcı tanımlı kopya ctor uygulamak zorunda kalmadan değer semantiklerini korurken, ücretsiz bir liste gibi kir ucuz bir sabit zamanlı ayırıcı * ile hafifletilebilir.

  • Performans yönü için pimpl en azından bir işaretçi yükü getiriyor, ancak pratik bir endişe teşkil eden durumlarda vakaların oldukça ciddi olması gerektiğini düşünüyorum. Mekansal konum, ayırıcı aracılığıyla önemli ölçüde bozulmazsa, nesne üzerinde yinelenen sıkı döngüler (eğer performans endişe verici ise, genellikle homojen olmalıdır), pratikte önbellek özlemlerini en aza indirgeme eğilimindedir. pimpl'i tahsis etmek için ücretsiz bir liste, sınıfın alanlarını büyük ölçüde bitişik bellek bloklarına koyarak.

Şahsen sadece bu olasılıkları tükettikten sonra böyle bir şey düşünürdüm. Alternatifin, belki de sadece ezoterik doğasının pratik endişe olduğu başlığa maruz kalan daha özel yöntemler gibi olması iyi bir fikirdir.

Bir alternatif

Hemen hemen aynı amaçlara ulaşan aklıma gelen bir alternatif, arkadaş yokluğunda şöyle:

struct PredicateListData
{
     int somePrivateField;
};

class PredicateList
{
    PredicateListData data;
public:
    bool match() const;
};

// In source file:
static bool fullMatch(const PredicateListData& p)
{
     // Can access p.somePrivateField here.
}

bool PredicateList::match() const
{
     return fullMatch(data);
}

Şimdi bu çok tartışmalı bir fark gibi görünebilir ve ben yine de ona "yardımcı" diyorum (muhtemelen bütünüyle inatçı bir anlamda, sınıfın tüm iç durumunu, hepsine ihtiyacı olsun ya da olmasın, fonksiyona geçiriyoruz) ancak "şok" karşılaşma faktörünü engellemez friend. Genel olarak friend, daha fazla denetim yapılmadığını görmek biraz korkutucu görünüyor, çünkü sınıf içi araçlarınıza başka bir yerden erişilebildiğini söylüyor (bu da kendi değişmezlerini koruyamayabileceği anlamına geliyor). Kullandığınız yolla, friendinsanlar uygulamanın farkında olduğundan,friendsadece sınıfın özel işlevselliğinin uygulanmasına yardımcı olan aynı kaynak dosyada ikamet ediyor, ancak yukarıdakiler, en azından bu türden kaçınan herhangi bir arkadaş içermemesi muhtemel tartışmalı bir fayda ile aynı etkiyi gerçekleştirir ("Oh ateş, bu sınıfın bir arkadaşı var. Başka nerelere erişiyor / mutasyona uğruyor? "). Oysa hemen yukarıdaki sürüm derhal uygulamada yapılan herhangi bir şeyin dışında haklara erişilmesinin / mutasyon geçirilmesinin bir yolu olmadığını bildirir PredicateList.

Bu belki de bu nüans seviyesiyle biraz dogmatik bölgelere doğru ilerliyor, çünkü herkes bir şeyleri aynı şekilde adlandırır *Helper*ve hepsini bir sınıfın özel uygulamasının bir parçası olarak bir araya getirildiği aynı kaynak dosyasına koyabilir. Ancak nit-seçici alırsak, belki de hemen üstteki stil, bir bakışta diz sarsıntısı reaksiyonuna neden olmaz, friendbu da biraz korkutucu görünme eğiliminde olan anahtar kelimeyi içermez.

Diğer sorular için:

Bir tüketici kendi PredicateList_HelperFunctions sınıfını tanımlayabilir ve özel alanlara erişmesine izin verebilir. Bunu büyük bir sorun olarak görmeme rağmen (gerçekten bu özel alanlarda istersen biraz döküm yapabilirsin), belki de tüketicileri bu şekilde kullanmaya teşvik eder?

Bu, istemcinin aynı ada sahip ikinci bir sınıf tanımlayabileceği ve bağlantı hatalarına neden olmadan iç kısımlara bu şekilde erişebileceği API sınırları boyunca bir olasılık olabilir. Sonra yine büyük ölçüde grafiklerde çalışan ve bu düzeydeki güvenlik kaygılarının öncelik listesinde çok düşük olduğu bir C kodlayıcıyım, bu yüzden bunlar gibi endişeler ellerimi sallayıp dans etme eğilimindeyim yokmuş gibi davranmaya çalışın. :-D Böyle endişelerin oldukça ciddi olduğu bir alanda çalışıyorsanız, bence bu iyi bir düşüncedir.

Yukarıdaki alternatif öneri de bu sorunu yaşamamaktadır. Yine de kullanmaya devam etmek istiyorsanız friend, yardımcıyı özel iç içe bir sınıf haline getirerek de bu sorunu önleyebilirsiniz.

class PredicateList
{
    ...

    // Declare nested class.
    class Helper;

    // Make it a friend.
    friend class Helper;

public:
    ...
};

// In source file:
class PredicateList::Helper
{
    ...
};

Bu, adı olan iyi bilinen bir tasarım deseni mi?

Bildiğim kadarıyla yok. Uygulama detaylarının ve stilinin minutisine girdiği için bir tane olacağından şüpheliyim.

"Yardımcı Cehennem"

Birçok "yardımcı" kod içeren uygulamaları gördüğümde bazen nasıl küfür ettiğim hakkında daha fazla açıklama talebi aldım ve bu bazılarıyla biraz tartışmalı olabilir, ancak bazı hata ayıklama yaparken gerçekten küfür ettiğim gibi sadece bir sürü "yardımcı" bulmak için meslektaşlarımın bir sınıfı uygulaması. :-D Ve tüm bu yardımcıların tam olarak ne yapması gerektiğini anlamaya çalışan başımda kayan tek takım ben değildim. Ben de "yardımcıları kullanamayacaksınız" gibi dogmatik bir şekilde çıkmak istemiyorum , ama pratik olmadığında bunların bulunmadığı şeylerin nasıl uygulanacağını düşünmeye yardımcı olabileceğine dair ufak bir öneride bulunacağım.

Tüm özel üye işlevleri tanım gereği yardımcı işlevler değil mi?

Ve evet, özel yöntemler ekliyorum. Basit bir ortak arayüze sahip bir sınıf görürsem find_implya find_detailda ya da ya da amaç olarak biraz kötü tanımlanmış sonsuz bir özel yöntem seti gibi görürsem find_helper, aynı şekilde benzer şekilde kandırırım.

Alternatif olarak önerdiğim static, sınıfınızı "diğerlerinin uygulanmasına yardımcı olan bir işlevden" en az daha genel bir amaçla uygulamaya yardımcı olmak için dahili bağlantıya sahip (beyan edilen veya anonim bir ad alanı içinde) olmayan üye olmayan arkadaşlık işlevleridir . Ve burada genel bir SE açısından tercih edilebileceği için Herb Sutter'i neden C ++ 'Kodlama Standartları'ndan alıntılayabilirim:

Üyelik ücretlerinden kaçının: Mümkünse, üyesi olmayanları işlevsiz yapmayı tercih edin. [...] Üye olmayan arkadaş olmayan işlevler bağımlılıkları en aza indirerek kapsüllemeyi geliştirir: İşlevin gövdesi sınıfın kamuya açık olmayan üyelerine bağlı olamaz (bkz. Madde 11). Ayrıca, ayrılabilir işlevselliği serbest bırakmak ve birleşmeyi daha da azaltmak için monolitik sınıfları ayırırlar (bkz. Madde 33).

Değişken kapsamını daraltmanın temel ilkesi açısından bir dereceye kadar konuştuğu “üyelik aidatlarını” da anlayabilirsiniz. En uç örnek olarak, tüm programınızın çalışması için gereken tüm kodlara sahip bir Tanrı nesnesini hayal ederseniz, o zaman bu türden "yardımcıları" (üye fonksiyonlar veya arkadaşlar olsun) tüm içsellere ( sınıfın temelleri) temel olarak bu değişkenleri genel değişkenlerden daha az problemli hale getirmez. Bu en uç örnekte durumu doğru yönetmek ve güvenliği sağlamak ve global değişkenlerle elde edeceğiniz değişmezleri korumak için tüm zorluklara sahipsiniz. Ve elbette çoğu gerçek örnek umarım bu uç noktaya yakın bir yerde değildir, ancak bilgi gizleme sadece erişilen bilginin kapsamını sınırladığı kadar yararlıdır.

Şimdi Sutter burada zaten güzel bir açıklama yapıyor, ancak ayrıştırma işlevlerini nasıl tasarladığınız açısından psikolojik bir gelişme (en azından beyniniz benim gibi çalışıyorsa) gibi gelişmeye meyilli. Yalnızca geçtiğiniz ilgili parametreler dışında sınıftaki her şeye erişemeyen işlevler tasarlamaya başladığınızda veya sınıf örneğini bir parametre olarak iletirseniz, yalnızca genel üyeleri, tercih eden bir tasarım zihniyetini teşvik etme eğilimindedir. ayırma işleminin üstünde ve gelişmiş kapsüllemeyi teşvik eden, her şeye erişebiliyorsanız, tasarım yapmak için cazip olabileceğinizden daha net bir amaca sahip işlevler.

Ekstremitelere geri dönersek, küresel değişkenlerle dolu bir kod tabanı, geliştiricileri, işlevleri açık ve genelleştirilmiş bir şekilde tasarlamaya tam olarak teşvik etmez. Çok hızlı bir şekilde bir fonksiyonda ne kadar çok bilgiye erişirseniz, fanilerimiz dejenere dönüşme ve o fonksiyona daha spesifik ve ilgili parametreleri kabul etmek yerine sahip olduğumuz tüm bu ekstra bilgilere erişme lehine netliğini azaltma cazibesi ile karşı karşıya kalıyoruz. devlete erişimini daraltmak ve uygulanabilirliğini genişletmek ve niyet netliğini arttırmak. Bu, üye işlevler veya arkadaşlarla (genellikle daha az derecede olsa da) geçerlidir.


1
Giriş için teşekkürler! Bu bölümle nereden geldiğini tam olarak anlamıyorum: "Kodda çok sayıda" yardımcı "gördüğümde bazen biraz da kandırırım." - Tüm özel üye işlevleri tanım gereği yardımcı işlevler değil mi? Bu genel olarak özel üye işlevleriyle ilgili bir sorun gibi görünüyor.
Robert Fraser

1
Ah, iç sınıfın "arkadaş" la hiç ihtiyacı yok, bu şekilde yapmak "arkadaş" anahtar kelimesinden tamamen kaçınıyor
Robert Fraser

"Tüm özel üye işlevleri yardımcı işlevler tanım gereği değil mi? Bu genel olarak özel üye işlevleriyle ilgili bir sorun gibi görünüyor." En büyük şey değil. Önemsiz bir sınıf uygulaması için, bir dizi özel fonksiyonun veya tüm sınıf üyelerine bir kerede erişime sahip yardımcıların olmasının pratik bir gereklilik olduğunu düşünürdüm. Ama Linus Torvalds, John Carmack gibi bazı harikaların tarzına baktım ve C'deki eski kodlar olmasına rağmen, bir nesnenin analog eşdeğerini kodladığında, onu genellikle Carmack ile birlikte kodlamamayı başarıyor.
Dragon Energy

Ve doğal olarak, kaynak dosyadaki yardımcıların, gereğinden fazla dış başlık içeren bazı büyük başlıklara tercih edildiğini düşünüyorum çünkü sınıfın uygulanmasına yardımcı olmak için çok sayıda özel işlev kullandı. Ancak yukarıdaki ve diğerlerinin stilini inceledikten sonra, bir sınıfın tüm iç üyelerine ve sadece bir sınıfı uygulamak için bile düşünmesi gereken türlerden biraz daha genelleştirilmiş işlevler yazmanın genellikle mümkün olduğunu fark ettim. işlevi iyi adlandırmak ve sık sık çalışması gereken belirli üyeleri geçmek için daha fazla zaman kazandırır [...]
Dragon Energy

[...] gerekenden daha sonra, daha sonra manipüle edilmesi daha kolay olan daha net bir uygulama sağlar. Bu PredicateList, her şeye erişen "tam eşleme" için bir "yardımcı yüklem" yazmak yerine , yüklem listesinden bir veya iki üyeyi erişim gerektirmeyen biraz daha genelleştirilmiş bir işleve geçirmek genellikle uygun olabilir her özel üye PredicateListve çoğu zaman bu iç işleve daha açık, daha genel bir isim ve amaç sunma ve aynı zamanda "gez kodu yeniden kullanımı" için daha fazla fırsat sunma eğilimindedir.
Dragon Energy
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.