C++ 中可变参数模板对的打印方法

Print method for variadic template pairs in C++

我想实现这样的目标:

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

其中 obj 可以是任何类型,只要它具有 << 重载 - 想法是稍后写入 ofstream。我试过(initializer_list 对):

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

std::any 不能在不知道基础类型的情况下成为 <<。可以使用可变参数模板和参数包扩展来实现吗?我也试过类似的东西:

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

但这显然是错误的。有什么建议吗?

{..} 没有类型,因此不允许大多数推导。

几个变通办法:

  • 更改调用以明确使用 std::pair

    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

  • 不使用模板:

    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 使用类型擦除,可能类似于:

    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

模板递归函数可以解决这个问题。

递归函数作为参数:

  • 需要通过所有层的对象,在本例中为输出流引用
  • 后面是您要一次处理一个的对象,在本例中是一个字符串和一个模板化对象
  • 最后是捕获所有剩余参数的可变参数包。

递归函数只处理单个给定对,然后在可变参数上递归调用自身。 最后需要一个简单的函数来结束递归。

在这种情况下,使用输出流引用更容易,因为它可以递归传递。您需要在另一个函数中处理打开文件等。

一个例子:

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

演示:https://godbolt.org/z/v9Gv9MG5d

在这种情况下,我选择简单地将名称和变量作为单独的对象,因为这实际上需要使用最少的语法。

对当然也可以用:

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

但是,您不能对它使用所需的 {"str",var} 语法,因为编译器不知道它应该转换成哪种类型。但是 std::make_pair("str",var)std::pair{"str",var} 应该可以。

std::any docs 的一点帮助下,我想到了这个解决方案。它并不完美,因为您需要手动为每种类型注册打印功能(访问者),但至少您可以将 export_vars 与成对 的容器一起使用,并且没有递归模板。

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
}