考虑以下构建存储函数的类的代码。
// Function list class
template <class... F>
struct function_list
{
template <class... G>
constexpr function_list(G&&... g) noexcept
: _f{std::forward<G>(g)...}
{
}
std::tuple</* F... OR F&&... */> _f;
};
// Function list maker
template <class... F, class R = /* Can we compute the return type here? */>
constexpr R make_function_list(F&&... f)
{
return function_list<
/* decltype(std::forward<F>(f))...
* OR F...
* OR F&&...
*/>(std::forward<F>(f)...);
}
我希望完美地转发这些函数(无论它们是函数指针,函子,lambdas ......)。但我并不完全理解std::forward
背后发生的所有类型推论和通用引用。在上面的代码中,我有三个问题:
_f
应该是std::tuple<F...>
还是std::tuple<F&&...>
类型(以及为什么?)R
(因为手动而不是auto/decltype(auto)
执行此操作将有助于了解正在发生的事情)function_list
模板参数应该是什么:decltype(std::forward<F>(f)...)
,F
或F&&...
(以及为什么?)注意:function_list
的构造函数不应该直接调用,而make_function_list
正在执行此工作。
编辑:
如果operator()
function_list
(此处未显示)不能保证在同一声明中被调用,这种情况是否安全?
template <class... F>
constexpr function_list<F...> make_function_list(F&&... f)
{
return function_list<F&&...>(std::forward<F>(f)...);
}
答案 0 :(得分:3)
但我并不完全理解
std::forward
背后发生的所有类型推论和通用引用。
通过一个例子来理解它很简单。
template <typename T>
void f(T&&)
{
std::tuple<T>{}; // (0)
std::tuple<T&&>{}; // (1)
}
如果是(0):
T
被推断为T
T
推断为左轮眼的T&
。在(1):
的情况下T
被推断为T&&
T
推断为左轮眼的T&
。如您所见,两者之间的唯一区别是如何推断 rvalues 。
关于std::forward
,这就是它的作用:
template <typename T>
void g(T&&);
template <typename T>
void f(T&& x)
{
g(x) // (0)
g(std::forward<T>(x)); // (1)
}
如果是(0):
x
始终是左值。在(1):
的情况下x
被推断为T&&
,则 T
会投放到T
。
x
否则会保留左值。
std::forward
通过查看推断x
的方式,基本上保留了T
的类型类别。
_f应该是
std::tuple<F...>
类型还是std::tuple<F&&...>
我认为在您的情况下,它应该是std::tuple<F...>
,因为您要存储左值引用或值。
std::tuple<F&&...>
会存储左值引用或右值引用 - 这会导致在临时情况下悬空引用。
是否可以推导出模板参数列表中的返回类型
R
是的,它只是function_list<F...>
。
template <class... F, class R = function_list<F...>>
constexpr R make_function_list(F&&... f)
{
return function_list<F...>(std::forward<F>(f)...);
}
您甚至不需要R
模板参数。
template <class... F>
constexpr function_list<F...> make_function_list(F&&... f)
{
return function_list<F...>(std::forward<F>(f)...);
}
在制作者中,
function_list
模板参数应为:decltype(std::forward<F>(f)...)
,F
或F&&...
function_list
应将F...
作为模板参数,原因在于本答案开头所列的原因(即避免悬挂对临时工具的引用)。
它仍然需要std::forward<F>(f)...
作为允许 rvalues 转发的参数(即将rvalues移动到function_list
的元组中)
答案 1 :(得分:2)
如果它们是F&&
,那么如果您将临时值传递给make_function_list
,则返回的包含tuple
的类将存储对传递给make_function_list
的临时值的右值引用
在下一行,它现在是悬挂参考。
在大多数用例中,这似乎很糟糕。这在所有用例中实际上并不是很糟糕; forward_as_tuple
这样做。但是这样的用例是不是一般用例。这种模式非常脆弱和危险。
通常,如果您要返回T&&
,则希望将其作为T
返回。这可能会导致对象的副本;但替代方案是danging-reference-hell。
这给了我们:
template<class... Fs>
struct function_list {
template<class... Gs>
explicit constexpr function_list(Gs&&... gs) noexcept
: fs(std::forward<Gs>(gs)...)
{}
std::tuple<Fs...> fs;
};
template<class... Fs, class R = function_list<Fs...>>
constexpr R make_function_list(Fs&&... fs) {
return R(std::forward<Fs>(fs)...);
}
同时使function_list
的ctor explicit
,因为在1参数的情况下,它转换为一个相当贪婪的隐式转换构造函数。这可以修复,但需要付出更多的努力。
operator()
需要一个实例。类型名称不是实例。
答案 2 :(得分:0)
这取决于function_list
的用途。基本上有两种情况:
function_list
是一个永远不会比它出现的语句更长的临时助手。在这里,我们可以存储对函数的引用,并将它们完美地转发到调用点:
template <class... F>
struct function_list
{
std::tuple<F&&...> f_;
// no need to make this template
constexpr function_list(F&&... f) noexcept
: f_{std::forward<F>(f)...}
{}
template <std::size_t i, typename... A>
decltype(auto) call_at(A&&... a)
{
return std::invoke(std::get<i>(f_), std::forward<A>(a)...);
}
};
function_list
是一个类似于std::bind
的包装器/容器对象,在这种情况下,您希望存储函数的衰减副本以避免悬空引用和完美转发这个上下文意味着将函数转发给f_
中其衰变版本的构造函数,然后在调用时使用function_list
本身的值类别填充衰减函数:
template <class... F>
struct function_list
{
std::tuple<std::decay_t<F>...> f_;
template <typename... G>
constexpr function_list(G&&... g)
: f_{std::forward<G>(g)...}
{}
template <std::size_t i, typename... A>
decltype(auto) call_at(A&&... a) &
{
return std::invoke(std::get<i>(f_), std::forward<A>(a)...);
}
template <std::size_t i, typename... A>
decltype(auto) call_at(A&&... a) const&
{
return std::invoke(std::get<i>(f_), std::forward<A>(a)...);
}
template <std::size_t i, typename... A>
decltype(auto) call_at(A&&... a) &&
{
return std::invoke(std::get<i>(std::move(f_)), std::forward<A>(a)...);
}
template <std::size_t i, typename... A>
decltype(auto) call_at(A&&... a) const&&
{
return std::invoke(std::get<i>(std::move(f_)), std::forward<A>(a)...);
}
};
与std::bind
一样,如果您确实要存储引用,则必须使用std::reference_wrapper
明确地执行此操作。
两种情况下的构造都是相同的:
template <class... F>
constexpr auto make_function_list(F&&... f)
{
return function_list<F...>(std::forward<F>(f)...);
}