简体   繁体   中英

Is there a better alternative to std::unordered_map<std::string, std::any> to have a map of strings to any type of value?

I'm writing a Log function that outputs to the console. The syntax that I eventually want to use would look like this:

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

Where value1 and value2 may not be of the same type (maybe a std::string for a name and an int their age). Is there a better approach that using std::unordered_map<std::string, std::any> to store the key value pairs and using std::any_cast to deduce the type before outputting to the console?

If the number of possible types is limited, use std::variant instead of std::any , eg

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

You can query a std::variant for the type it currently holds, via std::variant::index() or std::holds_alternative() , and then retrieve the value via std::get() (using index() with get() would be useful in a switch() block, for instance). Or, you can use std::get_if() to get a pointer to the data if it matches a given type.

Or, you can use std::visit() , which would be useful in a logging use-case, as you could provide separate visitors to log each possible type as needed.

Yes, there is a better approach:

#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;
    }
};

Usage:

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

Output:

Message 
    Key1: 10
    Key2: 1.4

The perfect solution would be akin to the one posted by @Miles Budnek:

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

Or with pairs:

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

But neither case will compile with this:

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

This is required: (@Miles used a tuple)

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

The reason is that even though we can see that Params should be deduced to int,double , the compiler does not because the brace syntax is not deducible. Deduction guides are no help here (even if we remade it into a Log constructor) since there is really no way how to match the brace syntax since it does not even have a type. I do not believe there is a solution to it with variadic templates.

But, the syntax you provided can lead to a solution. The only hope is to convert the brace initializer syntax to std::initializer_list<T> , but what T? It should look like std::pair<std::string_view,?> but each pair is different. Here, you could use std::any and it would work, there is an easier way.

Create a non-templated class LogPair with a templated constructor that can accept a brace initializer syntax matching any pair!

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

That's almost it, now log can accept std::initializer_list<LogPair> because it matches the syntax correctly and incorrect usage will lead to compile-time errors.

Now, the tempting this is to do the printing in the constructor, but the order of creation of these variables in unspecified and it would also precede the message print. So, the solution is to save it to a string printed later.

Note, that it is not possible to save the values in LogPair without reusing or reimplementing std::any . But that should not be required.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM