选择性转发功能
Selective forwarding function
任务是创建一个单参数函数,它转发除一个类型 (Foo) 之外的所有类型,并将其转换(到 Bar)。
(假设存在从 Foo 到 Bar 的转换)。
使用场景如下:
template<typename Args...>
void f( Args... args )
{
g( process<Args>(args)... );
}
(我已经尝试从原始上下文中extract/simplify它。--如果我犯了错误,请有人告诉我!)
这里有两种可能的实现方式:
template<typename T>
T&& process(T&& t) {
return std::forward<T>(t);
}
Bar process(Foo x) {
return Bar{x};
}
还有...
template <typename T, typename U>
T&& process(U&& u) {
return std::forward<T>(std::forward<U>(u));
}
template <typename T>
Bar process(Foo x) {
return Bar{x};
}
据我所知 (here) 第二种更可取。
但是,我无法理解给定的解释。我认为这是在深入研究 C++ 的一些最黑暗的角落。
我想我缺少理解正在发生的事情所必需的机制。有人可以详细解释一下吗?如果挖掘太多,谁能推荐学习必要先决条件概念的资源?
编辑:我想补充一点,在我的特定情况下,函数签名将匹配 this page 上的 typedef-s 之一。也就是说,每个参数要么是 PyObject*
(PyObject
是一个普通的 C 结构),要么是一些基本的 C 类型,如 const char*
、int
、float
。所以我的猜测是轻量级实现可能是最合适的(我不喜欢过度概括)。但我真的很想获得正确的心态来解决这些问题。
版本 2 的第一部分还不够吗?只有:
template <typename T, typename U>
T&& process(U&& u) {
return std::forward<T>(std::forward<U>(u));
}
给定一个具有现有转换的用例(来自 "Foo" 的 "Bar" 的构造函数),例如:
struct Foo {
int x;
};
struct Bar {
int y;
Bar(Foo f) {
y = f.x;
}
};
int main() {
auto b = process<Bar>(Foo()); // b will become a "Bar"
auto i = process<int>(1.5f);
}
无论如何,您都必须指定第一个模板参数(要转换为的类型),因为编译器无法推断出它。所以它知道你期望什么类型,并且会构造一个 "Bar" 类型的临时对象,因为有一个构造函数。
我感觉到您对所面临的用例的理解存在细微的误解。
首先,这是一个函数模板:
struct A
{
template <typename... Args>
void f(Args... args)
{
}
};
这不是函数模板:
template <typename... Args>
struct A
{
void f(Args... args)
{
}
};
在前面的定义(使用函数模板)中,参数类型推导发生了。在后者中,没有类型推导。
您没有使用函数模板。您正在使用来自 class 模板的非模板成员函数,并且对于这个特定的成员函数,其签名是固定的。
通过如下定义 trap
class:
template <typename T, T t>
struct trap;
template <typename R, typename... Args, R(Base::*t)(Args...)>
struct trap<R(Base::*)(Args...), t>
{
static R call(Args... args);
};
并引用其成员函数如下:
&trap<decltype(&Base::target), &Base::target>::call;
你最终得到一个指向静态非模板 call
函数的指针,它具有固定的签名,与 target
函数的签名相同。
现在,call
函数用作中间调用程序。您将调用 call
函数,该函数将调用 target
成员函数,传递它自己的参数来初始化 target
的参数,比如:
template <typename R, typename... Args, R(Base::*t)(Args...)>
struct trap<R(Base::*)(Args...), t>
{
static R call(Args... args)
{
return (get_base()->*t)(args...);
}
};
假设用来实例化trap
class模板的target
函数定义如下:
struct Base
{
int target(Noisy& a, Noisy b);
};
通过实例化 trap
class 您最终得到以下 call
函数:
// what the compiler *sees*
static int call(Noisy& a, Noisy b)
{
return get_base()->target(a, b);
}
幸运的是,a
通过引用 传递 ,它只是被 target
参数中的同类引用转发和绑定。不幸的是,这不适用于 b
对象 - 无论 Noisy
class 是否可移动,您都在制作 b
实例的多个副本,因为那个是 by value:
第一个:当call
函数被外部上下文调用时。
第二个:从call
.
[=173=函数体中调用target
函数时复制b
实例]
这有点低效:如果你能把 b
实例变成 一个 xvalue,你至少可以保存一个复制构造函数调用,把它变成一个移动构造函数调用:
static int call(Noisy& a, Noisy b)
{
return get_base()->target(a, std::move(b));
// ~~~~~~~~~~~^
}
现在它将为第二个参数调用移动构造函数。
到目前为止一切顺利,但这是手动完成的(std::move
添加时知道应用移动语义是安全的)。现在,问题是,在 参数包 上运行时如何应用相同的功能?:
return get_base()->target(std::move(args)...); // WRONG!
您不能对参数包中的每个参数应用 std::move
调用。如果同样应用于所有参数,这可能会导致编译器错误。
幸运的是,即使 Args...
不是 forwarding-reference,也可以使用 std::forward
辅助函数。也就是说,根据 <T>
类型在 std::forward<T>
中的类型(左值引用或非左值引用),std::forward
的行为会有所不同:
对于左值引用(例如,如果 T
是 Noisy&
):表达式的值类别仍然是左值(即 Noisy&
)。
对于非左值引用(例如,如果 T
是 Noisy&&
或普通的 Noisy
):表达式的值类别变为 xvalue (即 Noisy&&
).
话虽如此,通过如下定义 target
函数:
static R call(Args... args)
{
return (get_base()->*t)(std::forward<Args>(args)...);
}
你最终得到:
static int call(Noisy& a, Noisy b)
{
// what the compiler *sees*
return get_base()->target(std::forward<Noisy&>(a), std::forward<Noisy>(b));
}
将涉及b
的表达式的值类别转换为b
的xvalue,即Noisy&&
。这让编译器选择移动构造函数来初始化 target
函数的第二个参数,而 a
保持不变。
DEMO 3 (与DEMO 1比较输出)
基本上,这就是 std::forward
的用途。通常,std::forward
与 forwarding-reference 一起使用,其中 T
保存根据转发引用的类型推导规则推导的类型。请注意,它总是要求您显式传递 <T>
部分,因为它会根据该类型应用不同的行为(不取决于其参数的值类别)。如果没有显式类型模板参数 <T>
,std::forward
将始终为通过名称引用的参数推导左值引用(就像扩展参数包时一样)。
现在,您想 另外 将一些参数从一种类型转换为另一种类型,同时转发所有其他参数。如果你不关心参数包中 std::forward
ing 参数的技巧,并且总是调用复制构造函数很好,那么你的版本 就可以了:
template <typename T> // transparent function
T&& process(T&& t) {
return std::forward<T>(t);
}
Bar process(Foo x) { // overload for specific type of arguments
return Bar{x};
}
//...
get_base()->target(process(args)...);
但是,如果您想避免在演示中复制 Noisy
参数,您需要以某种方式将 std::forward
调用与 process
调用结合起来, 和 传递 Args
类型,以便 std::forward
可以应用适当的行为(变成 xvalues 或不做任何事情)。我只是给你一个简单的例子来说明如何实现:
template <typename T, typename U>
T&& process(U&& u) {
return std::forward<T>(std::forward<U>(u));
}
template <typename T>
Bar process(Foo x) {
return Bar{x};
}
//...
get_base()->target(process<Args>(args)...);
但这只是选择之一。可以对其进行简化、重写或重新排序,以便在调用 process
函数(您的版本)之前调用 std::forward
:
get_base()->target(process(std::forward<Args>(args))...);
DEMO 5 (与DEMO 4比较输出)
而且它也可以正常工作(即,与您的版本一起使用)。所以关键是,额外的 std::forward
只是为了稍微优化你的代码,而 provided idea 只是该功能的一种可能实现(如你所见,它带来了相同的效果).
任务是创建一个单参数函数,它转发除一个类型 (Foo) 之外的所有类型,并将其转换(到 Bar)。
(假设存在从 Foo 到 Bar 的转换)。
使用场景如下:
template<typename Args...>
void f( Args... args )
{
g( process<Args>(args)... );
}
(我已经尝试从原始上下文
这里有两种可能的实现方式:
template<typename T>
T&& process(T&& t) {
return std::forward<T>(t);
}
Bar process(Foo x) {
return Bar{x};
}
还有...
template <typename T, typename U>
T&& process(U&& u) {
return std::forward<T>(std::forward<U>(u));
}
template <typename T>
Bar process(Foo x) {
return Bar{x};
}
据我所知 (here) 第二种更可取。
但是,我无法理解给定的解释。我认为这是在深入研究 C++ 的一些最黑暗的角落。
我想我缺少理解正在发生的事情所必需的机制。有人可以详细解释一下吗?如果挖掘太多,谁能推荐学习必要先决条件概念的资源?
编辑:我想补充一点,在我的特定情况下,函数签名将匹配 this page 上的 typedef-s 之一。也就是说,每个参数要么是 PyObject*
(PyObject
是一个普通的 C 结构),要么是一些基本的 C 类型,如 const char*
、int
、float
。所以我的猜测是轻量级实现可能是最合适的(我不喜欢过度概括)。但我真的很想获得正确的心态来解决这些问题。
版本 2 的第一部分还不够吗?只有:
template <typename T, typename U>
T&& process(U&& u) {
return std::forward<T>(std::forward<U>(u));
}
给定一个具有现有转换的用例(来自 "Foo" 的 "Bar" 的构造函数),例如:
struct Foo {
int x;
};
struct Bar {
int y;
Bar(Foo f) {
y = f.x;
}
};
int main() {
auto b = process<Bar>(Foo()); // b will become a "Bar"
auto i = process<int>(1.5f);
}
无论如何,您都必须指定第一个模板参数(要转换为的类型),因为编译器无法推断出它。所以它知道你期望什么类型,并且会构造一个 "Bar" 类型的临时对象,因为有一个构造函数。
我感觉到您对所面临的用例的理解存在细微的误解。
首先,这是一个函数模板:
struct A
{
template <typename... Args>
void f(Args... args)
{
}
};
这不是函数模板:
template <typename... Args>
struct A
{
void f(Args... args)
{
}
};
在前面的定义(使用函数模板)中,参数类型推导发生了。在后者中,没有类型推导。
您没有使用函数模板。您正在使用来自 class 模板的非模板成员函数,并且对于这个特定的成员函数,其签名是固定的。
通过如下定义 trap
class:
template <typename T, T t>
struct trap;
template <typename R, typename... Args, R(Base::*t)(Args...)>
struct trap<R(Base::*)(Args...), t>
{
static R call(Args... args);
};
并引用其成员函数如下:
&trap<decltype(&Base::target), &Base::target>::call;
你最终得到一个指向静态非模板 call
函数的指针,它具有固定的签名,与 target
函数的签名相同。
现在,call
函数用作中间调用程序。您将调用 call
函数,该函数将调用 target
成员函数,传递它自己的参数来初始化 target
的参数,比如:
template <typename R, typename... Args, R(Base::*t)(Args...)>
struct trap<R(Base::*)(Args...), t>
{
static R call(Args... args)
{
return (get_base()->*t)(args...);
}
};
假设用来实例化trap
class模板的target
函数定义如下:
struct Base
{
int target(Noisy& a, Noisy b);
};
通过实例化 trap
class 您最终得到以下 call
函数:
// what the compiler *sees*
static int call(Noisy& a, Noisy b)
{
return get_base()->target(a, b);
}
幸运的是,a
通过引用 传递 ,它只是被 target
参数中的同类引用转发和绑定。不幸的是,这不适用于 b
对象 - 无论 Noisy
class 是否可移动,您都在制作 b
实例的多个副本,因为那个是 by value:
第一个:当
call
函数被外部上下文调用时。第二个:从
[=173=函数体中调用call
.target
函数时复制b
实例]
这有点低效:如果你能把 b
实例变成 一个 xvalue,你至少可以保存一个复制构造函数调用,把它变成一个移动构造函数调用:
static int call(Noisy& a, Noisy b)
{
return get_base()->target(a, std::move(b));
// ~~~~~~~~~~~^
}
现在它将为第二个参数调用移动构造函数。
到目前为止一切顺利,但这是手动完成的(std::move
添加时知道应用移动语义是安全的)。现在,问题是,在 参数包 上运行时如何应用相同的功能?:
return get_base()->target(std::move(args)...); // WRONG!
您不能对参数包中的每个参数应用 std::move
调用。如果同样应用于所有参数,这可能会导致编译器错误。
幸运的是,即使 Args...
不是 forwarding-reference,也可以使用 std::forward
辅助函数。也就是说,根据 <T>
类型在 std::forward<T>
中的类型(左值引用或非左值引用),std::forward
的行为会有所不同:
对于左值引用(例如,如果
T
是Noisy&
):表达式的值类别仍然是左值(即Noisy&
)。对于非左值引用(例如,如果
T
是Noisy&&
或普通的Noisy
):表达式的值类别变为 xvalue (即Noisy&&
).
话虽如此,通过如下定义 target
函数:
static R call(Args... args)
{
return (get_base()->*t)(std::forward<Args>(args)...);
}
你最终得到:
static int call(Noisy& a, Noisy b)
{
// what the compiler *sees*
return get_base()->target(std::forward<Noisy&>(a), std::forward<Noisy>(b));
}
将涉及b
的表达式的值类别转换为b
的xvalue,即Noisy&&
。这让编译器选择移动构造函数来初始化 target
函数的第二个参数,而 a
保持不变。
DEMO 3 (与DEMO 1比较输出)
基本上,这就是 std::forward
的用途。通常,std::forward
与 forwarding-reference 一起使用,其中 T
保存根据转发引用的类型推导规则推导的类型。请注意,它总是要求您显式传递 <T>
部分,因为它会根据该类型应用不同的行为(不取决于其参数的值类别)。如果没有显式类型模板参数 <T>
,std::forward
将始终为通过名称引用的参数推导左值引用(就像扩展参数包时一样)。
现在,您想 另外 将一些参数从一种类型转换为另一种类型,同时转发所有其他参数。如果你不关心参数包中 std::forward
ing 参数的技巧,并且总是调用复制构造函数很好,那么你的版本 就可以了:
template <typename T> // transparent function
T&& process(T&& t) {
return std::forward<T>(t);
}
Bar process(Foo x) { // overload for specific type of arguments
return Bar{x};
}
//...
get_base()->target(process(args)...);
但是,如果您想避免在演示中复制 Noisy
参数,您需要以某种方式将 std::forward
调用与 process
调用结合起来, 和 传递 Args
类型,以便 std::forward
可以应用适当的行为(变成 xvalues 或不做任何事情)。我只是给你一个简单的例子来说明如何实现:
template <typename T, typename U>
T&& process(U&& u) {
return std::forward<T>(std::forward<U>(u));
}
template <typename T>
Bar process(Foo x) {
return Bar{x};
}
//...
get_base()->target(process<Args>(args)...);
但这只是选择之一。可以对其进行简化、重写或重新排序,以便在调用 process
函数(您的版本)之前调用 std::forward
:
get_base()->target(process(std::forward<Args>(args))...);
DEMO 5 (与DEMO 4比较输出)
而且它也可以正常工作(即,与您的版本一起使用)。所以关键是,额外的 std::forward
只是为了稍微优化你的代码,而 provided idea 只是该功能的一种可能实现(如你所见,它带来了相同的效果).