PAYATHON 2026

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 左值 StringListT = StringList&StringList& 左值bar(const StringList&)
const 左值 StringListT = const StringList&const StringList& 左值bar(const StringList&)
右值 StringListT = StringListStringList&& 右值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>。

备注:内容仅供参考。