型消去テクニック


136

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

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

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

それで、私の実際の質問のために:
あなたは他にどんなタイプの消去テクニックを知っていますか?可能な場合は、サンプルコード、使用例、それらについての経験、および詳細を読むためのリンクを提供してください。

編集
(これを回答として追加するか、質問を編集するだけか確信が持てなかったので、安全
な方法を実行します。)仮想関数や操作なしで何かの実際のタイプを非表示にするもう1つの優れた手法void*は、 1つの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();
}

1
「型消去」とは、本当に「多態性」を指しているのですか。「型消去」にはやや特定の意味があると思います。これは通常、たとえばJavaジェネリックに関連付けられています。
Oliver Charlesworth 2011年

3
@オリ:型消去はポリモーフィズムで実装できますが、それが唯一のオプションではありません。私の2番目の例はそれを示しています。:)そして型消去とは、あなたの構造体が例えばテンプレート型に依存しないことを意味します。Boost.Functionは、ファンクター、関数ポインター、またはラムダにさえフィードしても構いません。Boost.Shared_Ptrと同じです。アロケータと割り当て解除関数を指定できますが、実際のタイプはshared_ptrこれを反映していませんshared_ptr<int>。たとえば、標準のコンテナとは異なり、常に同じです。
Xeo

2
@Matthieu:2番目の例もタイプセーフだと思います。あなたは常にあなたが操作している正確なタイプを知っています。それとも何か不足していますか?
Xeo

2
@Matthieu:その通りです。通常、そのようなAs(s)関数はそのように実装されません。私が言ったように、決して安全に使用できません!:)
Xeo

4
@lurscher:まあ... 次のいずれかのブーストまたは標準バージョンを使用したことはありませんか?functionshared_ptrany、など?彼らはすべて甘い甘いユーザーの利便性のために型消去を採用しています。
Xeo

回答:


100

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タイプの消去ですが、非常に便利にパッケージ化されています。


9
偶然にも、私はshared_ptr<void>数日前に実装例を書いて私の友人に動作を説明しなければなりませんでした。:)それは本当にクールです。
Xeo

いい答えだ; 驚くべきことに、消去されたタイプごとにfake-vtableを静的に作成する方法のスケッチは非常に教育的です。fake-vtablesと関数ポインターの実装では、ローカルに簡単に格納でき、仮想化しているデータから(簡単に)分離できる既知のメモリサイズの構造(純粋仮想タイプと比較)が提供されることに注意してください。
Yakk-Adam Nevraumont 2016年

そのため、shared_ptrがDerived *を格納しているが、Base *がデストラクタを仮想として宣言していなかった場合でも、shared_ptr <void>は、最初から基本クラスを認識していないため、意図したとおりに機能します。涼しい!
TamaMcGlinn

@Apollys:それはありませんが、unique_ptrあなたが割り当てたい場合は、デリータを消去に入力しないようunique_ptr<T>unique_ptr<void>、あなたは削除する方法を知っている、明示的、deleterは引数を提供する必要がT通過void*。あなたは今、割り当てたい場合はS、あまりにも、あなたはデリータを必要とする、明示的に、それは削除する方法を知っていTvoid*S通じvoid*および、与えられたvoid*知っていることだかどうTS。その時点で、型消去されたの削除プログラムを作成したunique_ptrので、でも機能しunique_ptrます。箱から出しただけではありません。
マルク・ムッツ-mmutz

あなたが回答した質問は「これが機能しないという事実をどうやって回避するのunique_ptrですか?」一部の人には便利ですが、私の質問には対応しませんでした。答えは、標準ライブラリの開発で共有ポインタがより注目されたためだと思います。一意のポインタは単純なので、少し悲しいと思います。基本的な機能を実装する方が簡単で、より効率的であるため、人々はそれらをより多く使用する必要があります。代わりに、私たちは正反対です。
Apollysがモニカをサポートする

54

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

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

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


1
それで、言い換えれば、私が質問で与えた例ですか?ただし、このように記述していただきありがとうございます。特に、仮想関数と、タイプ消去されたデータに対する複数の操作について説明します。
Xeo

他に少なくとも2つのオプションがあります。回答を作成しています。
John Dibling、

25

void*「生のストレージ」の使用も検討します(と同様)。char buffer[N]

C ++ 0xでは std::aligned_storage<Size,Align>::typeこれです。

十分に小さく、配置を適切に処理する限り、そこに必要なものを格納できます。


4
ええ、そうです、Boost.Functionは実際には、これと私が与えた2番目の例の組み合わせを使用しています。ファンクタが十分に小さい場合、内部でfunctor_buffer内に格納します。知っておいてよかったstd::aligned_storage、ありがとう!:)
Xeo

これには、新しい配置を使用することもできます
rustyx

2
@RustyX:実際、あなたはそうしなければなりませstd::aligned_storage<...>::typeとは異なりchar [sizeof(T)]、適切に整列された単なる生のバッファです。ただし、それ自体は不活性です。メモリを初期化せず、オブジェクトを構築しません。したがって、このタイプのバッファをnew取得constructしたら、(配置またはアロケータメソッドを使用して)バッファ内にオブジェクトを手動で構築する必要があります。また、バッファ内のオブジェクトも手動で破棄する必要があります(デストラクタを手動で呼び出すか、アロケータdestroyメソッドを使用します)。)。
Matthieu M.16年

22

でStroustrup氏、C ++プログラミング言語(第4版)§25.3、状態:

いくつかの型の値に単一のランタイム表現を使用し、それらが宣言された型に従ってのみ使用されることを保証するために(静的)型システムに依存する手法のバリアントは、型消去と呼ばれています。

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

Stroustrupの本で提供されている例も同じくらい楽しいです。

template<class T> class Vector沿ってコンテナを実装することを考えてくださいstd::vector。いつ使うかVector異なるポインタの種類がたくさんで、それはしばしば起こるように、コンパイラは、おそらくすべてのポインタ型ごとに異なるコードを生成します。

このコードの肥大化は、ポインターのベクターの特殊化を定義し、void*この特殊化をVector<T*>他のすべての型の共通の基本実装として使用することで防ぐことができます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 ++共有すると、そのポインタ型が持つ、実装のバイナリ)コードを消去背後にありますvoid*


2
冒とくする意味はありません。Stroustrupの手法よりもCRTPの方が好きです。
davidhigh 2015年

@davidhighどういう意味ですか?
Paolo M

CRTP基本クラスtemplate<typename Derived> VectorBase<Derived>を使用することで、同じ動作を(より厄介な構文で)取得できます。CRTP基本クラスは、として特殊化されtemplate<typename T> VectorBase<Vector<T*> >ます。さらに、このアプローチはポインターだけでなく、どのタイプでも機能します。
davidhigh 2015年

3
優れたC ++リンカーは、同一のメソッドと関数(ゴールドリンカーまたはMSVCコマンドフォールディング)をマージすることに注意してください。コードは生成されますが、リンク中に破棄されます。
Yakk-Adam Nevraumont 2016年

1
@davidhigh私はあなたのコメントを理解しようとしていますが、検索するパターンのリンクまたは名前(CRTPではなく、仮想関数または関数ポインターなしで型消去を可能にする手法の名前)を私に与えることができるかどうか疑問に思っています。敬具、-クリス
クリス・キアソン2016年


7

マークが述べたように、キャストを使用できますstd::shared_ptr<void>。たとえば、を関数ポインタに格納し、キャストして、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
    // !
}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.