Tip silme teknikleri


136

(Tip silme ile, bir sınıfla ilgili tür bilgilerinin bir kısmını veya tamamını, Boost.Any gibi gizlemek istedim .)
Bildiğim bunları paylaşırken , tip silme tekniklerinden de yararlanmak istiyorum. Umudum, birinin en karanlık saatinde düşündüğü çılgın bir teknik bulmak gibi. :)

Biliyorum, ilk ve en belirgin ve yaygın olarak ele alınan yaklaşım sanal işlevlerdir. Sınıfınızın uygulanmasını arayüz tabanlı bir sınıf hiyerarşisinde gizlemeniz yeterlidir. Birçok Boost kütüphanesi bunu yapar, örneğin Boost.Any bunu türünüzü gizlemek için yapar ve Boost.Shared_ptr bunu (de) ayırma mekanizmasını gizlemek için yapar.

Daha sonra, gerçek nesneyi Boostvoid* gibi bir işaretçide tutarken, ayarlanmış işlevlere işlev işaretçileri olan bir seçenek vardır. İşlev, işlevin gerçek türünü gizlemek için yapar. Örnek uygulamalar sorunun sonunda bulunabilir.

Yani, asıl sorum için:
Başka hangi tip silme tekniklerini biliyorsunuz? Lütfen mümkünse örnek bir kod, kullanım örnekleri, onlarla olan deneyiminizi ve daha fazla okuma için bağlantılar sağlayın.

Düzenle
(Bunu bir cevap olarak eklemek isteyip istemediğimden emin olmadığımdan veya sadece soruyu düzenlediğimden, daha güvenli olanı yapacağım.) Sanal işlevler veya uğraşmadan
gerçek bir şeyi gizlemek için başka bir güzel teknik , bir Gman istihdam buraya alaka ile, sorumu tam olarak nasıl bu eserlerin üzerinde.void*


Örnek kod:

#include <iostream>
#include <string>

// NOTE: The class name indicates the underlying type erasure technique

// this behaves like the Boost.Any type w.r.t. implementation details
class Any_Virtual{
        struct holder_base{
                virtual ~holder_base(){}
                virtual holder_base* clone() const = 0;
        };

        template<class T>
        struct holder : holder_base{
                holder()
                        : held_()
                {}

                holder(T const& t)
                        : held_(t)
                {}

                virtual ~holder(){
                }

                virtual holder_base* clone() const {
                        return new holder<T>(*this);
                }

                T held_;
        };

public:
        Any_Virtual()
                : storage_(0)
        {}

        Any_Virtual(Any_Virtual const& other)
                : storage_(other.storage_->clone())
        {}

        template<class T>
        Any_Virtual(T const& t)
                : storage_(new holder<T>(t))
        {}

        ~Any_Virtual(){
                Clear();
        }

        Any_Virtual& operator=(Any_Virtual const& other){
                Clear();
                storage_ = other.storage_->clone();
                return *this;
        }

        template<class T>
        Any_Virtual& operator=(T const& t){
                Clear();
                storage_ = new holder<T>(t);
                return *this;
        }

        void Clear(){
                if(storage_)
                        delete storage_;
        }

        template<class T>
        T& As(){
                return static_cast<holder<T>*>(storage_)->held_;
        }

private:
        holder_base* storage_;
};

// the following demonstrates the use of void pointers 
// and function pointers to templated operate functions
// to safely hide the type

enum Operation{
        CopyTag,
        DeleteTag
};

template<class T>
void Operate(void*const& in, void*& out, Operation op){
        switch(op){
        case CopyTag:
                out = new T(*static_cast<T*>(in));
                return;
        case DeleteTag:
                delete static_cast<T*>(out);
        }
}

class Any_VoidPtr{
public:
        Any_VoidPtr()
                : object_(0)
                , operate_(0)
        {}

        Any_VoidPtr(Any_VoidPtr const& other)
                : object_(0)
                , operate_(other.operate_)
        {
                if(other.object_)
                        operate_(other.object_, object_, CopyTag);
        }

        template<class T>
        Any_VoidPtr(T const& t)
                : object_(new T(t))
                , operate_(&Operate<T>)
        {}

        ~Any_VoidPtr(){
                Clear();
        }

        Any_VoidPtr& operator=(Any_VoidPtr const& other){
                Clear();
                operate_ = other.operate_;
                operate_(other.object_, object_, CopyTag);
                return *this;
        }

