关于c ++:类型擦除技术

Type erasure techniques

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

我知道,第一个也是最明显也是最常用的方法是虚拟函数。只需将类的实现隐藏在基于接口的类层次结构中。许多boost库都会这样做,例如boost.any这样做是为了隐藏您的类型和boost.shared&ptr这样做是为了隐藏(de)分配机制。

然后有一个选项,其中包含指向模板化函数的函数指针,同时将实际对象保存在一个void*指针中,比如boost.function会隐藏函数的实际类型。示例实现可以在问题的末尾找到。

所以,对于我的实际问题:你还知道什么其他类型的擦除技术?如果可能的话,请向他们提供示例代码、用例、您对它们的体验,或者提供进一步阅读的链接。

编辑(因为我不确定是把这个作为答案,还是编辑这个问题,所以我会做一个更安全的。)另一个隐藏没有虚拟功能或void*的东西的实际类型的好方法是gman在这里使用的一个方法,与我关于这个方法如何精确工作的问题有关。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
#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();
}


C++中的所有类型擦除技术都是用函数指针(行为)和EDCOX1〔0〕(用于数据)来完成的。"不同"的方法在添加语义糖的方式上完全不同。虚拟函数,例如,只是

1
2
3
4
5
6
struct Class {
    struct vtable {
        void (*dtor)(Class*);
        void (*func)(Class*,double);
    } * vtbl
};

IOW:函数指针。

也就是说,有一种技术我特别喜欢:它是shared_ptr,仅仅是因为它让那些不知道你能做到这一点的人失去了主意:你可以在shared_ptr中存储任何数据,并且在最后仍然有正确的析构函数调用,因为shared_ptr构造函数是一个函数模板,并且将使用默认情况下为创建删除程序而传递的实际对象:

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

当然,这只是通常的void*/函数指针类型擦除,但非常方便打包。


基本上,这些是您的选项:虚拟函数或函数指针。

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

在这种情况下,存储指向基的指针很好地工作,即使数据是单独存储的,如果您希望将多个操作应用于类型已擦除的数据。否则,将以多个函数指针(每种类型的已擦除函数一个)或具有指定要执行的操作的参数的函数结束。


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

在C++0x中,你有EDOCX1,2。

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


Stroustrup,在C++程序设计语言(第四版)第25.3章中指出:

Variants of the technique of using a single runt-time representation for values of a number of types and relying on the (static) type system to ensure that they are used only according to their declared type has been called type erasure.

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

斯特劳斯鲁的书中提供的例子同样令人愉快。

考虑实施template class Vector,一个沿着std::vector线的容器。当您将Vector与许多不同的指针类型一起使用时,通常情况下,编译器会为每个指针类型生成不同的代码。

通过为void*指针定义向量的专门化,然后将此专门化用作所有其他类型TVector的公共基实现,可以防止代码膨胀:

1
2
3
4
5
6
7
8
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)); }
};

正如你所看到的,我们有一个强类型的容器,但是EDOCX1,17,EDCOX1,18,EDCOX1,19,…,将共享相同的(C++和二进制)代码来实现,它们的指针类型在EDCOX1×0后面被擦除。


有关类型擦除技术的列表(相当短),以及关于权衡的讨论,请参阅本系列文章:第一部分,第二部分:第三部分:第四部分。

我还没有提到的是adobe.poly和boost.variant,它在某种程度上可以被视为类型擦除。


如马克所说,一个人可以使用铸造的std::shared_ptr。例如,将类型存储在函数指针中,将其强制转换并存储在只有一种类型的函数中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#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
    // !
}