类型擦除技术


136

(使用类型擦除,我的意思是隐藏有关某个类的某些或所有类型信息,有点像Boost.Any。)
我想掌握类型擦除技术,同时还要分享我所知道的那些技术。我的希望是找到一些在他/她最黑暗的时刻想到的疯狂技术。:)

我知道,第一个也是最常见的方法是虚函数。只需在基于接口的类层次结构中隐藏类的实现即可。许多Boost库都执行此操作,例如Boost.Any执行此操作以隐藏您的类型,Boost.Shared_ptr执行此操作以隐藏(取消)分配机制。

然后是带有指向模板化函数的函数指针的选项,同时将实际对象保留在void*指针中,例如Boost.Function确实隐藏了仿函数的真实类型。在问题的末尾可以找到示例实现。

那么,对于我的实际问题:
您还知道其他什么类型的擦除技术?请为他们提供示例代码,用例,您的使用经验以及可能的链接,以供进一步阅读。

编辑
(因为我不确定是否可以将其添加为答案,或者只是编辑问题,所以我会做得更安全。)
另一种很好的技巧是在没有虚函数或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();
}

1
用“类型擦除”,您真的是在指“多态性”吗?我认为“类型擦除”具有某种特定含义,通常与Java泛型相关联。
奥利弗·查尔斯沃思

3
@Oli:类型擦除可以通过多态实现,但这不是唯一的选择,我的第二个示例表明了这一点。:)和类型擦除,我的意思是,例如,您的结构不依赖于模板类型。Boost.Function不在乎是否向您提供函子,函数指针甚至lambda。与Boost.Shared_Ptr相同。您可以指定分配器和释放函数,但是的实际类型shared_ptr不能反映这一点,shared_ptr<int>例如,与标准容器不同,它始终是相同的。
Xeo

2
@Matthieu:我认为第二个示例也是安全的。您总是知道要使用的确切类型。还是我错过了什么?
Xeo

2
@Matthieu:你是对的。通常,这种As功能不会以这种方式实现。就像我说的那样,绝对不能安全使用!:)
Xeo

4
@lurscher:好吧...从未使用以下任何版本的boost或std版本?functionshared_ptrany,等?它们都采用了类型擦除功能,以提供甜美的用户便利。
Xeo

Answers:


100

C ++中的所有类型擦除技术都是通过函数指针(用于行为)和void*(用于数据)来完成的。“不同”方法的不同之处仅在于它们添加语义糖的方式不同。虚拟功能,例如,对于

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

iow:函数指针。

就是说,有一种我特别喜欢的技术:这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

好答案; 令人惊讶的是,如何为每种已删除类型静态创建一个假vtable的草图很有教育意义。请注意,fake-vtables和函数指针实现为您提供了已知的内存大小的结构(与纯虚拟类型相比),可以轻松地在本地存储它们,并(轻松地)将它们与要虚拟化的数据分开。
Yakk-Adam Nevraumont

因此,如果shared_ptr然后存储了一个Derived *,但Base *没有将析构函数声明为虚拟的,则shared_ptr <void>仍将按预期工作,因为它甚至都不知道要开始的基类。凉!
TamaMcGlinn

@Apollys:确实如此,但unique_ptr并不TYPE-删除删除器,所以如果你想要分配unique_ptr<T>到一个unique_ptr<void>,您需要提供有删除的说法,明确地知道如何删除T通过void*。如果你现在要分配S,太,那么你需要有删除,明确,知道如何删除T通过void*和也是一个S通过void*并且,给定一个void*知道它是否是一个T或一个S。到那时,您已经为编写了一个类型擦除的删除器unique_ptr,然后它也适用于unique_ptr。只是不是开箱即用。
Marc Mutz-mmutz

