C++ 如何在 class 中缓存模板类型 T 的变量?
C++ How to cache a variable of template type T in a class?
假设我有一个这样的Foo
class,我需要它的很多实例。
class Foo {
public:
Pool* bars; // a global list of bars, each may have a different type
template<typename T>
T& AddBar(int x) {
return bars->emplace<T>(x);
}
template<typename T>
T& GetBar() {
return bars->get<T>(); // a very slow function
}
}
Foo
的所有实例共享同一个柱池,其中包含许多可能不同类型的柱。例如,bars
可能是柱 {A bar1, B bar2, A bar3, C bar4}
的列表,其中 ABC
是一些 class 类型,但每个 Foo foo
实例只能有一个特定的柱类型,例如,一个 foo
实例不能有两个 A
.
类型的柱
给定实例 Foo foo
,我可以使用 foo.GetBar<A>()
、foo.GetBar<B>()
等获取特定类型的柱,但调用 bars->get<T>()
函数既慢又昂贵.因此,我正在考虑缓存 GetBar()
的结果,以便后续调用可以立即 return 而无需再次查询池。
现在我想到了:我在成员函数中创建了一个静态变量来存储bar的值,它只被初始化和赋值一次。
template<typename T>
T& GetBar() {
static T bar {};
if (bar == T {}) {
bar = bars->get<T>(); // a very slow function
}
return bar;
}
问题是,使用 static
关键字,这个变量现在在 Foo
的所有实例之间共享。如果我尝试从不同的实例中获取 A
类型的柱,它们会 return 相同的结果。
Foo foo1;
Foo foo2;
foo1.AddBar<A>(1);
foo2.AddBar<A>(2);
foo1.GetBar<A>(); // returns a bar (type = A, value = 1)
foo2.GetBar<A>(); // returns the same bar with value 1, not 2
如何在 class 中缓存每个 T
类型的柱并防止它被其他实例共享?我不知道如何将泛型类型存储为成员变量,此外,存储 bar 的每个类型 T
可能是一个巨大的混乱。
编辑: 我知道将结果缓存在 class 之外的调用方会容易得多。我只是好奇 class.
中是否有一种优雅的缓存方式
Edit2: bars
是指向注册池的指针,其类型是复杂的数据结构,而不是原始列表或数组。澄清一下,我正在使用 EnTT 库将实体组件系统集成到我的应用程序中,但不确定池是如何在内部详细维护的。
Edit3: 如果您想知道 ABC
是什么,从概念上讲,这些类型在编译时是未知的。但需要在运行时确定。事实上,它们只是我实现的许多其他 class 类型,所以我也可以将它们硬编码到 Foo
class 中,在这种情况下我可能应该使用工厂模式和脚本用于自动代码生成的语言,但这首先会破坏使用泛型的目的。
在写mockup的时候,有了n的想法。 1.8e9-where's-my-share m.,对于你的“复杂注册表池”,我写的实际可能是 Foo
的实现。我留在那儿 Foo
只是为了也提供一些建议。如果你想拥有一种类型的多个变量,你当然必须更改映射的值类型,比如从 std::any
到 std::vector<std::any>
。否则请进一步说明您的问题。
#include <iostream>
#include <string>
#include <map>
#include <any>
struct Pool {
template<typename T>
void emplace(T x) {
this->elements_.insert_or_assign(typeid(T).hash_code(), std::make_any<T>(x));
}
template<typename T>
T& get() {
return std::any_cast<T&>(elements_.at(typeid(T).hash_code()));
}
private:
std::map<std::size_t, std::any> elements_;
};
class Foo {
public:
Foo(Pool& pool): bars_(pool) {}
void AddBar(int x) {
return bars_.emplace<int>(x);
}
template<typename T>
T& GetBar() {
return bars_.get<T>(); // a very slow function
}
private:
Pool& bars_;
};
int main(){
Pool pool;
pool.emplace(4.3); pool.emplace(std::string("a value"));
Foo foo1(pool);
foo1.AddBar(3);
std::cout << foo1.GetBar<int>() << "\n";
}
所有 ECS 实现都在其引擎盖深处的某个地方放弃了静态类型安全,尽管它们可以向用户隐藏丑陋的转换或使用 std::any
之类的东西,就像另一个不错的答案一样。
就是说,这里有另一种方法来解决它(简化但它应该给你正确的想法)它避免了地图查找,除了一次调用新类型的 get 函数时,T:
#include <iostream>
#include <unordered_map>
#include <typeinfo>
#include <any>
class Foo
{
public:
template <class T>
T& get()
{
// Fetch a unique index for T to use for our std::vector.
const std::size_t n = type_index<T>();
// Resize if it's a new type we're encountering.
if (n >= bars.size())
bars.resize(n+1);
// Emplace if it's a former type of bar that's new for this instance
// of Foo.
if (!bars[n].has_value())
bars[n].emplace<T>();
// Returns the bar for that index cast to T&.
return std::any_cast<T&>(bars[n]);
}
private:
// Stores all the elements.
std::vector<std::any> bars;
// Returns a unique type index for T.
template <class T>
static std::size_t type_index()
{
// Using static here avoids repeat lookups into the hash map.
static const std::size_t n = lookup_type_index<T>();
return n;
}
// Looks up a unique type index for T.
template <class T>
static std::size_t lookup_type_index()
{
// Warning: hash_code is not guaranteed to be unique for all
// types in all compilers (two different types could return
// the same hash code, e.g.). I recommend using something else but
// that gets a bit involved (can expand and show you how if
// needed). Also consider a lock here for thread safety.
std::size_t key = typeid(T).hash_code();
auto it = idxs.find(key);
if (it != idxs.end())
return it->second;
idxs[key] = counter;
return counter++;
}
static inline std::unordered_map<std::size_t, std::size_t> idxs;
static inline std::size_t counter = 0;
};
int main()
{
using namespace std;
Foo f, f2;
f.get<int>() = 123;
f.get<double>() = 1.23;
f2.get<int>() = 456;
f2.get<double>() = 4.56;
cout << f.get<int>() << endl; // --> 123
cout << f.get<double>() << endl; // --> 1.23
cout << f2.get<int>() << endl; // --> 456
cout << f2.get<double>() << endl; // --> 4.56
}
我没有费心去测试它,但它应该给你这个想法的要点。 更新:我费心去测试它并用一个仓促的程序,你可以 运行 在发现一些拼写错误后,只要仔细检查我写的东西,并意识到我至少应该尝试编译我写的东西。 为了避免不断地查看类型映射,我们将类型映射到向量中的索引。您可以按照原始示例所建议的那样使用子索引等进行扩展。以上说明了主要思想。
请注意上面代码中关于 std::type_info::hash_code
的警告,因为它适用于我的和其他答案。我可以提供一种安全且便携的替代方案,甚至不需要 RTTI,但会涉及到一些问题。如果您寻找在编译时可移植地将类型 T 映射到可以在 运行 时使用的整数的方法,通常可以找到一堆示例。
假设我有一个这样的Foo
class,我需要它的很多实例。
class Foo {
public:
Pool* bars; // a global list of bars, each may have a different type
template<typename T>
T& AddBar(int x) {
return bars->emplace<T>(x);
}
template<typename T>
T& GetBar() {
return bars->get<T>(); // a very slow function
}
}
Foo
的所有实例共享同一个柱池,其中包含许多可能不同类型的柱。例如,bars
可能是柱 {A bar1, B bar2, A bar3, C bar4}
的列表,其中 ABC
是一些 class 类型,但每个 Foo foo
实例只能有一个特定的柱类型,例如,一个 foo
实例不能有两个 A
.
给定实例 Foo foo
,我可以使用 foo.GetBar<A>()
、foo.GetBar<B>()
等获取特定类型的柱,但调用 bars->get<T>()
函数既慢又昂贵.因此,我正在考虑缓存 GetBar()
的结果,以便后续调用可以立即 return 而无需再次查询池。
现在我想到了:我在成员函数中创建了一个静态变量来存储bar的值,它只被初始化和赋值一次。
template<typename T>
T& GetBar() {
static T bar {};
if (bar == T {}) {
bar = bars->get<T>(); // a very slow function
}
return bar;
}
问题是,使用 static
关键字,这个变量现在在 Foo
的所有实例之间共享。如果我尝试从不同的实例中获取 A
类型的柱,它们会 return 相同的结果。
Foo foo1;
Foo foo2;
foo1.AddBar<A>(1);
foo2.AddBar<A>(2);
foo1.GetBar<A>(); // returns a bar (type = A, value = 1)
foo2.GetBar<A>(); // returns the same bar with value 1, not 2
如何在 class 中缓存每个 T
类型的柱并防止它被其他实例共享?我不知道如何将泛型类型存储为成员变量,此外,存储 bar 的每个类型 T
可能是一个巨大的混乱。
编辑: 我知道将结果缓存在 class 之外的调用方会容易得多。我只是好奇 class.
中是否有一种优雅的缓存方式Edit2: bars
是指向注册池的指针,其类型是复杂的数据结构,而不是原始列表或数组。澄清一下,我正在使用 EnTT 库将实体组件系统集成到我的应用程序中,但不确定池是如何在内部详细维护的。
Edit3: 如果您想知道 ABC
是什么,从概念上讲,这些类型在编译时是未知的。但需要在运行时确定。事实上,它们只是我实现的许多其他 class 类型,所以我也可以将它们硬编码到 Foo
class 中,在这种情况下我可能应该使用工厂模式和脚本用于自动代码生成的语言,但这首先会破坏使用泛型的目的。
在写mockup的时候,有了n的想法。 1.8e9-where's-my-share m.,对于你的“复杂注册表池”,我写的实际可能是 Foo
的实现。我留在那儿 Foo
只是为了也提供一些建议。如果你想拥有一种类型的多个变量,你当然必须更改映射的值类型,比如从 std::any
到 std::vector<std::any>
。否则请进一步说明您的问题。
#include <iostream>
#include <string>
#include <map>
#include <any>
struct Pool {
template<typename T>
void emplace(T x) {
this->elements_.insert_or_assign(typeid(T).hash_code(), std::make_any<T>(x));
}
template<typename T>
T& get() {
return std::any_cast<T&>(elements_.at(typeid(T).hash_code()));
}
private:
std::map<std::size_t, std::any> elements_;
};
class Foo {
public:
Foo(Pool& pool): bars_(pool) {}
void AddBar(int x) {
return bars_.emplace<int>(x);
}
template<typename T>
T& GetBar() {
return bars_.get<T>(); // a very slow function
}
private:
Pool& bars_;
};
int main(){
Pool pool;
pool.emplace(4.3); pool.emplace(std::string("a value"));
Foo foo1(pool);
foo1.AddBar(3);
std::cout << foo1.GetBar<int>() << "\n";
}
所有 ECS 实现都在其引擎盖深处的某个地方放弃了静态类型安全,尽管它们可以向用户隐藏丑陋的转换或使用 std::any
之类的东西,就像另一个不错的答案一样。
就是说,这里有另一种方法来解决它(简化但它应该给你正确的想法)它避免了地图查找,除了一次调用新类型的 get 函数时,T:
#include <iostream>
#include <unordered_map>
#include <typeinfo>
#include <any>
class Foo
{
public:
template <class T>
T& get()
{
// Fetch a unique index for T to use for our std::vector.
const std::size_t n = type_index<T>();
// Resize if it's a new type we're encountering.
if (n >= bars.size())
bars.resize(n+1);
// Emplace if it's a former type of bar that's new for this instance
// of Foo.
if (!bars[n].has_value())
bars[n].emplace<T>();
// Returns the bar for that index cast to T&.
return std::any_cast<T&>(bars[n]);
}
private:
// Stores all the elements.
std::vector<std::any> bars;
// Returns a unique type index for T.
template <class T>
static std::size_t type_index()
{
// Using static here avoids repeat lookups into the hash map.
static const std::size_t n = lookup_type_index<T>();
return n;
}
// Looks up a unique type index for T.
template <class T>
static std::size_t lookup_type_index()
{
// Warning: hash_code is not guaranteed to be unique for all
// types in all compilers (two different types could return
// the same hash code, e.g.). I recommend using something else but
// that gets a bit involved (can expand and show you how if
// needed). Also consider a lock here for thread safety.
std::size_t key = typeid(T).hash_code();
auto it = idxs.find(key);
if (it != idxs.end())
return it->second;
idxs[key] = counter;
return counter++;
}
static inline std::unordered_map<std::size_t, std::size_t> idxs;
static inline std::size_t counter = 0;
};
int main()
{
using namespace std;
Foo f, f2;
f.get<int>() = 123;
f.get<double>() = 1.23;
f2.get<int>() = 456;
f2.get<double>() = 4.56;
cout << f.get<int>() << endl; // --> 123
cout << f.get<double>() << endl; // --> 1.23
cout << f2.get<int>() << endl; // --> 456
cout << f2.get<double>() << endl; // --> 4.56
}
我没有费心去测试它,但它应该给你这个想法的要点。 更新:我费心去测试它并用一个仓促的程序,你可以 运行 在发现一些拼写错误后,只要仔细检查我写的东西,并意识到我至少应该尝试编译我写的东西。 为了避免不断地查看类型映射,我们将类型映射到向量中的索引。您可以按照原始示例所建议的那样使用子索引等进行扩展。以上说明了主要思想。
请注意上面代码中关于 std::type_info::hash_code
的警告,因为它适用于我的和其他答案。我可以提供一种安全且便携的替代方案,甚至不需要 RTTI,但会涉及到一些问题。如果您寻找在编译时可移植地将类型 T 映射到可以在 运行 时使用的整数的方法,通常可以找到一堆示例。