【问题标题】:Print method for variadic template pairs in C++C++中可变参数模板对的打印方法
【发布时间】:2022-01-06 08:03:28
【问题描述】:

我想达到这样的目标:

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 在不知道底层类型的情况下不能是 &lt;&lt;。可以使用可变参数模板和参数包扩展来实现吗?我也尝试过类似的东西:

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

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

【问题讨论】:

  • 你的问题是{stuff, other_stuff}没有类型。你不能对没有类型的东西进行类型推断。
  • 是的,我一开始遇到了这个错误。使用pairs根本不可行吗?
  • 应该是:((std::cout &lt;&lt;args.first &lt;&lt; ": " &lt;&lt; args.second &lt;&lt; std::endl), ...);Demo
  • 天啊!不幸的是,我仍然得到@NathanOliver 提到的Candidate template ignored: substitution failure: deduced incomplete pack &lt;(no value)&gt; for template parameter 'Args'
  • 但是,这行得通:export_vars("path", std::pair{ "hello", 1 }, std::pair{ "hello", "world" }); .. 有没有办法帮助编译器推断这是它与可变参数模板的一对?

标签: c++ templates cout variadic parameter-pack


【解决方案1】:

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

几个变通方法:

  • 更改调用以明确使用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"}});
    }
    

    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

【讨论】:

  • 我认为这可能是最好的解决方案;否则 export_vars("path", "name1", var1, "name2", var2, ...) 带有折叠表达式也可能是一个不错的方法。
【解决方案2】:

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

递归函数作为参数:

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

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

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

一个例子:

#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::pair 解决方案需要使用更多语法。类型擦除解决方案不也会遭受更多的实例化(除了更多的代码)吗?
  • 您可以使用template&lt;typename ...Ts&gt; void export_vars(std::ostream&amp; o, const std::pair&lt;std::string,Ts&gt;&amp;... pairs) 强制执行std::pair 而无需递归。折叠与递归不仅仅是一个样式问题,因为它会影响编译时间。
  • 类型擦除解决方案不是行为等效的,因此与其进行比较并不真正相关;但它有一个按类型实例化(并且不依赖于与可变参数模板解决方案相反的参数数量),但它具有虚拟调用的运行时成本。
  • @Jardo42 定义template&lt;typename... Ts&gt; void export_vars(const std::string&amp; path, const std::pair&lt;std::string,Ts&gt;&amp;... pairs) 不幸的是仍然不允许自动类型推断。假设这是不可能的,至少在 C++17 中是不可能的。
【解决方案3】:

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
}

【讨论】:

  • 使用专用擦除类型而不是std::any,您可能会避免注册并进行编译时检查而不是运行时检查。
  • @pptaszni 我一开始也做了类似的事情,但是手动映射使其对于自定义类等来说是非通用的。
猜你喜欢
  • 2013-06-04
  • 1970-01-01
  • 2022-01-14
  • 2016-12-25
  • 2016-05-08
  • 1970-01-01
  • 2021-05-19
  • 2021-12-11
  • 1970-01-01
相关资源
最近更新 更多