类型擦除技术

Xeo*_*Xeo 129 c++ type-erasure

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

我所知道的第一个也是最明显的,也是最常用的方法是虚函数.只需在基于接口的类层次结构中隐藏类的实现.许多Boost库都这样做,例如Boost.Any这样做是为了隐藏你的类型,而Boost.Shared_ptr这样做是为了隐藏(de)分配机制.

然后有一个函数指针指向模板化函数的选项,同时将实际对象保存在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();
}
Run Code Online (Sandbox Code Playgroud)

Mar*_*utz 99

C++中的所有类型擦除技术都是使用函数指针(用于行为)和void*(用于数据)完成的."不同"方法在添加语义糖的方式上略有不同.虚函数,例如,只是语义糖

struct Class {
    struct vtable {
        void (*dtor)(Class*);
        void (*func)(Class*,double);
    } * vtbl
};
Run Code Online (Sandbox Code Playgroud)

iow:函数指针.

也就是说,有一种我特别喜欢的技术:它shared_ptr<void>只是因为它会让那些不知道你可以做到这一点的人感到震惊:你可以将任何数据存储在一个shared_ptr<void>,并且仍然拥有正确的析构函数.结束,因为shared_ptr构造函数是一个函数模板,默认情况下将使用为创建删除函数而传递的实际对象的类型:

{
    const shared_ptr<void> sp( new A );
} // calls A::~A() here
Run Code Online (Sandbox Code Playgroud)

当然,这只是通常的void*/函数指针式擦除,但包装非常方便.

  • 巧合的是,我不得不在几天前用一个示例实现向我的一个朋友解释`shared_ptr <void>`的行为.:)真的很酷. (9认同)

Ant*_*ams 54

从根本上说,这些是你的选择:虚函数或函数指针.

如何存储数据并将其与功能相关联可能会有所不同.例如,您可以存储指向基类的指针,并使派生类包含数据虚函数实现,或者您可以将数据存储在其他位置(例如,在单独分配的缓冲区中),并且只需提供派生类虚函数实现,它void*指向数据.如果将数据存储在单独的缓冲区中,则可以使用函数指针而不是虚函数.

如果您希望将多个操作应用于类型擦除数据,则即使数据是单独存储的,也可以在此上下文中存储指针到基础.否则,您最终会得到多个函数指针(每个函数指针对应一个类型擦除函数),或者带有指定要执行的操作的参数的函数.


Mat*_* M. 25

我也会考虑(类似void*)使用"原始存储":char buffer[N].

在C++ 0x中你有std::aligned_storage<Size,Align>::type这个.

你可以在那里存储你想要的任何东西,只要它足够小并且你正确处理对齐.

  • 好吧,Boost.Function实际上使用了这个和我给出的第二个例子的组合.如果仿函数足够小,它会将其存储在functor_buffer内部.很高兴知道`std :: aligned_storage`,谢谢!:) (4认同)
  • @RustyX:实际上,您*已经*。`std :: aligned_storage &lt;...&gt; :: type`只是一个原始缓冲区,与`char [sizeof(T)]`不同,它是适当对齐的。但是,它本身是惰性的:它不初始化其内存,不构建对象,什么也不做。因此,一旦有了这种类型的缓冲区,就必须在其中手动构造对象(使用“ new”或分配器“ construct”方法),也必须手动销毁其内部的对象(手动调用它们的对象)。析构函数或使用分配器的“销毁”方法)。 (2认同)

Pao*_*o M 22

Stroustrup,用C++编程语言(第4版)§25.3,声明:

对于多种类型的值使用单个runt-time表示并依赖于(静态)类型系统以确保仅根据其声明的类型使用它们的技术的变体被称为类型擦除.

特别是,如果我们使用模板,则不需要使用虚函数或函数指针来执行类型擦除.根据存储在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)); }
};
Run Code Online (Sandbox Code Playgroud)

正如你所看到的,我们有一个强类型的容器,但是Vector<Animal*>,Vector<Dog*>,Vector<Cat*>,...,将共享相同的(C++ 二进制)实现代码,有他们的指针类型删除后面void*.

  • 请注意,良好的C++链接器合并相同的方法和函数:黄金链接器或MSVC comdat折叠.生成代码,但在链接期间丢弃. (3认同)
  • 没有意义亵渎:我更喜欢CRTP到Stroustrup给出的技术. (2认同)
  • @davidhigh 我正在尝试理解您的评论,并想知道您是否可以给我一个链接或要搜索的模式名称(不是 CRTP,而是一种允许在没有虚函数或函数指针的情况下进行类型擦除的技术的名称) . 恭敬地,-克里斯 (2认同)

And*_*zej 17

请参阅这一系列帖子,了解(相当简短的)类型擦除技术列表以及有关权衡的讨论: 第一部分, 第二部分, 第三部分, 第四部分.

我还没有提到的那个Adobe.PolyBoost.Variant,它在某种程度上可以被认为是类型擦除.


Jan*_*zak 7

正如Marc所说,人们可以使用演员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
    // !
}
Run Code Online (Sandbox Code Playgroud)