如何将 std::tuple 转换为 std::any?
How to cast std::tuple to std::any?
这是我正在尝试的:
auto fwd_args = std::forward_as_tuple(std::forward<Args>(args)...);
auto key = std::make_pair(std::type_index(typeid(T)), std::any(fwd_args));
错误是:
error C2440: '': cannot convert from 'std::tuple<const char (&)[78],_Ty &&>' to 'std::any'
追溯到这里:
factory->get<Font>(R"(C:\Fonts\myfont.ttf)", 24)
其中 Font
c'tor 是:
explicit Font(const std::string& filename, float fontSize=32) {
我的问题:
- 我可以将任意参数转换为
std::any
吗?
- 如果不是,我如何使用任意参数作为映射中的键?
完整代码如下:
class SingletonFactory {
public:
template<typename T, typename... Args>
const T &get(Args &&... args) {
auto fwd_args = std::forward_as_tuple(std::forward<Args>(args)...);
auto key = std::make_pair(std::type_index(typeid(T)), std::any(fwd_args));
auto it = _cache.find(key);
if(it != _cache.end()) {
return std::any_cast<T>(it->second);
}
return std::any_cast<T>(_cache.emplace(std::piecewise_construct, std::forward_as_tuple(key), fwd_args).first);
}
private:
std::map<std::pair<std::type_index, std::any>, std::any> _cache{};
};
我们可以尝试使用实现所有必要方法的结构来代替 std::pair<std::type_index, std::any>
...
我想做什么:
我正在尝试为我的游戏构建一些 assets/resources 的缓存。例如如果我想在两个不同的地方使用相同的字体,我不想加载它两次(这涉及从磁盘读取它并将其转换为纹理并将其上传到 GPU)。因为资源有句柄,所以确定性地调用它们的析构函数很重要(例如,当卸载关卡时,我会销毁工厂)。
我当然可以手动完成这一切,但我不想跟踪我使用 24px FontA 和 32px FontB 的位置并手动将这些对象传送到我的游戏中。我只想要可以将所有内容转储到其中的通用缓存。然后,如果我以前使用过该特定资产,很好,它会被重新使用,如果没有,它可以制作一个新的。如果我后来决定放弃那个关卡或资产或你有什么,我只需删除 get<> 它就消失了,我不必回溯并找到我通过管道传递它的每个地方。
如评论中所述,问题不在于 std::tuple
模板。它特别是您代码中的这一部分:
const T &get(Args &&... args) {
auto fwd_args = std::forward_as_tuple(std::forward<Args>(args)...);
这显然是一个引用元组。您甚至不能将 one 引用放在 std::any
中,更不用说它们的元组了。如果您可以接受副本,只需将参考资料放在此处即可。 C++ 中没有规定必须将模板参数包作为引用元组转发。
至于问题 2,“如果不是,我如何使用任意参数作为映射中的键?” - 你不能。映射键必须具有部分顺序。即使 float fontSize
已经有点问题了,如果有人会通过 NaN
.
请记住 std::any
和 std::function
等类型擦除工具仅公开接口
他们承诺仅此而已: std::any
封装了复制能力,仅此而已,因此您不能
比较相等性/散列一个 std::any
对象。使用额外的 std::function
只是为了存储
operator<
很麻烦(这实际上是为每种类型使用两个 vtable),你最好
使用手卷式擦除。
此外,根据您的要求推断,您必须特殊情况const char*
或const char(&)[N]
参数,因为您希望将它们存储为 std::string
,以及它们的比较运算符。这个
还解决了您的“在 std::any
中存储带有参考成员的 std::tuple
”问题。 (有关更多讨论,请参阅编辑说明 #2。)
您的 godbolt link 中的代码在某些地方不正确,尤其是您通过
T 的构造函数构造一个 std::any
的参数(缺少前面的 std::in_place_type<T>
,
那就是)。
为方便起见,以下实现使用 C++20,但它可以在
有一些修改的旧标准。
编辑 #1:修复了未初始化的初始哈希值,这对我来说真的是一个菜鸟错误。
编辑 #2:是的,特殊情况 const char*
的技巧并不好,它可以防止采取 const char*
的 c'tor从工作。您可以将其重写为“仅衰减每个参数并且不对 const char*
或 const char(&)[N]
采取任何特殊操作”,这对所有 c'tor 都有效。但这也只有在您传入字符串文字时才有效,否则您的哈希映射中可能会存储一个悬空指针。如果您指定每个真正想要通过 std::string
传递引用的位置(例如,通过使用像 "hello"s
的 UDL 或显式构建 std::string
),那么这种方法可能是可行的。
AFAIK 你不能得到 c'tors 的参数类型,因为 C++ 明确不允许获取 c'tors 的地址,如果你不能形成指向成员函数的指针,你就不能在它上面做模板技巧。此外,重载决议可能是实现这一目标的另一个障碍。
编辑#3:我没有注意到会有不可复制的对象要缓存。在这种情况下,std::any
没有用,因为它只能存储可复制的对象。使用类似类型的擦除技术也可以存储不可复制的对象。我的实现只是使用 std::unique_ptr
来存储擦除的键和值,强制将它们存储在堆上。这个简单的方法甚至支持不可复制的 和 不可移动类型。如果需要 SBO,则必须使用更复杂的方法来存储类型擦除的对象。
#include <iostream>
#include <unordered_map>
#include <type_traits>
// Algorithm taken from boost
template <typename T>
void hash_combine(std::size_t& seed, const T& value)
{
static constexpr std::size_t golden_ratio = []
{
if constexpr (sizeof(std::size_t) == 4)
return 0x9e3779b9u;
else if constexpr (sizeof(std::size_t) == 8)
return 0x9e3779b97f4a7c15ull;
}();
seed ^= std::hash<T>{}(value) + golden_ratio +
std::rotl(seed, 6) + std::rotr(seed, 2);
}
class Factory
{
public:
template <typename T, typename... Args>
const T& get(Args&&... args)
{
Key key = construct_key<T, Args...>(static_cast<Args&&>(args)...);
if (const auto iter = cache_.find(key); iter != cache_.end())
return static_cast<ValueImpl<T>&>(*iter->second).value;
Value value = key->construct();
const auto [iter, emplaced] = cache_.emplace(
std::piecewise_construct,
// Move the key, or it would be forwarded as an lvalue reference in the tuple
std::forward_as_tuple(std::move(key)),
// Also the value, remember that this tuple constructs a std::any, not a T
std::forward_as_tuple(std::move(value))
);
return static_cast<ValueImpl<T>&>(*iter->second).value;
}
private:
struct ValueModel
{
virtual ~ValueModel() noexcept = default;
};
template <typename T>
struct ValueImpl final : ValueModel
{
T value;
template <typename... Args>
explicit ValueImpl(Args&&... args): value(static_cast<Args&&>(args)...) {}
};
using Value = std::unique_ptr<ValueModel>;
struct KeyModel
{
virtual ~KeyModel() noexcept = default;
virtual std::size_t hash() const = 0;
virtual bool equal(const KeyModel& other) const = 0;
virtual Value construct() const = 0;
};
template <typename T, typename... Args>
class KeyImpl final : public KeyModel
{
public:
template <typename... Ts>
explicit KeyImpl(Ts&&... args): args_(static_cast<Ts&&>(args)...) {}
// Use hash_combine to get a hash
std::size_t hash() const override
{
std::size_t seed{};
std::apply([&](auto&&... args)
{
(hash_combine(seed, args), ...);
}, args_);
return seed;
}
bool equal(const KeyModel& other) const override
{
const auto* ptr = dynamic_cast<const KeyImpl*>(&other);
if (!ptr) return false; // object types or parameter types don't match
return args_ == ptr->args_;
}
Value construct() const override
{
return std::apply([](const Args&... args)
{
return std::make_unique<ValueImpl<T>>(args...);
}, args_);
}
private:
std::tuple<Args...> args_;
};
using Key = std::unique_ptr<KeyModel>;
using Hasher = decltype([](const Key& key) { return key->hash(); });
using KeyEqual = decltype([](const Key& lhs, const Key& rhs) { return lhs->equal(*rhs); });
std::unordered_map<Key, Value, Hasher, KeyEqual> cache_;
template <typename T, typename... Args>
static Key construct_key(Args&&... args)
{
constexpr auto decay_or_string = []<typename U>(U&& arg)
{
// convert to std::string if U decays to const char*
if constexpr (std::is_same_v<std::decay_t<U>, const char*>)
return std::string(arg);
// Or just decay the parameter otherwise
else
return std::decay_t<U>(arg);
};
using KeyImplType = KeyImpl<T, decltype(decay_or_string(static_cast<Args&&>(args)))...>;
return std::make_unique<KeyImplType>(decay_or_string(static_cast<Args&&>(args))...);
}
};
struct IntRes
{
int id;
explicit IntRes(const int id): id(id) {}
};
struct StringRes
{
std::string id;
explicit StringRes(std::string id): id(std::move(id)) {}
};
int main()
{
Factory factory;
std::cout << factory.get<IntRes>(42).id << std::endl;
std::cout << factory.get<StringRes>("hello").id << std::endl;
}
忽略您提供的代码,并阅读“我正在尝试做什么”。
这是我所有的:
#include <map>
#include <any>
#include <string_view>
class LevelCache {
std::map<const std::string_view, std::any> self_;
public:
template <class T> auto get(std::string_view key) const -> T const& {
// If it's not there what on earth do I return? Throw. You handle it.
return std::any_cast<T const&>(self_.at(key));
}
auto operator [](std::string_view key) -> std::any& {
return self_[key]; // not const. Used for insert
}
};
#include <iostream>
auto main() -> int {
auto lvl = LevelCache();
lvl["Some resource"] = 1;
lvl["a string"] = "some string"; // Not sure if UB, or not.
std::cout << "lvl[\"Some resource\"] -> " << lvl.get<int>("Some resource") << '\n'
<< "lvl[\"a string\"] -> " << lvl.get<char const*>("a string");
}
我想不出更好的办法。这能解决(部分)您面临的问题吗?因为我很难理解确切的问题。
Compiler Explorer
这是我正在尝试的:
auto fwd_args = std::forward_as_tuple(std::forward<Args>(args)...);
auto key = std::make_pair(std::type_index(typeid(T)), std::any(fwd_args));
错误是:
error C2440: '': cannot convert from 'std::tuple<const char (&)[78],_Ty &&>' to 'std::any'
追溯到这里:
factory->get<Font>(R"(C:\Fonts\myfont.ttf)", 24)
其中 Font
c'tor 是:
explicit Font(const std::string& filename, float fontSize=32) {
我的问题:
- 我可以将任意参数转换为
std::any
吗? - 如果不是,我如何使用任意参数作为映射中的键?
完整代码如下:
class SingletonFactory {
public:
template<typename T, typename... Args>
const T &get(Args &&... args) {
auto fwd_args = std::forward_as_tuple(std::forward<Args>(args)...);
auto key = std::make_pair(std::type_index(typeid(T)), std::any(fwd_args));
auto it = _cache.find(key);
if(it != _cache.end()) {
return std::any_cast<T>(it->second);
}
return std::any_cast<T>(_cache.emplace(std::piecewise_construct, std::forward_as_tuple(key), fwd_args).first);
}
private:
std::map<std::pair<std::type_index, std::any>, std::any> _cache{};
};
我们可以尝试使用实现所有必要方法的结构来代替 std::pair<std::type_index, std::any>
...
我想做什么:
我正在尝试为我的游戏构建一些 assets/resources 的缓存。例如如果我想在两个不同的地方使用相同的字体,我不想加载它两次(这涉及从磁盘读取它并将其转换为纹理并将其上传到 GPU)。因为资源有句柄,所以确定性地调用它们的析构函数很重要(例如,当卸载关卡时,我会销毁工厂)。
我当然可以手动完成这一切,但我不想跟踪我使用 24px FontA 和 32px FontB 的位置并手动将这些对象传送到我的游戏中。我只想要可以将所有内容转储到其中的通用缓存。然后,如果我以前使用过该特定资产,很好,它会被重新使用,如果没有,它可以制作一个新的。如果我后来决定放弃那个关卡或资产或你有什么,我只需删除 get<> 它就消失了,我不必回溯并找到我通过管道传递它的每个地方。
如评论中所述,问题不在于 std::tuple
模板。它特别是您代码中的这一部分:
const T &get(Args &&... args) {
auto fwd_args = std::forward_as_tuple(std::forward<Args>(args)...);
这显然是一个引用元组。您甚至不能将 one 引用放在 std::any
中,更不用说它们的元组了。如果您可以接受副本,只需将参考资料放在此处即可。 C++ 中没有规定必须将模板参数包作为引用元组转发。
至于问题 2,“如果不是,我如何使用任意参数作为映射中的键?” - 你不能。映射键必须具有部分顺序。即使 float fontSize
已经有点问题了,如果有人会通过 NaN
.
请记住 std::any
和 std::function
等类型擦除工具仅公开接口
他们承诺仅此而已: std::any
封装了复制能力,仅此而已,因此您不能
比较相等性/散列一个 std::any
对象。使用额外的 std::function
只是为了存储
operator<
很麻烦(这实际上是为每种类型使用两个 vtable),你最好
使用手卷式擦除。
此外,根据您的要求推断,您必须特殊情况const char*
或const char(&)[N]
参数,因为您希望将它们存储为 std::string
,以及它们的比较运算符。这个
还解决了您的“在 std::any
中存储带有参考成员的 std::tuple
”问题。 (有关更多讨论,请参阅编辑说明 #2。)
您的 godbolt link 中的代码在某些地方不正确,尤其是您通过
T 的构造函数构造一个 std::any
的参数(缺少前面的 std::in_place_type<T>
,
那就是)。
为方便起见,以下实现使用 C++20,但它可以在 有一些修改的旧标准。
编辑 #1:修复了未初始化的初始哈希值,这对我来说真的是一个菜鸟错误。
编辑 #2:是的,特殊情况 const char*
的技巧并不好,它可以防止采取 const char*
的 c'tor从工作。您可以将其重写为“仅衰减每个参数并且不对 const char*
或 const char(&)[N]
采取任何特殊操作”,这对所有 c'tor 都有效。但这也只有在您传入字符串文字时才有效,否则您的哈希映射中可能会存储一个悬空指针。如果您指定每个真正想要通过 std::string
传递引用的位置(例如,通过使用像 "hello"s
的 UDL 或显式构建 std::string
),那么这种方法可能是可行的。
AFAIK 你不能得到 c'tors 的参数类型,因为 C++ 明确不允许获取 c'tors 的地址,如果你不能形成指向成员函数的指针,你就不能在它上面做模板技巧。此外,重载决议可能是实现这一目标的另一个障碍。
编辑#3:我没有注意到会有不可复制的对象要缓存。在这种情况下,std::any
没有用,因为它只能存储可复制的对象。使用类似类型的擦除技术也可以存储不可复制的对象。我的实现只是使用 std::unique_ptr
来存储擦除的键和值,强制将它们存储在堆上。这个简单的方法甚至支持不可复制的 和 不可移动类型。如果需要 SBO,则必须使用更复杂的方法来存储类型擦除的对象。
#include <iostream>
#include <unordered_map>
#include <type_traits>
// Algorithm taken from boost
template <typename T>
void hash_combine(std::size_t& seed, const T& value)
{
static constexpr std::size_t golden_ratio = []
{
if constexpr (sizeof(std::size_t) == 4)
return 0x9e3779b9u;
else if constexpr (sizeof(std::size_t) == 8)
return 0x9e3779b97f4a7c15ull;
}();
seed ^= std::hash<T>{}(value) + golden_ratio +
std::rotl(seed, 6) + std::rotr(seed, 2);
}
class Factory
{
public:
template <typename T, typename... Args>
const T& get(Args&&... args)
{
Key key = construct_key<T, Args...>(static_cast<Args&&>(args)...);
if (const auto iter = cache_.find(key); iter != cache_.end())
return static_cast<ValueImpl<T>&>(*iter->second).value;
Value value = key->construct();
const auto [iter, emplaced] = cache_.emplace(
std::piecewise_construct,
// Move the key, or it would be forwarded as an lvalue reference in the tuple
std::forward_as_tuple(std::move(key)),
// Also the value, remember that this tuple constructs a std::any, not a T
std::forward_as_tuple(std::move(value))
);
return static_cast<ValueImpl<T>&>(*iter->second).value;
}
private:
struct ValueModel
{
virtual ~ValueModel() noexcept = default;
};
template <typename T>
struct ValueImpl final : ValueModel
{
T value;
template <typename... Args>
explicit ValueImpl(Args&&... args): value(static_cast<Args&&>(args)...) {}
};
using Value = std::unique_ptr<ValueModel>;
struct KeyModel
{
virtual ~KeyModel() noexcept = default;
virtual std::size_t hash() const = 0;
virtual bool equal(const KeyModel& other) const = 0;
virtual Value construct() const = 0;
};
template <typename T, typename... Args>
class KeyImpl final : public KeyModel
{
public:
template <typename... Ts>
explicit KeyImpl(Ts&&... args): args_(static_cast<Ts&&>(args)...) {}
// Use hash_combine to get a hash
std::size_t hash() const override
{
std::size_t seed{};
std::apply([&](auto&&... args)
{
(hash_combine(seed, args), ...);
}, args_);
return seed;
}
bool equal(const KeyModel& other) const override
{
const auto* ptr = dynamic_cast<const KeyImpl*>(&other);
if (!ptr) return false; // object types or parameter types don't match
return args_ == ptr->args_;
}
Value construct() const override
{
return std::apply([](const Args&... args)
{
return std::make_unique<ValueImpl<T>>(args...);
}, args_);
}
private:
std::tuple<Args...> args_;
};
using Key = std::unique_ptr<KeyModel>;
using Hasher = decltype([](const Key& key) { return key->hash(); });
using KeyEqual = decltype([](const Key& lhs, const Key& rhs) { return lhs->equal(*rhs); });
std::unordered_map<Key, Value, Hasher, KeyEqual> cache_;
template <typename T, typename... Args>
static Key construct_key(Args&&... args)
{
constexpr auto decay_or_string = []<typename U>(U&& arg)
{
// convert to std::string if U decays to const char*
if constexpr (std::is_same_v<std::decay_t<U>, const char*>)
return std::string(arg);
// Or just decay the parameter otherwise
else
return std::decay_t<U>(arg);
};
using KeyImplType = KeyImpl<T, decltype(decay_or_string(static_cast<Args&&>(args)))...>;
return std::make_unique<KeyImplType>(decay_or_string(static_cast<Args&&>(args))...);
}
};
struct IntRes
{
int id;
explicit IntRes(const int id): id(id) {}
};
struct StringRes
{
std::string id;
explicit StringRes(std::string id): id(std::move(id)) {}
};
int main()
{
Factory factory;
std::cout << factory.get<IntRes>(42).id << std::endl;
std::cout << factory.get<StringRes>("hello").id << std::endl;
}
忽略您提供的代码,并阅读“我正在尝试做什么”。
这是我所有的:
#include <map>
#include <any>
#include <string_view>
class LevelCache {
std::map<const std::string_view, std::any> self_;
public:
template <class T> auto get(std::string_view key) const -> T const& {
// If it's not there what on earth do I return? Throw. You handle it.
return std::any_cast<T const&>(self_.at(key));
}
auto operator [](std::string_view key) -> std::any& {
return self_[key]; // not const. Used for insert
}
};
#include <iostream>
auto main() -> int {
auto lvl = LevelCache();
lvl["Some resource"] = 1;
lvl["a string"] = "some string"; // Not sure if UB, or not.
std::cout << "lvl[\"Some resource\"] -> " << lvl.get<int>("Some resource") << '\n'
<< "lvl[\"a string\"] -> " << lvl.get<char const*>("a string");
}
我想不出更好的办法。这能解决(部分)您面临的问题吗?因为我很难理解确切的问题。
Compiler Explorer