【问题标题】:Is there a better alternative to std::unordered_map<std::string, std::any> to have a map of strings to any type of value?有没有更好的替代 std::unordered_map<std::string, std::any> 将字符串映射到任何类型的值?
【发布时间】:2020-07-27 20:10:21
【问题描述】:

我正在编写一个输出到控制台的日志函数。我最终要使用的语法如下所示:

Log(Info, 
    "Message", 
    { {"Key1", value1},
      {"Key2", value2} })

其中value1value2 可能不是同一类型(可能是名称的std::string 和他们的年龄的int)。有没有更好的方法是使用std::unordered_map&lt;std::string, std::any&gt; 存储键值对并使用std::any_cast 在输出到控制台之前推断类型?

【问题讨论】:

  • void* 怎么样?
  • std::any 不起作用,无法打印。我会使用std::vector 和一个类作为元素类型,这个类有一个模板构造函数(有两个参数,一个字符串和一个模板)。
  • 这不是您想要的界面,但a template-based approach 可以完全避免对异构容器的需求。
  • @MilesBudnek 回答一下,我喜欢它胜过地图方法。

标签: c++


【解决方案1】:

如果可能的类型数量有限,请使用std::variant 而不是std::any,例如

std::unordered_map<std::string, std::variant<int, std::string, etc>>

您可以通过std::variant::index()std::holds_alternative() 查询std::variant 当前持有的类型,然后通过std::get() 检索值(使用index()get() 在@ 中很有用例如 987654336@ 块)。或者,您可以使用std::get_if() 获取指向数据的指针(如果它与给定类型匹配)。

或者,您可以使用std::visit(),这在日志记录用例中很有用,因为您可以根据需要提供单独的访问者来记录每种可能的类型。

【讨论】:

  • 更好的是,不要将异构数据存储在容器中。对此的需求极为罕见。另外,我认为您需要详细说明如何正确使用 index 和 get,因为最直接的方法 get&lt;index&gt; 显然会失败。
【解决方案2】:

是的,有更好的方法:

#include <iostream>
#include <string_view>
#include <sstream>

struct LogPair{
    template<typename T>
    LogPair(std::string_view key, T&& value){
        std::stringstream buffer;
        //Feel free to change
        buffer<<key <<": "<<std::forward<T>(value);
        msg = buffer.str();
    }

    std::string msg;
};

enum class LogLevel{
    Info = 0,
};
void log(LogLevel level, std::string_view msg, std::initializer_list<LogPair> args){
    (void)level;//TODO
    std::cout<<msg<<'\n';
    for(const LogPair& s: args)
        std::cout<<'\t'<<s.msg<<'\n';
}

struct Test{//Complex type with custom print
    float val;
    friend std::ostream& operator<<(std::ostream& os, const Test& t){
        os<<t.val;
        return os;
    }
};

用法:

int main(){
    int value1 = 10;
    Test value2{1.4};
    
    log(LogLevel::Info,"Message", { {"Key1", value1},{"Key2", value2} });
}

输出:

Message 
    Key1: 10
    Key2: 1.4

完美的解决方案类似于@Miles Budnek 发布的解决方案:

template <typename... Params>
void Log(LogLevel level, std::string_view message, const Params&... params);

或成对:

template <typename... Params>
void Log(LogLevel level, std::string_view message, const std::pair<std::string_view,Params>&... params)

但这两种情况都不会编译:

Log(level, "Msg", {"Key1",value1},{"Key2", value2})

这是必需的:(@Miles 使用了一个元组)

Log(level, "Msg", std::pair{"Key1",10},std::pair{"Key2", 1.2})

原因是即使我们可以看到Params 应该被推导出为int,double,编译器却没有,因为大括号语法是不可推导出的。推导指南在这里没有帮助(即使我们将其重新制作为 Log 构造函数),因为实际上没有办法匹配大括号语法,因为它甚至没有类型。我不相信有可变参数模板的解决方案。

但是,您提供的语法可以导致解决方案。唯一的希望是将大括号初始值设定项语法转换为std::initializer_list&lt;T&gt;,但是什么 T?它应该看起来像std::pair&lt;std::string_view,?&gt;,但每一对都不同。在这里,您可以使用std::any,它会起作用,还有一种更简单的方法。

使用模板化构造函数创建一个非模板化LogPair,它可以接受匹配任何对的大括号初始化器语法!

template<typename T> LogPair(std::string_view key, T&& value);

差不多了,现在日志可以接受std::initializer_list&lt;LogPair&gt;,因为它正确匹配语法,不正确的使用会导致编译时错误。

现在,很诱人的是在构造函数中进行打印,但未指定这些变量的创建顺序,并且它也会在消息打印之前。所以,解决办法是把它保存到稍后打印的字符串中。

请注意,如果不重用或重新实现 std::any,则无法保存 LogPair 中的值。但这不应该是必需的。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2016-09-10
    • 1970-01-01
    • 2011-06-06
    • 1970-01-01
    • 2015-07-11
    • 2014-01-11
    • 1970-01-01
    相关资源
    最近更新 更多