(对于类型擦除,我的意思是隐藏有关类的部分或全部类型信息,有点像Boost.Any。)
我想要掌握类型擦除技术,同时也分享这些,我知道。我希望找到一些有人在他/她最黑暗的时刻想到的疯狂技巧。 :)
我所知道的第一个也是最明显的,也是最常用的方法是虚函数。只需在基于接口的类层次结构中隐藏类的实现。许多Boost库都这样做,例如Boost.Any这样做可以隐藏你的类型,Boost.Shared_ptr这样做可以隐藏(de)分配机制。
然后有一个带有模板化函数的函数指针的选项,而将实际对象保存在void*
指针中,就像Boost.Function一样,隐藏了函数的实际类型。示例实现可以在问题的最后找到。
所以,对于我的实际问题:
您知道哪种其他类型的擦除技术?如果可能的话,请提供示例代码,用例,您对它们的体验以及可能的进一步阅读链接。
修改
(由于我不确定是否将此作为答案添加,或者只是编辑问题,我只会做更安全的问题。)
隐藏实际类型的没有虚拟函数或void*
摆弄的另一个很好的技巧是GMan使用here,与my question关于如何准确相关这很有效。
示例代码:
#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();
}
答案 0 :(得分:97)
C ++中的所有类型擦除技术都是使用函数指针(用于行为)和void*
(用于数据)完成的。 “不同”方法在添加语义糖的方式上略有不同。虚函数,例如,只是
struct Class {
struct vtable {
void (*dtor)(Class*);
void (*func)(Class*,double);
} * vtbl
};
iow:函数指针。
也就是说,有一种我特别喜欢的技术:它是shared_ptr<void>
,只是因为它让那些不知道你可以做到这一点的人感到震惊:你可以将任何数据存储在{{ 1}},并且仍然在最后调用了正确的析构函数,因为shared_ptr<void>
构造函数是一个函数模板,并且默认情况下将使用为创建删除函数而传递的实际对象的类型:
shared_ptr
当然,这只是通常的{
const shared_ptr<void> sp( new A );
} // calls A::~A() here
/函数指针类型擦除,但包装非常方便。
答案 1 :(得分:53)
从根本上说,这些是你的选择:虚函数或函数指针。
如何存储数据并将其与功能相关联可能会有所不同。例如,您可以存储指向基类的指针,并使派生类包含数据和虚拟函数实现,或者您可以将数据存储在其他位置(例如,在单独分配的缓冲区中),只需让派生类提供虚函数实现,它将void*
指向数据。如果将数据存储在单独的缓冲区中,则可以使用函数指针而不是虚函数。
如果您希望将多个操作应用于类型擦除数据,则即使数据是单独存储的,也可以在此上下文中存储指针到基础。否则,您最终会得到多个函数指针(每个函数指针对应一个类型擦除函数),或者带有指定要执行的操作的参数的函数。
答案 2 :(得分:24)
我还会考虑(类似于void*
)使用“原始存储空间”:char buffer[N]
。
在C ++ 0x中,你有std::aligned_storage<Size,Align>::type
。
你可以在那里存储任何你想要的东西,只要它足够小并且你正确处理对齐。
答案 3 :(得分:20)
Stroustrup, C ++编程语言(第4版)§25.3,声明:
对多种类型的值使用单个runt-time表示并依赖于(静态)类型系统以确保仅根据其声明的类型使用它们的技术的变体被称为类型擦除
特别是,如果我们使用模板,则需要不使用虚函数或函数指针来执行类型擦除。根据存储在std::shared_ptr<void>
中的类型,在其他答案中已经提到的正确析构函数调用的情况就是一个例子。
Stroustrup的书中提供的例子同样令人愉快。
考虑实施template<class T> class Vector
,一个与std::vector
一致的容器。当您将Vector
与许多不同的指针类型一起使用时,编译器通常会为每种指针类型生成不同的代码。
可以通过为void*
指针定义 Vector 的特化,然后使用此专门化作为{{1的公共基础实现来防止代码膨胀对于所有其他类型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*>
,...,将共享相同的内容(C ++ 和二进制代码实现代码,其指针类型已擦除在Vector<Cat*>
后面。
答案 4 :(得分:16)
请参阅这一系列帖子,了解(相当简短的)类型擦除技术列表以及有关权衡的讨论: Part I, Part II, Part III, Part IV
我还没有看到的那个是Adobe.Poly和Boost.Variant,它在某种程度上可以被视为类型擦除。
答案 5 :(得分: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
// !
}