【问题标题】:How to cast std::tuple to std::any?如何将 std::tuple 转换为 std::any?
【发布时间】:2022-01-11 06:54:58
【问题描述】:

这是我正在尝试的:

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));

错误是:

错误 C2440: '': 无法从 'std::tuple' 转换为 'std::any'

这可以追溯到这里:

factory->get<Font>(R"(C:\Fonts\myfont.ttf)", 24)

Font c'tor 在哪里:

explicit Font(const std::string& filename, float fontSize=32) {

我的问题:

  1. 我可以将任意 args 转换为 std::any 吗?
  2. 如果不是,我如何使用任意 args 作为映射中的键?

完整代码如下:

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&lt;std::type_index, std::any&gt;...

Example on Godbolt


我想做什么:

我正在尝试为我的游戏的一些资产/资源构建缓存。例如如果我想在两个不同的地方使用相同的字体,我不想加载它两次(这涉及从磁盘读取它并将其转换为纹理并将其上传到 GPU)。而且由于资源有句柄,因此确定性地调用它们的析构函数很重要(例如,在卸载关卡时,我将销毁工厂)。

我当然可以手动完成这一切,但我不想跟踪我在哪里使用 24px FontA 和 32px FontB 并在我的游戏周围手动管道这些对象。我只想要可以将所有内容转储到的通用缓存。然后,如果我以前使用过该特定资产,那太好了,它会被重新使用,如果没有,它可以制作一个新的。如果我后来决定放弃那个级别或资产或你有什么,我只需删除 get 并且它就消失了,我不必回溯并找到我通过管道的每个地方。

【问题讨论】:

  • 试图破坏 C++ 的类型安全性(这是 C++ 的核心、基本、内置属性)总是以泪水告终。
  • 根据std::any 上的cppreference:“any 类描述了用于任何 可复制构造类型的单个值的类型安全容器。”我不认为引用元组是可复制构造的。
  • @mpen 在允许的情况下在该页面上见下文。
  • 与具体问题分开,使用pair&lt;std::type_index, std::any&gt;作为std::map的键已经不行了。 std::any 没有定义任何比较运算符。
  • Ehm... (8) 是复制构造函数,而不是 (2)。并且要求是所有元组的元素都是可复制构造的。

标签: c++ c++20


【解决方案1】:

请记住,std::anystd::function 等类型擦除工具仅公开接口 他们承诺仅此而已:std::any 封装了复制能力,仅此而已,因此您不能 比较相等/散列 std::any 对象。使用额外的std::function 只是为了存储 operator&lt; 很麻烦(基本上每种类型都使用两个 vtable),你最好 使用手卷式擦除。

此外,根据您的要求,您必须特例 const char*const char(&amp;)[N] 参数,因为您希望它们存储为std::string,并且还用于它们的比较运算符。这 还解决了您的“使用std::any 中的参考成员存储std::tuple”问题。 (有关更多讨论,请参阅编辑说明 #2。)

您的神螺栓链接中的代码在某些地方不正确,尤其是您传递的代码 T 的构造函数的参数构造一个std::any(缺少前面的std::in_place_type&lt;T&gt;, 就是)。

为方便起见,以下实现使用 C++20,但它可以在 旧标准稍作修改。

编辑 #1:修复了未初始化的初始哈希值,对我来说真的是一个菜鸟错误。

编辑 #2:是的,特殊情况 const char* 的技巧不是很好,它会阻止使用 const char* 的 c'tors 工作。您可以将其重写为“仅衰减每个参数,不对const char*const char(&amp;)[N] 采取任何特殊操作”,这将适用于所有c'tors。但这也仅在您传入字符串文字时才有效,否则您可能会在哈希映射中存储一个悬空指针。如果您通过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;
}

【讨论】:

  • 做得很好。本地seed不应该在KeyImpl::hash()中初始化吗?
  • 感谢您的详细回答,但我认为这里有问题。缓存永远不会命中? godbolt.org/z/MPMo5YxzK
  • 另一件事,std::is_same_v&lt;std::decay_t&lt;U&gt;, const char*&gt; 技巧是否会阻止我在其构造函数参数中拥有实际上确实采用const char* 的对象?是否可以从 T 的构造函数中提取类型,而不是尝试从 Args 推断它们?
  • @mpen 抱歉,我错过了对象也可能不可复制的要求。在这种情况下,std::any 没有用处,因为std::any 要求它存储的对象是可复制的。我将再次更新答案,调整代码以支持不可复制的类型。
  • @mpen 至于为什么 d'tor 被调用两次:请注意缓存未命中路径中有 std::any 的移动构造。 std::any 仅在对象类型较小且该类型不可移动,以满足 noexcept-ness 的 std::any 的移动 c'tor 时才使用小缓冲区优化。 No special c'tors 意味着IntRes 适合SBO,因此调用IntRes move c'tor,导致另一个d'tor 输出;编写复制 c'tor 禁用移动,从而禁用 SBO,移动 std::any 只是复制指针(到堆分配的对象),所以没有额外的 d'tor 输出。
【解决方案2】:

如 cmets 中所述,问题不在于 std::tuple 模板。这是您代码中的这一部分:

const T &get(Args &&... args) {
    auto fwd_args = std::forward_as_tuple(std::forward<Args>(args)...);

这显然是一个引用元组。您甚至不能将 一个 引用放在 std::any 中,更不用说它们的元组了。如果您可以忍受副本,只需将参考资料放在此处即可。 C++ 中没有规定必须将模板参数包作为引用元组转发。

至于 Q2,“如果没有,我如何使用任意 args 作为映射中的键?” - 你不能。映射键必须具有部分排序。即使是float fontSize 也已经有点问题了,如果有人会通过NaN

【讨论】:

  • 不用太担心 NaN,所有的 fontSizes 都会被我硬编码。我可以忍受副本。不知道为什么我不能进行部分排序。如果 type_index 匹配,那么我应该能够进行比较。
  • @mpen:only 可以使用type_index,问题是比较两个std::any 对象;你不知道他们有什么类型。以类型擦除的 lambda 形式存储该信息确实是一种解决方法。
  • 您可以将引用元组放在std::any 中,只要它们是左值引用:-)。问题是第二个元组元素是该实例化中的右值引用,这使得元组无法复制构造。
【解决方案3】:

忽略您提供的代码,并阅读“我正在尝试做什么”。
这就是我所拥有的:

#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

【讨论】:

  • 没有。您缺少两个关键要求。一个是我不想预先填充缓存。二是get() 应该接受 args,而不仅仅是一个键。
  • 好的,如果我错了,请纠正我:您希望工厂在您调用 get 时返回对某个资源的引用,并且它应该在内部存储该资源?你为什么要使用地图?您不需要存储传递给构造函数的参数,对吗?不就是和存储构造的对象一样吗?
  • 是的,如果它已经构建,我希望它返回一个引用。是的,我确实需要存储参数。我如何区分“arial.ttf”和“verdana.ttf”?它们都是 Font 对象,但它们是不同的资源。
  • this code 是否至少解决了一个问题(暂时忽略另一个问题;即,这总是会创建对象)
  • 是的。缓存确实是最重要的部分:-)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-05-23
  • 1970-01-01
  • 1970-01-01
  • 2021-03-17
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多