С++可变参数模板:实现可变参数函数
С++ variadic templates: implement variadic functor
我的同事给了我一个"small quiz",他让他的学生解决了一次。看来我的弱智无法理解现代C++功能的所有美妙之处。
主题:
实现一个 join
函数,接受任意仿函数并 returning 另一个仿函数,它的行为与它们中的任何一个一样。例如:
{
auto result = std::visit(custom::join(
[](std::string const& s) { return "it's a string"; },
[](std::pair<int, int> const& p) { return "it's a pair"; }
), var);
assert(result == "it's a string");
var = std::make_pair(10, 20);
auto lvalue_lambda = [](std::string const& s) { return "it's a string"; };
result = std::visit(custom::join(
lvalue_lambda,
[](std::pair<int, int> const& p) { return "it's a pair"; }
), var);
assert(result == "it's a pair");
}
好吧,经过一番思考我明白了,就 "type-safe union" 而言,std::variant
意味着 "one of the listed",所以我需要一个元组。试过类似的东西:
namespace custom
{
template<typename ...Functors>
class ResultFunctor
{
public:
ResultFunctor(Functors&&... funcs)
: m_funcs(std::make_tuple(std::move(funcs)...))
{}
template<typename ...Params>
auto operator()(Params... params) // that's where I got stuck
{
// return std::get<void(Params...)>(m_funcs)(params...); // No, the return type spoils this idea
return std::get<0>(m_funcs)(params...); // Now I need to choose the correct functor
}
private:
std::tuple<Functors...> m_funcs;
};
template<typename ...Functors>
ResultFunctor<Functors...> join(Functors&&... funcs)
{
return ResultFunctor(std::move(funcs)...);
}
}
如果只针对 void
return 类型的仿函数,我很容易得到想要的元组元素。但是似乎没有办法确定它, return 类型不能从给定的参数中推导出来(很明显)。
另一个想法是使用一些 SFINAE 技巧来选择正确的 operator()()
版本,但是这样或那样我将不得不 "run through" 所有元组项(这很讨厌,但仍然可以用谷歌搜索),然后根据给定的参数包检查该项目是否合适。
嗯,这就是我停下来仔细考虑的地方。如果有人(谁能更好地处理所有可变参数)有任何想法,我将不胜感激。
namespace custom {
template<class...Fs>
struct overloaded : Fs... {
using Fs::operator()...;
};
template<class...Fs>
overloaded(Fs...)->overloaded<Fs...>;
template<class F>
F&& as_obj( F&& f ){ return std::forward<F>(f); }
template<class R, class...Args>
auto as_obj( R(*f)(Args...) {
struct helper {
R(*f)(Args...);
R operator()(Args...args) const { return f(std::forward<Args>(args)...); }
};
return helper{f};
}
template<class...Fs>
auto join( Fs&&...fs ){
return overloaded{as_obj(std::forward<Fs>(fs))...};
}
}
我添加了对非重载函数指针的额外支持。
这是一个非常简单的解决方案,不涉及 SFINAE 或模板元编程(只是常规模板)。
第一步是编写一个代表联合重载集的仿函数。这很容易通过继承实现,并且因为所有用作输入的仿函数 必须具有不同的类型 ,我们不必做任何花哨的事情。
// This represents overload set
template<class F1, class F2>
struct Joint : public F1, public F2 {
using F1::operator();
using F2::operator();
};
为了方便用户,我们可以添加推导指南:
template<class F1, class F2>
Joint(F1, F2) -> Joint<F1, F2>;
因为Joint
在C++17及以上是聚合类型,我们不用提供构造函数,因为我们可以使用聚合初始化:
// This code magically works
auto result = std::visit(Joint{
[](std::string const& s) { return "it's a string"; },
[](std::pair<int, int> const& p) { return "it's a pair"; }
}, var);
写一个custom::join
函数同样简单:
template<class F1, class F2>
auto join(F1&& f1, F2&& f2) {
return Joint { std::forward<F1>(f1), std::forward<F2>(f2) };
}
现在我们有了基本情况,我们可以很容易地概括它:
template<class F, class F2, class... Fs>
auto join(F&& f, F2&& f2, Fs&&... fs) {
return Joint{
std::forward<F>(f),
join(std::forward<F2>(f2), std::forward<Fs>(fs)...)
};
}
解决潜在的批评
- 为什么不为 Joint 定义构造函数? 聚合初始化是最有效的初始化形式,因为当您不定义构造函数时,编译器能够在-放置而无需复制或移动它们。
- 为什么要使用多重继承? 如果我们依赖 SFINAE,那会增加编译时间,增加代码复杂性,并且在某些情况下它不能按预期工作。使用 SFINAE,您必须检查重载集的每个成员以查看它是否合适。在某些情况下,由于隐式转换,会选择更差的重载,因为它是匹配项。
通过使用继承,我们可以使用语言的内置模式匹配来进行函数调用。
- 为什么要添加推导指南?它们使代码更清晰,在这种情况下它们完全按预期工作:参数按值存储
我的同事给了我一个"small quiz",他让他的学生解决了一次。看来我的弱智无法理解现代C++功能的所有美妙之处。
主题:
实现一个 join
函数,接受任意仿函数并 returning 另一个仿函数,它的行为与它们中的任何一个一样。例如:
{
auto result = std::visit(custom::join(
[](std::string const& s) { return "it's a string"; },
[](std::pair<int, int> const& p) { return "it's a pair"; }
), var);
assert(result == "it's a string");
var = std::make_pair(10, 20);
auto lvalue_lambda = [](std::string const& s) { return "it's a string"; };
result = std::visit(custom::join(
lvalue_lambda,
[](std::pair<int, int> const& p) { return "it's a pair"; }
), var);
assert(result == "it's a pair");
}
好吧,经过一番思考我明白了,就 "type-safe union" 而言,std::variant
意味着 "one of the listed",所以我需要一个元组。试过类似的东西:
namespace custom
{
template<typename ...Functors>
class ResultFunctor
{
public:
ResultFunctor(Functors&&... funcs)
: m_funcs(std::make_tuple(std::move(funcs)...))
{}
template<typename ...Params>
auto operator()(Params... params) // that's where I got stuck
{
// return std::get<void(Params...)>(m_funcs)(params...); // No, the return type spoils this idea
return std::get<0>(m_funcs)(params...); // Now I need to choose the correct functor
}
private:
std::tuple<Functors...> m_funcs;
};
template<typename ...Functors>
ResultFunctor<Functors...> join(Functors&&... funcs)
{
return ResultFunctor(std::move(funcs)...);
}
}
如果只针对 void
return 类型的仿函数,我很容易得到想要的元组元素。但是似乎没有办法确定它, return 类型不能从给定的参数中推导出来(很明显)。
另一个想法是使用一些 SFINAE 技巧来选择正确的 operator()()
版本,但是这样或那样我将不得不 "run through" 所有元组项(这很讨厌,但仍然可以用谷歌搜索),然后根据给定的参数包检查该项目是否合适。
嗯,这就是我停下来仔细考虑的地方。如果有人(谁能更好地处理所有可变参数)有任何想法,我将不胜感激。
namespace custom {
template<class...Fs>
struct overloaded : Fs... {
using Fs::operator()...;
};
template<class...Fs>
overloaded(Fs...)->overloaded<Fs...>;
template<class F>
F&& as_obj( F&& f ){ return std::forward<F>(f); }
template<class R, class...Args>
auto as_obj( R(*f)(Args...) {
struct helper {
R(*f)(Args...);
R operator()(Args...args) const { return f(std::forward<Args>(args)...); }
};
return helper{f};
}
template<class...Fs>
auto join( Fs&&...fs ){
return overloaded{as_obj(std::forward<Fs>(fs))...};
}
}
我添加了对非重载函数指针的额外支持。
这是一个非常简单的解决方案,不涉及 SFINAE 或模板元编程(只是常规模板)。
第一步是编写一个代表联合重载集的仿函数。这很容易通过继承实现,并且因为所有用作输入的仿函数 必须具有不同的类型 ,我们不必做任何花哨的事情。
// This represents overload set
template<class F1, class F2>
struct Joint : public F1, public F2 {
using F1::operator();
using F2::operator();
};
为了方便用户,我们可以添加推导指南:
template<class F1, class F2>
Joint(F1, F2) -> Joint<F1, F2>;
因为Joint
在C++17及以上是聚合类型,我们不用提供构造函数,因为我们可以使用聚合初始化:
// This code magically works
auto result = std::visit(Joint{
[](std::string const& s) { return "it's a string"; },
[](std::pair<int, int> const& p) { return "it's a pair"; }
}, var);
写一个custom::join
函数同样简单:
template<class F1, class F2>
auto join(F1&& f1, F2&& f2) {
return Joint { std::forward<F1>(f1), std::forward<F2>(f2) };
}
现在我们有了基本情况,我们可以很容易地概括它:
template<class F, class F2, class... Fs>
auto join(F&& f, F2&& f2, Fs&&... fs) {
return Joint{
std::forward<F>(f),
join(std::forward<F2>(f2), std::forward<Fs>(fs)...)
};
}
解决潜在的批评
- 为什么不为 Joint 定义构造函数? 聚合初始化是最有效的初始化形式,因为当您不定义构造函数时,编译器能够在-放置而无需复制或移动它们。
- 为什么要使用多重继承? 如果我们依赖 SFINAE,那会增加编译时间,增加代码复杂性,并且在某些情况下它不能按预期工作。使用 SFINAE,您必须检查重载集的每个成员以查看它是否合适。在某些情况下,由于隐式转换,会选择更差的重载,因为它是匹配项。 通过使用继承,我们可以使用语言的内置模式匹配来进行函数调用。
- 为什么要添加推导指南?它们使代码更清晰,在这种情况下它们完全按预期工作:参数按值存储