C++ 如何用一个二元函数模板处理四种引用组合?
数据小陈
结论
将两个参数声明为转发引用 TLeft&& 和 TRight&&,再分别通过 std::forward<TLeft> 和 std::forward<TRight> 转发。这样,foo 模板就能保留两个实参各自的值类别,并调用合适的 bar 重载。
#include <utility>
template <typename TLeft, typename TRight>
void foo(TLeft&& a, TRight&& b)
{
bar(std::forward<TLeft>(a));
bar(std::forward<TRight>(b));
}
TLeft&& 和 TRight&& 是转发引用。函数调用推导类型参数时,模板会根据实参是左值还是右值来推导类型,再通过引用折叠确定最终的参数类型。
四种参数组合
| 实参 | 推导出的模板参数 | 转发后的表达式 | bar 重载 |
|---|---|---|---|
非 const 左值 StringList | T = StringList& | StringList& 左值 | bar(const StringList&) |
const 左值 StringList | T = const StringList& | const StringList& 左值 | bar(const StringList&) |
右值 StringList | T = StringList | StringList&& 右值 | bar(StringList&&) |
foo 会分别推导两个参数的类型,因此每个参数都会按自己的值类别转发。下面四种调用都可以:
StringList l1;
StringList l2;
foo(l1, l2);
foo(l1, StringList{{"A", "B"}});
foo(StringList{{"A", "B"}}, l2);
foo(StringList{{"A", "B"}}, StringList{{"C", "D"}});
为什么按值接收和类型比较不行
原模板按值接收参数,声明为 TLeft a。这种推导会丢掉实参的引用属性,因此传入 StringList{} 时,TLeft 会被推导为 StringList,而不是 StringList&&。按值接收还会拷贝左值实参。
改用转发引用后,也不应通过 std::is_same_v<TLeft, StringList&&> 判断参数类别。转发引用推导出的 TLeft 通常是 StringList、StringList& 或 const StringList&。右值属性体现在 TLeft&& 经引用折叠后得到的参数类型中。std::forward<TLeft>(a) 会根据推导结果恢复原来的值类别。
使用时注意
- 使用
std::forward时需要包含<utility>。 - 每个参数只转发一次。如果
bar(StringList&&)内部会移动对象,调用者之后应避免继续依赖已被移动对象的内容。 - 这个模板也会尝试接受其他可以传给
bar的类型。如果foo只应接受StringList,可以在 C++20 中使用std::same_as和std::remove_cvref_t为两个模板参数添加约束。相应地,还需要包含<concepts>和<type_traits>。
备注:内容仅供参考。