        template<class T>
        Any_VoidPtr& operator=(T const& t){
                Clear();
                object_ = new T(t);
                operate_ = &Operate<T>;
                return *this;
        }

        void Clear(){
                if(object_)
                        operate_(0,object_,DeleteTag);
                object_ = 0;
        }

        template<class T>
        T& As(){
                return *static_cast<T*>(object_);
        }

private:
        typedef void (*OperateFunc)(void*const&,void*&,Operation);

        void* object_;
        OperateFunc operate_;
};

int main(){
        Any_Virtual a = 6;
        std::cout << a.As<int>() << std::endl;

        a = std::string("oh hi!");
        std::cout << a.As<std::string>() << std::endl;

        Any_Virtual av2 = a;

        Any_VoidPtr a2 = 42;
        std::cout << a2.As<int>() << std::endl;

        Any_VoidPtr a3 = a.As<std::string>();
        a2 = a3;
        a2.As<std::string>() += " - again!";
        std::cout << "a2: " << a2.As<std::string>() << std::endl;
        std::cout << "a3: " << a3.As<std::string>() << std::endl;

        a3 = a;
        a3.As<Any_Virtual>().As<std::string>() += " - and yet again!!";
        std::cout << "a: " << a.As<std::string>() << std::endl;
        std::cout << "a3->a: " << a3.As<Any_Virtual>().As<std::string>() << std::endl;

        std::cin.get();
}

1
"Tip silme" ile gerçekten "polimorfizm" den bahsediyor musunuz? Bence "tip silme" genellikle Java generics ile ilişkili biraz belirli bir anlamı vardır.
Oliver Charlesworth

3
@Oli: Tip silme polimorfizm ile uygulanabilir, ancak tek seçenek bu değil, ikinci örneğim bunu gösteriyor. :) Ve tür silme ile, sadece yapınızın örneğin bir şablon türüne bağlı olmadığını kastediyorum. İşlev, bir functor, bir işlev işaretçisi veya bir lambda besleyip beslememeniz önemli değildir. Boost.Shared_Ptr ile aynı. Bir ayırıcı ve ayırma işlevi belirtebilirsiniz, ancak gerçek türü shared_ptrbunu yansıtmaz, shared_ptr<int>örneğin standart konteynerin aksine her zaman aynı olacaktır .
Xeo

2
@Matthieu: İkinci örnek de güvenli yazın. Üzerinde çalıştığınız türü her zaman bilirsiniz. Yoksa bir şey mi kaçırıyorum?
Xeo

2
@ Matthieu: Haklısın. Normalde böyle bir As(s) işlevi bu şekilde uygulanamaz. Dediğim gibi, hiçbir şekilde kullanımı güvenli! :)
Xeo

4
@lurscher: Peki ... aşağıdakilerin hiçbirinin boost veya std versiyonlarını hiç kullanmadınız mı? function, shared_ptr, anyVb? Hepsi tatlı tatlı kullanıcı rahatlığı için tip silme kullanır.
Xeo

Yanıtlar:


100

C ++ 'daki tüm tip silme teknikleri, işlev göstergeleri (davranış için) ve void*(veri için) ile yapılır. "Farklı" yöntemler semantik şeker ekleme şekillerinde farklılık gösterir. Sanal işlevler, örneğin, sadece semantik şekerdir.

struct Class {
    struct vtable {
        void (*dtor)(Class*);
        void (*func)(Class*,double);
    } * vtbl
};

iow: fonksiyon göstergeleri.

Bununla birlikte, özellikle sevdiğim bir teknik var: Bunun shared_ptr<void>nedeni, bunu yapabileceğinizi bilmeyen insanların zihinlerini havaya uçurmasıdır: Herhangi bir veriyi bir a'da saklayabilir shared_ptr<void>ve yine de çünkü shared_ptrkurucu bir işlev şablonudur ve varsayılan olarak siliciyi oluşturmak için geçirilen gerçek nesnenin türünü kullanır:

{
    const shared_ptr<void> sp( new A );
} // calls A::~A() here

Tabii ki, bu sadece normal void*/ işlev işaretçisi tipi silme, ancak çok uygun bir şekilde paketlenmiş.


9
Tesadüfen, shared_ptr<void>bir arkadaşımın davranışını sadece birkaç gün önce örnek bir uygulama ile açıklamak zorunda kaldım . :) Gerçekten harika.
Xeo

