有没有办法在 C++ 中同时为多个模板分配一个类型?
Is there a way to simultaneously assign a type to multiple templates in C++?
本题基于下面的示例代码,其灵感来自Sean Parent's talk。
下面代码的目标是提供一个类似于 boost::any 的对象包装器。我写这段代码是为了让自己了解类型擦除。因此,这段代码没有实际用途(考虑到已经有 boost::any)。
class ObjWrap {
public:
template <typename T>
ObjWrap(T O) : Self(new Obj<T>(std::move(O))) {}
template <typename T>
friend typename T * getObjPtr(ObjWrap O) {
return static_cast<T*>(O.Self->getObjPtr_());
}
private:
struct Concept {
virtual ~Concept() = 0;
virtual void* getObjPtr_() = 0;
};
template <typename T>
struct Obj : Concept {
Obj(T O) : Data(std::move(O)) {}
void* getObjPtr_() { return static_cast<void*>(&Data); }
T Data;
};
std::unique_ptr<Concept> Self;
};
在我真正提出问题之前,让我们从以下几个方面检查代码:
Concept::getObjPtr_
returns void*
因为 a) Concept
不能是模板,否则 unique_ptr<Concept> Self
将不起作用; b) void*
是我知道如何在 C++ 中以类型不可知的方式 return Obj::Data
的唯一方法。如有不妥请指正...
T * getObjPtr(ObjWrap O)
是一个模板,需要与 ObjWrap
构造函数分开实例化。
ObjWrap
的使用主要包括:a) 在现有对象上创建一个新的ObjWrap
; b) 检索给定 ObjWrap
的基础对象。例如:
ObjWrap a(1);
ObjWrap b(std::string("b"));
int* p_a = getObjPtr<int>(a);
std::string* p_b = getObjPtr<std::string>(b);
这有效,但很明显 getObjPtr<int>(b)
没有按预期工作。
所以,我的问题是:
有没有办法修复上面的代码,以便我们可以简单地使用 int* p_a = getObjPtr(a)
和 std::string* p_b = getObjPtr(b)
或更好的 auto p_a = getObjPtr(a)
和 auto p_b = getObjPtr(b)
?换句话说,C++中有没有办法同时实例化两个模板(如果是这样,我们可以在ObjWrap
对象的编译时实例化ObjWrap
构造函数和T* getObjPtr(ObjWrap)
,例如,ObjWrap a(1)
)?
编辑 1:
使 ObjWrap 成为模板化 class 无济于事,因为它违背了类型擦除的目的。
template <typename T>
class ObjWrap {
/* ... */
};
ObjWrap<int> a(1); // this is no good for type erasure.
编辑 2:
我正在阅读代码并意识到可以对其进行修改以更好地反映这个想法。所以,也请看下面的代码:
class ObjWrap {
public:
template <typename T>
ObjWrap(T O) : Self(new Obj<T>(std::move(O))) {}
template <typename T>
T * getObjPtr() {
return static_cast<T*>(Self->getObjPtr_());
}
private:
struct Concept {
virtual ~Concept() = 0;
virtual void* getObjPtr_() = 0;
};
template <typename T>
struct Obj : Concept {
Obj(T O) : Data(std::move(O)) {}
void* getObjPtr_() { return static_cast<void*>(&Data); }
T Data;
};
std::unique_ptr<Concept> Self;
};
int main() {
ObjWrap a(1);
ObjWrap b(std::string("b"));
int* p_a = a.getObjPtr<int>();
std::string* p_b = b.getObjPtr<std::string>();
std::cout << *p_a << " " << *p_b << "\n";
return 0;
}
此版本代码与上面版本的主要区别在于 T * getObjPtr()
是一个成员函数,由 ObjWrap
对象封装。
编辑 3:
我关于类型擦除的问题已被接受的答案回答。然而,关于多个模板的同时类型实例化的问题还有待回答。我的猜测是目前 C++ 不允许这样做,但很高兴听到对此有更多经验的人的意见。
- (etc. etc.) Please correct me if this is wrong...
你的前提至少在原则上是错误的,如果不是在实践中也是如此。您坚持要使 getObjPtr()
成为虚方法,并使用抽象基础 class。但是 - 你还没有确定这是必要的。记住——使用虚拟方法是昂贵的!为什么我要为虚拟机付费才能进行类型擦除?
Is there a way to fix the above code so that we can simply use int* p_a = getObjPtr(a)
牢记 Sean Parent 的演讲标题(与他在演讲中确实使用继承的事实相反),放弃继承,答案应该是“是”。 编辑:擦除类型的代码和un-erases类型的代码知道类型是什么就足够了——只要你不需要以 type-specific 方式对 type-erased 数据执行 。在 Sean Parent 的演讲中,你需要能够制作它的 non-trivial 副本,移动它,绘制它等等。使用 std::any
/boost::any
你可能需要复制和移动,这可能需要虚拟 - 但这是最常见的用例。
甚至 std::any
也限制了你能做什么和不能做什么,正如这个问题中所讨论的:
有几件事可能会有所帮助。
首先要说的是,如果 Obj 需要公开对象的地址,那不是 Sean Parent 的 'inheritance is the root of all evil' 类型擦除容器。
诀窍是确保 Obj 的接口提供包装器永远需要的所有语义操作和查询。
为了提供这个,在概念中缓存对象的地址及其type_id
通常是一个合理的想法。
考虑以下更新的示例,其中有一个 public 方法 - operator==。规则是如果两个 Obj 包含相同类型的对象并且这些对象比较相等,则它们相等。
注意地址和type_id:
1) 是实现细节,不会暴露在 Obj 的接口上
2) 无需虚拟调用即可访问,这会使不相等的情况短路。
#include <memory>
#include <utility>
#include <typeinfo>
#include <utility>
#include <cassert>
#include <iostream>
class ObjWrap
{
public:
template <typename T>
ObjWrap(T O) : Self(new Model<T>(std::move(O))) {}
// objects are equal if they contain the same type of model
// and the models compare equal
bool operator==(ObjWrap const& other) const
{
// note the short-circuit when the types are not the same
// this means is_equal can guarantee that the address can be cast
// without a further check
return Self->info == other.Self->info
&& Self->is_equal(other.Self->addr);
}
bool operator!=(ObjWrap const& other) const
{
return !(*this == other);
}
friend std::ostream& operator<<(std::ostream& os, ObjWrap const& o)
{
return o.Self->emit(os);
}
private:
struct Concept
{
// cache the address and type here in the concept.
void* addr;
std::type_info const& info;
Concept(void* address, std::type_info const& info)
: addr(address)
, info(info)
{}
virtual ~Concept() = default;
// this is the concept's interface
virtual bool is_equal(void const* other_address) const = 0;
virtual std::ostream& emit(std::ostream& os) const = 0;
};
template <typename T>
struct Model : Concept
{
Model(T O)
: Concept(std::addressof(Data), typeid(T))
, Data(std::move(O)) {}
// no need to check the pointer before casting it.
// Obj takes care of that
/// @pre other_address is a valid pointer to a T
bool is_equal(void const* other_address) const override
{
return Data == *(static_cast<T const*>(other_address));
}
std::ostream& emit(std::ostream& os) const override
{
return os << Data;
}
T Data;
};
std::unique_ptr<Concept> Self;
};
int main()
{
auto x = ObjWrap(std::string("foo"));
auto y = ObjWrap(std::string("foo"));
auto z = ObjWrap(int(2));
assert(x == y);
assert(y != z);
std::cout << x << " " << y << " " << z << std::endl;
}
本题基于下面的示例代码,其灵感来自Sean Parent's talk。 下面代码的目标是提供一个类似于 boost::any 的对象包装器。我写这段代码是为了让自己了解类型擦除。因此,这段代码没有实际用途(考虑到已经有 boost::any)。
class ObjWrap {
public:
template <typename T>
ObjWrap(T O) : Self(new Obj<T>(std::move(O))) {}
template <typename T>
friend typename T * getObjPtr(ObjWrap O) {
return static_cast<T*>(O.Self->getObjPtr_());
}
private:
struct Concept {
virtual ~Concept() = 0;
virtual void* getObjPtr_() = 0;
};
template <typename T>
struct Obj : Concept {
Obj(T O) : Data(std::move(O)) {}
void* getObjPtr_() { return static_cast<void*>(&Data); }
T Data;
};
std::unique_ptr<Concept> Self;
};
在我真正提出问题之前,让我们从以下几个方面检查代码:
Concept::getObjPtr_
returnsvoid*
因为 a)Concept
不能是模板,否则unique_ptr<Concept> Self
将不起作用; b)void*
是我知道如何在 C++ 中以类型不可知的方式 returnObj::Data
的唯一方法。如有不妥请指正...T * getObjPtr(ObjWrap O)
是一个模板,需要与ObjWrap
构造函数分开实例化。ObjWrap
的使用主要包括:a) 在现有对象上创建一个新的ObjWrap
; b) 检索给定ObjWrap
的基础对象。例如:ObjWrap a(1); ObjWrap b(std::string("b")); int* p_a = getObjPtr<int>(a); std::string* p_b = getObjPtr<std::string>(b);
这有效,但很明显 getObjPtr<int>(b)
没有按预期工作。
所以,我的问题是:
有没有办法修复上面的代码,以便我们可以简单地使用 int* p_a = getObjPtr(a)
和 std::string* p_b = getObjPtr(b)
或更好的 auto p_a = getObjPtr(a)
和 auto p_b = getObjPtr(b)
?换句话说,C++中有没有办法同时实例化两个模板(如果是这样,我们可以在ObjWrap
对象的编译时实例化ObjWrap
构造函数和T* getObjPtr(ObjWrap)
,例如,ObjWrap a(1)
)?
编辑 1:
使 ObjWrap 成为模板化 class 无济于事,因为它违背了类型擦除的目的。
template <typename T>
class ObjWrap {
/* ... */
};
ObjWrap<int> a(1); // this is no good for type erasure.
编辑 2:
我正在阅读代码并意识到可以对其进行修改以更好地反映这个想法。所以,也请看下面的代码:
class ObjWrap {
public:
template <typename T>
ObjWrap(T O) : Self(new Obj<T>(std::move(O))) {}
template <typename T>
T * getObjPtr() {
return static_cast<T*>(Self->getObjPtr_());
}
private:
struct Concept {
virtual ~Concept() = 0;
virtual void* getObjPtr_() = 0;
};
template <typename T>
struct Obj : Concept {
Obj(T O) : Data(std::move(O)) {}
void* getObjPtr_() { return static_cast<void*>(&Data); }
T Data;
};
std::unique_ptr<Concept> Self;
};
int main() {
ObjWrap a(1);
ObjWrap b(std::string("b"));
int* p_a = a.getObjPtr<int>();
std::string* p_b = b.getObjPtr<std::string>();
std::cout << *p_a << " " << *p_b << "\n";
return 0;
}
此版本代码与上面版本的主要区别在于 T * getObjPtr()
是一个成员函数,由 ObjWrap
对象封装。
编辑 3:
我关于类型擦除的问题已被接受的答案回答。然而,关于多个模板的同时类型实例化的问题还有待回答。我的猜测是目前 C++ 不允许这样做,但很高兴听到对此有更多经验的人的意见。
- (etc. etc.) Please correct me if this is wrong...
你的前提至少在原则上是错误的,如果不是在实践中也是如此。您坚持要使 getObjPtr()
成为虚方法,并使用抽象基础 class。但是 - 你还没有确定这是必要的。记住——使用虚拟方法是昂贵的!为什么我要为虚拟机付费才能进行类型擦除?
Is there a way to fix the above code so that we can simply use
int* p_a = getObjPtr(a)
牢记 Sean Parent 的演讲标题(与他在演讲中确实使用继承的事实相反),放弃继承,答案应该是“是”。 编辑:擦除类型的代码和un-erases类型的代码知道类型是什么就足够了——只要你不需要以 type-specific 方式对 type-erased 数据执行 。在 Sean Parent 的演讲中,你需要能够制作它的 non-trivial 副本,移动它,绘制它等等。使用 std::any
/boost::any
你可能需要复制和移动,这可能需要虚拟 - 但这是最常见的用例。
甚至 std::any
也限制了你能做什么和不能做什么,正如这个问题中所讨论的:
有几件事可能会有所帮助。
首先要说的是,如果 Obj 需要公开对象的地址,那不是 Sean Parent 的 'inheritance is the root of all evil' 类型擦除容器。
诀窍是确保 Obj 的接口提供包装器永远需要的所有语义操作和查询。
为了提供这个,在概念中缓存对象的地址及其type_id
通常是一个合理的想法。
考虑以下更新的示例,其中有一个 public 方法 - operator==。规则是如果两个 Obj 包含相同类型的对象并且这些对象比较相等,则它们相等。
注意地址和type_id:
1) 是实现细节,不会暴露在 Obj 的接口上
2) 无需虚拟调用即可访问,这会使不相等的情况短路。
#include <memory>
#include <utility>
#include <typeinfo>
#include <utility>
#include <cassert>
#include <iostream>
class ObjWrap
{
public:
template <typename T>
ObjWrap(T O) : Self(new Model<T>(std::move(O))) {}
// objects are equal if they contain the same type of model
// and the models compare equal
bool operator==(ObjWrap const& other) const
{
// note the short-circuit when the types are not the same
// this means is_equal can guarantee that the address can be cast
// without a further check
return Self->info == other.Self->info
&& Self->is_equal(other.Self->addr);
}
bool operator!=(ObjWrap const& other) const
{
return !(*this == other);
}
friend std::ostream& operator<<(std::ostream& os, ObjWrap const& o)
{
return o.Self->emit(os);
}
private:
struct Concept
{
// cache the address and type here in the concept.
void* addr;
std::type_info const& info;
Concept(void* address, std::type_info const& info)
: addr(address)
, info(info)
{}
virtual ~Concept() = default;
// this is the concept's interface
virtual bool is_equal(void const* other_address) const = 0;
virtual std::ostream& emit(std::ostream& os) const = 0;
};
template <typename T>
struct Model : Concept
{
Model(T O)
: Concept(std::addressof(Data), typeid(T))
, Data(std::move(O)) {}
// no need to check the pointer before casting it.
// Obj takes care of that
/// @pre other_address is a valid pointer to a T
bool is_equal(void const* other_address) const override
{
return Data == *(static_cast<T const*>(other_address));
}
std::ostream& emit(std::ostream& os) const override
{
return os << Data;
}
T Data;
};
std::unique_ptr<Concept> Self;
};
int main()
{
auto x = ObjWrap(std::string("foo"));
auto y = ObjWrap(std::string("foo"));
auto z = ObjWrap(int(2));
assert(x == y);
assert(y != z);
std::cout << x << " " << y << " " << z << std::endl;
}