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"});
}
不使用模板:
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;
}
};
模板递归函数可以解决这个问题。
递归函数作为参数:
- 需要通过所有层的对象,在本例中为输出流引用
- 后面是您要一次处理一个的对象,在本例中是一个字符串和一个模板化对象
- 最后是捕获所有剩余参数的可变参数包。
递归函数只处理单个给定对,然后在可变参数上递归调用自身。
最后需要一个简单的函数来结束递归。
在这种情况下,使用输出流引用更容易,因为它可以递归传递。您需要在另一个函数中处理打开文件等。
一个例子:
#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
与成对 的容器一起使用,并且没有递归模板。
#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
}
我想实现这样的目标:
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"}); }
不使用模板:
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; } };
模板递归函数可以解决这个问题。
递归函数作为参数:
- 需要通过所有层的对象,在本例中为输出流引用
- 后面是您要一次处理一个的对象,在本例中是一个字符串和一个模板化对象
- 最后是捕获所有剩余参数的可变参数包。
递归函数只处理单个给定对,然后在可变参数上递归调用自身。 最后需要一个简单的函数来结束递归。
在这种情况下,使用输出流引用更容易,因为它可以递归传递。您需要在另一个函数中处理打开文件等。
一个例子:
#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
与成对
#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
}