web-dev-qa-db-ja.com

型消去技術

(型の消去では、 Boost.Any のように、クラスに関する型情報の一部またはすべてを非表示にすることを意味します。)
型消去技術を手に入れたいのですが、それらを共有することも知っています。私の希望は、誰かが彼/彼女の最も暗い時間に考えたクレイジーなテクニックを見つけることです。 :)

私が知っている最初で最も明白で一般的に採用されているアプローチは、仮想関数です。インターフェイスベースのクラス階層内でクラスの実装を非表示にするだけです。多くのBoostライブラリはこれを行います。たとえば、 Boost.Any はタイプを非表示にするためにこれを行い、 Boost.Shared_ptr は(割り当て解除)メカニズムを非表示にするためにこれを行います。

次に、実際のオブジェクトをvoid*ポインターで保持しながら、テンプレート関数への関数ポインターを使用するオプションがあります。 Boost.Function は、実際のタイプのファンクターを非表示にします。実装例は質問の最後にあります。

したがって、私の実際の質問:
他にどんなタイプの消去テクニックを知っていますか?可能な場合は、サンプルコード、ユースケース、使用経験、さらにはリンクを提供してください。

編集
(これを回答として追加するか、単に質問を編集するかどうかわからなかったので、より安全なものを実行します。)
何かの実際のタイプを非表示にするもう1つの素敵なテクニックwithout仮想関数またはvoid*をいじる、GManが採用している方法- こちら私の質問 に関連して、これがどのように機能するかについて。


サンプルコード:

#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();
}
129
Xeo

C++のすべての型消去手法は、関数ポインター(動作用)とvoid*(データ用)。 「異なる」メソッドは、セマンティックシュガーを追加する方法が異なるだけです。仮想関数は、たとえば、

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

iow:関数ポインタ。

ただし、私が特に気に入っているテクニックが1つあります。それはshared_ptr<void>、単にあなたがこれを行うことができると知らない人々の心を吹き飛ばすからです:あなたはshared_ptr<void>であり、最後に呼び出される正しいデストラクタがあります。なぜならshared_ptrコンストラクタは関数テンプレートであり、デフォルトで削除者を作成するために渡される実際のオブジェクトのタイプを使用します。

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

もちろん、これは通常のvoid*/function-pointer type erasure、しかし非常に便利にパッケージ化されています。

99

基本的に、これらはあなたのオプションです:仮想関数または関数ポインター。

データを保存して関数に関連付ける方法はさまざまです。たとえば、ベースへのポインタを保存し、派生クラスにデータを含めることができますand仮想関数の実装、またはデータを別の場所(たとえば、個別に割り当てられたバッファ)に保存できます。派生クラスに仮想関数の実装を提供させるだけで、void*データを指します。データを別のバッファーに保存する場合、仮想関数ではなく関数ポインターを使用できます。

型消去されたデータに複数の操作を適用したい場合は、データが個別に保存されている場合でも、このコンテキストではベースへのポインターの保存がうまく機能します。そうしないと、複数の関数ポインター(型消去された関数ごとに1つ)、または実行する操作を指定するパラメーターを持つ関数になります。

54

私も考慮します(void*)「rawストレージ」の使用:char buffer[N]

C++ 0xでは、std::aligned_storage<Size,Align>::type このため。

十分に小さく、アライメントを適切に処理する限り、そこに好きなものを保存できます。

25
Matthieu M.

C++プログラミング言語(第4版)§25.3のStroustrupは次のように述べています。

多数の型の値に単一のランタイム表現を使用し、(静的)型システムに依存して、宣言された型に従ってのみ使用されることを保証する手法のバリエーションが呼び出されました erasureと入力します。

特に、テンプレートを使用する場合、タイプ消去を実行するには仮想関数または関数ポインターの使用なしが必要です。 std::shared_ptr<void>に格納されているタイプに応じた正しいデストラクタ呼び出しのケースは、すでに他の回答で言及されていますが、その例です。

Stroustrupの本で提供されている例は、同様に楽しいものです。

template<class T> class Vectorの行に沿ったコンテナであるstd::vectorの実装を検討してください。 Vectorを多くの異なるポインター型で使用する場合、よくあることですが、コンパイラーはすべてのポインター型に対して異なるコードを生成すると思われます。

このcode bloatは、void*ポインターにVectorの特殊化を定義し、この特殊化を他のすべてのタイプTVector<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)); }
};

ご覧のとおり、強く型付けされたコンテナがありますが、Vector<Animal*>Vector<Dog*>Vector<Cat*>、...は同じものを共有します(C++ and binary) void*の後ろにポインタ型erasedを持つ実装のコード。

22
Paolo M

型消去技術の(かなり短い)リストとトレードオフに関する議論については、この一連の投稿を参照してください。 パートIパートIIパートIIIパートIV

私がまだ言及していないものは Adobe.PolyBoost.Variant であり、これはある程度タイプの消去と見なすことができます。

17
Andrzej

Marcが述べたように、キャストstd::shared_ptr<void>。たとえば、typeを関数ポインターに格納し、キャストして、1つのタイプのみのファンクターに格納します。

#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
    // !
}
7
Janek Olszak