İyi cevap; şaşırtıcı kılmak için, sahte silinebilir her silinen tip için statik olarak nasıl oluşturulabilir bir kroki çok eğitici. Sahte araçların ve işlev işaretçisi uygulamalarının, yerel olarak kolayca depolanabilen ve sanallaştırdıkları verilerden (kolayca) boşaltılabilen bilinen bellek boyutlu yapıları (saf sanal türlerle karşılaştırıldığında) sağladığını unutmayın.
Yakk - Adam Nevraumont

bu nedenle, shared_ptr daha sonra bir Türetilmiş * depolarsa, ancak Base *, yıkıcıyı sanal olarak bildirmediyse, shared_ptr <void>, başlangıçta bir temel sınıf bile bilmediğinden hala amaçlandığı gibi çalışır. Güzel!
TamaMcGlinn

@Apollys: Öyle ama unique_ptralmaması için bir atamak istiyorsanız, deleter tip-silmek unique_ptr<T>a unique_ptr<void>silmek olduğunu bilir, açıkça, bir deleter argüman sağlamak için gereken Tbir içinden void*. Şimdi bir atamak isterseniz Sde, o zaman açıkça, bu bir silme bilen bir deleter ihtiyaç Tbir içinden void*de bir ve Sbir ile void*, ve , belirli bir void*, bilir o olsun Tya da S. Bu noktada, tür için silinen bir silici yazdınız unique_ptrve sonra da işe yarıyor unique_ptr. Kutunun dışında değil.
Marc Mutz - mmutz

Yanıtladığınız sorunun "Bunun işe yaramadığı gerçeğini nasıl geçici olarak çözebilirim unique_ptr?" Bazı insanlar için kullanışlıdır, fakat soruma cevap vermedi. Sanırım cevap şudur, çünkü paylaşılan işaretçiler standart kütüphanenin geliştirilmesinde daha fazla ilgi görmüştür. Sanırım biraz üzücü çünkü benzersiz işaretçiler daha basit, bu yüzden temel işlevleri uygulamak daha kolay olmalı ve daha verimli olduklarından insanlar daha fazla kullanmalı. Bunun yerine tam tersi var.
Apollys, Monica

54

Temel olarak, bunlar seçeneklerinizdir: sanal işlevler veya işlev işaretçileri.

Verileri saklama ve işlevlerle ilişkilendirme şekli değişebilir. Örneğin, bir işaretçi-temel depolayabilir ve türetilmiş sınıfın verileri ve sanal işlev uygulamalarını içermesini sağlayabilir ya da verileri başka bir yerde (örneğin ayrı olarak ayrılmış bir arabellekte) depolayabilir ve türetilmiş sınıfın void*verilere işaret eden sanal işlev uygulamaları . Verileri ayrı bir arabellekte saklarsanız, sanal işlevler yerine işlev işaretçileri kullanabilirsiniz.

İşaretçi-tabana saklamak, veriler ayrı olarak depolansa bile, tür tarafından silinen verilerinize uygulamak istediğiniz birden çok işlem olsa bile bu bağlamda iyi çalışır. Aksi takdirde, birden çok işlev işaretçisi (tür silinmiş işlevlerin her biri için bir tane) veya gerçekleştirilecek işlemi belirten bir parametreye sahip işlevlerle sonuçlanırsınız.


1
Başka bir deyişle, soruda verdiğim örnekler? Yine de, bu şekilde yazdığınız için teşekkürler, özellikle sanal işlevler ve tür silinmiş veriler üzerinde çoklu işlemler.
Xeo

En az 2 seçenek daha var. Bir cevap yazıyorum.
John Dibling

25

Ben de (benzer dikkate alacağını void*"ham depolama" kullanımı): char buffer[N].

C ++ 0x'de bunun için var std::aligned_storage<Size,Align>::type.

İstediğiniz her şeyi, yeterince küçük olduğu ve hizalama ile düzgün bir şekilde uğraştığınız sürece saklayabilirsiniz.


4
Evet, Boost.Function aslında bunun ve verdiğim ikinci örneğin bir kombinasyonunu kullanıyor. Functor yeterince küçükse, functor_buffer içinde dahili olarak saklar. std::aligned_storageGerçi bilmek güzel , teşekkürler! :)
Xeo

Bunun için yeni yerleşimi de kullanabilirsiniz .
rustyx

2
@RustyX: Aslında, zorundasın . std::aligned_storage<...>::typeaksine char [sizeof(T)], uygun şekilde hizalanmış ham bir tampondur. Yine de, kendi başına inert: hafızasını başlatmaz, bir nesne inşa etmez, hiçbir şey. Bu nedenle, bu tür bir arabellek oluşturduktan sonra, içindeki nesneleri manuel olarak oluşturmanız gerekir (yerleşim newveya bir ayırıcı constructyöntemiyle) ve içindeki nesneleri de el ile yıkmanız gerekir (yıkıcılarını manuel olarak çağırmak veya bir ayırıcı destroyyöntemi kullanarak ).
Matthieu M.

22

Stroustrup, C ++ programlama dilinde (4. baskı) §25.3 , şunları belirtir:

Bir dizi tipin değerleri için tek bir çalışma zamanı gösterimi kullanma tekniğinin varyantlarına ve (statik) tip sistemine dayanarak bunların sadece beyan edilen tiplerine göre kullanılmalarını sağlamak için tip silme olarak adlandırılmıştır .

Özellikle, şablonlar kullanırsak tür silme işlemini gerçekleştirmek için sanal işlevler veya işlev işaretçileri kullanılmasına gerek yoktur. Diğer cevaplarda daha önce sözü edilen, a'da saklanan tipe göre doğru yıkıcı çağrısının durumu buna std::shared_ptr<void>bir örnektir.

Stroustrup'un kitabında verilen örnek aynı derecede eğlencelidir.

Uygulanması düşünün template<class T> class Vector, çizgisinde bir kap std::vector. VectorÇok sayıda farklı işaretçi türüyle kullanacağınız zaman , sıkça olduğu gibi, derleyici her işaretçi türü için farklı kod üretecektir.

Bu kod bloat'ı , işaretçiler için Vector uzmanlığını tanımlayarak void*ve daha sonra bu uzmanlığı Vector<T*>diğer tüm türler için ortak bir temel uygulama olarak kullanarak önlenebilir T:

template<typename T>
class Vector<T*> : private Vector<void*>{
// all the dirty work is done once in the base class only 
public:
    // ...
    // static type system ensures that a reference of right type is returned
    T*& operator[](size_t i) { return reinterpret_cast<T*&>(Vector<void*>::operator[](i)); }
};

Gördüğünüz gibi, biz kesinlikle yazılı konteyner ama var Vector<Animal*>, Vector<Dog*>, Vector<Cat*>, ..., aynı (C ++ paylaşacak ve onların işaretçi tipi olan ikili) uygulanması için kod sildim arkasında void*.


2
Küfür etmek anlamsız: CRTP'yi Stroustrup'un verdiği tekniğe tercih ederim.
davidhigh

@davidhigh Ne demek istiyorsun?
Paolo M

Aynı davranış (daha az akward sözdizimi ile) daha sonra olarak uzmanlaşmış bir CRTP taban sınıfı kullanarak template<typename Derived> VectorBase<Derived>elde edilebilir template<typename T> VectorBase<Vector<T*> >. Dahası, bu yaklaşım sadece işaretçiler için değil, her tür için de işe yarar.
davidhigh

3
İyi C ++ bağlayıcılarının aynı yöntemleri ve işlevleri birleştirdiğini unutmayın: altın bağlayıcı veya MSVC comdat katlama. Kod oluşturulur, ancak daha sonra bağlantı sırasında atılır.
Yakk - Adam Nevraumont

1
@davidhigh Yorumunuzu anlamaya çalışıyorum ve bana bir bağlantı veya arama yapılacak bir model adı (CRTP değil, sanal işlevler veya işlev işaretçileri olmadan tür silinmesine izin veren bir tekniğin adı) verebilir misiniz merak ediyorum . Saygılarımla, - Chris
Chris Chiasson


7

Marc tarafından belirtildiği gibi, döküm kullanılabilir std::shared_ptr<void>. Örneğin, türü bir işlev işaretçisinde saklayın, yayınlayın ve yalnızca bir tür bir işleçte saklayın:

#include <iostream>
#include <memory>
#include <functional>

using voidFun = void(*)(std::shared_ptr<void>);

template<typename T>
void fun(std::shared_ptr<T> t)
{
    std::cout << *t << std::endl;
}

int main()
{
    std::function<void(std::shared_ptr<void>)> call;

    call = reinterpret_cast<voidFun>(fun<std::string>);
    call(std::make_shared<std::string>("Hi there!"));

    call = reinterpret_cast<voidFun>(fun<int>);
    call(std::make_shared<int>(33));

    call = reinterpret_cast<voidFun>(fun<char>);
    call(std::make_shared<int>(33));


    // Output:,
    // Hi there!
    // 33
    // !
}
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.