我觉得您回答的问题是“我该如何解决这一问题unique_ptr?” 对某些人有用,但没有解决我的问题。我猜答案是,因为共享指针在标准库的开发中得到了更多的关注。我认为有些遗憾,因为唯一指针更简单,因此应该更容易实现基本功能,并且它们更高效,因此人们应该更多地使用它们。相反,我们恰恰相反。
Apollys支持Monica19'Sep

54

从根本上讲,这些是您的选择:虚拟函数或函数指针。

存储数据并将其与功能关联的方式可能会有所不同。例如,您可以存储指向基础的指针,并让派生类包含数据虚函数实现,或者您可以将数据存储在其他地方(例如,在单独分配的缓冲区中),而只是让派生类提供虚拟函数实现,该实现void*指向数据。如果将数据存储在单独的缓冲区中,则可以使用函数指针而不是虚拟函数。

在这种情况下,即使要单独存储数据,也要对类型擦除的数据进行多种操作,存储指向基础的指针也能很好地工作。否则,您将获得多个函数指针(每个类型擦除的函数一个),或带有指定执行操作的参数的函数。


1
那么,换句话说,我在问题中给出的示例?但是,感谢您像这样编写它,尤其是对虚函数和对类型擦除的数据进行的多种操作。
Xeo

至少还有2个其他选项。我正在写一个答案。
John Dibling

25

我还将考虑(类似于void*)“原始存储”的使用:char buffer[N]

在C ++ 0x中,您需要这样做std::aligned_storage<Size,Align>::type

您可以在其中存储任何想要的东西,只要它足够小并且可以正确处理对齐方式即可。


4
是的,Boost.Function实际上使用了这个和我给出的第二个示例的组合。如果函子足够小,它将内部存储在functor_buffer中。很高兴知道std::aligned_storage,谢谢!:)
Xeo

您也可以为此使用展示位置
rustyx

2
@RustyX:实际上,您必须这样做。std::aligned_storage<...>::type只是一个原始缓冲区,与不同char [sizeof(T)],它被适当地对齐。但是,它本身是惰性的:它不初始化其内存,不构建对象,什么也不做。因此,一旦有了这种类型的缓冲区,就必须在其中手动构造对象(使用放置new或分配器construct方法),也必须手动销毁其中的对象(手动调用其析构函数或使用分配器destroy方法) )。
Matthieu M.

22

Stroustrup在C ++编程语言(第4版)第25.3节中指出:

使用多种类型的值的单个欠时表示并依靠(静态)类型系统以确保仅根据声明的类型使用它们的技术的变体称为类型擦除

特别是,如果我们使用模板,则不需要使用虚拟函数或函数指针来执行类型擦除。在其他答案中已经提到的根据存储在a中的类型进行正确的析构函数调用的情况std::shared_ptr<void>就是一个例子。

Stroustrup的书中提供的示例同样令人愉快。

考虑实现template<class T> class Vector,一个遵循的容器std::vector。当您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
没有亵渎的意思:我更喜欢CRTP而不是Stroustrup提供的技术。
davidhigh

@davidhigh是什么意思?
Paolo M

通过使用CRTP基类template<typename Derived> VectorBase<Derived>,然后将其专门化为,可以获取相同的行为(语法不太笨拙)template<typename T> VectorBase<Vector<T*> >。而且,这种方法不仅只适用于指针,而且不适用于任何类型。
davidhigh

3
请注意,良好的C ++链接器合并了相同的方法和功能:黄金链接器或MSVC comdat折叠。生成代码,但随后在链接期间将其丢弃。
Yakk-Adam Nevraumont

1
@davidhigh我想了解您的评论,并想知道是否可以给我一个链接或要搜索的模式的名称(不是CRTP,而是一种允许在没有虚拟函数或函数指针的情况下进行类型擦除的技术的名称) 。
恭喜


7

如Marc所述,可以使用cast std::shared_ptr<void>。例如,将类型存储在函数指针中,进行强制转换并将其存储在仅一种类型的函子中:

#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
    // !
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.