Home > database >  Print method for variadic template pairs in C
Print method for variadic template pairs in C

Time:11-30

I want to achieve something like:

export_vars("path/to/file.dat", {"variable_name", obj}, {"another_variable", 2});

where obj can be any type as long as it has an << overload - the idea is to write to an ofstream later on. I have tried (for an initializer_list of pairs):

void
export_vars(const std::string& path, std::initializer_list<std::pair<std::string, std::any>> args)
{       
    for (auto& [name, var] : args)
        std::cout << name << ": " << var << std::endl;
}

but std::any cannot be << without knowing the underlying type. Can it maybe be achieved using variadic templates and parameter pack expansion? I also tried something like:

template <class... Args>
void
export_vars(const std::string& path, Args... args)
{   
    (std::cout << ... << args.first << args.second) << std::endl;
}

but that's obviously wrong. Any suggestions?

CodePudding user response:

{..} has no type, and so disallow most deduction.

Several work arounds:

  • Change call to use std::pair explicitly:

    template <typename ... Pairs>
    void export_vars(const std::string&, const Pairs&... args)
    {       
        ((std::cout << args.first << ": " << args.second << std::endl), ...);
    }
    
    int main()
    {
        export_vars("unused", std::pair{"int", 42}, std::pair{"cstring", "toto"});
    }
    

    Demo

  • Don't use template:

    void export_vars(const std::string&,
                     const std::initializer_list<std::pair<std::string, Streamable>>& args)
    {
        for (const auto& [name, value] : args) {
            std::cout << name << ": " << value << std::endl;
        }
    }
    int main()
    {
        export_vars("unused", {{"int", 42}, {"cstring", "toto"}});
    }
    

    with Streamable using type-erasure, possibly something like:

    class Streamable
    {
        struct IStreamable
        {
            virtual ~IStreamable() = default;
            virtual void print(std::ostream&) = 0;
        };
    
        template <typename T>
        struct StreamableT : IStreamable
        {
            StreamableT(T t) : data(std::forward<T>(t)) {}
            virtual void print(std::ostream& os) { os << data; }
    
            T data;
        };
    
        std::unique_ptr<IStreamable> ptr;
    public:
        template <typename T>
        // Possibly some concepts/SFINAE as requires(is_streamable<T>)
        Streamable(T&& t) : ptr{std::make_unique<StreamableT<std::decay_t<T>>>(t)} {}
    
        friend std::ostream& operator << (std::ostream& os, const Streamable& streamable)
        {
            streamable.ptr->print(os);
            return os;
        } 
    };
    

    Demo

CodePudding user response:

A templated recursive function can solve this problem.

The recursive function takes as parameters:

  • the objects that need to pass through all layers, in this case an output stream reference
  • followed by objects you want to process one at a time, in this case a string together with a templated object
  • lastly followed by variadic parameter pack that captures all remaining arguments.

The recursive function process just the single given pair and then calls itself recursively on the variadic argument. At the end a trivial function is needed to end the recursion.

In this case, it's easier to use an output stream reference as that can be passed on recursively. You'll need to handle opening the file etc. in another function.

An example:

#include <string>
#include <iostream>
#include <utility>

void export_vars(std::ostream& o)
{
}

template<typename T, typename... Args>
void export_vars(std::ostream& o, const std::string& name, const T& var, Args&&... args)
{       
    o << name << ": " << var << std::endl;
    export_vars(o, std::forward<Args>(args)...);
}

int main()
{
    export_vars(std::cout, "test", int(0), "test2", unsigned(1));
}

Demo: https://godbolt.org/z/v9Gv9MG5d

In this case I chose to simply put the name and variable as separate objects as this actually requires the least syntax in use.

Pairs can of course also be used:

template<typename T, typename... Args>
void export_vars(std::ostream& o, const std::pair<std::string,T>& var, Args&&... args)
{       
    o << var.first << ": " << var.second << std::endl;
    export_vars(o, std::forward<Args>(args)...);
}

However, you cannot use the desired {"str",var} syntax with it as the compiler won't know to which type it should convert to. But std::make_pair("str",var) or std::pair{"str",var} should work.

CodePudding user response:

With a little bit of help from std::any docs I came up with this solution. It is not perfect, because you need to register printing functions (visitors) for each of your type by hand, but then at least you can use your export_vars with container of pairs <string, any>, and there are no recursive templates.

Demo

#include <type_traits>
#include <any>
#include <functional>
#include <iomanip>
#include <iostream>
#include <typeindex>
#include <typeinfo>
#include <unordered_map>
#include <vector>

template <class T, class F>
inline std::pair<const std::type_index, std::function<void(std::ostream& ostr, std::any const&)>> to_any_visitor(F const& f)
{
    return { std::type_index(typeid(T)), [g = f](std::ostream& ostr, std::any const& a) {
                if constexpr (std::is_void_v<T>)
                    g(ostr);
                else
                    g(ostr, std::any_cast<T const&>(a));
            } };
}

static std::unordered_map<std::type_index, std::function<void(std::ostream& ostr, std::any const&)>> any_visitor{
    to_any_visitor<void>([](std::ostream& ostr) { ostr << "{}"; }),
    to_any_visitor<int>([](std::ostream& ostr, int x) { ostr << x; }),
    to_any_visitor<unsigned>([](std::ostream& ostr, unsigned x) { ostr << x; }),
    to_any_visitor<float>([](std::ostream& ostr, float x) { ostr << x; }),
    to_any_visitor<double>([](std::ostream& ostr, double x) { ostr << x; }),
    to_any_visitor<char const*>([](std::ostream& ostr, char const* s) { ostr << std::quoted(s); })
};

void export_vars(std::ostream& ostr, const std::vector<std::pair<std::string, std::any>>& args)
{
    for (const auto& [name, var] : args)
    {
        if (const auto it = any_visitor.find(std::type_index(var.type())); it != any_visitor.cend())
        {
            ostr << name << ": ";
            it->second(ostr, var);
            ostr << std::endl;
        }
        else
        {
            throw std::runtime_error("Print function not registered");
        }
    }
}

int main()
{
    std::vector<std::pair<std::string, std::any>> pairs{ { "xxx", 123.456 }, { "yyy", "some text" }, { "zzz", 789 } };
    export_vars(std::cout, pairs);
    export_vars(std::cout, {{"xxx", 123}, {"yyy", 5.6}});  // this will also work
}
  • Related