C++ 通过 const ref 与 universal ref 传递

Ant*_*oxo 16 c++

所以我最近了解了通用引用和引用折叠。

假设我有这样的 max 函数的两种不同实现。

template<class T>
T&& max(T&& a, T&& b)
{
    return (a < b) ? b : a;
}

template<class T>
const T& max(const T& a, const T& b)
{
    return (a < b) ? b : a;
}
Run Code Online (Sandbox Code Playgroud)

一种版本通过 const 引用获取参数,另一种版本通过通用引用获取参数。

让我困惑的是什么时候应该使用它们。主要使用 const 传递,以便您可以将临时变量绑定到它。

在 stl 中也有不同的用途。std::move 采用通用引用,而 std::max 采用 const ref。

什么情况下应该使用两者?

假设我想在返回时避免复制或在返回时保留引用。函数的 const 和非常量版本是否有意义?

Ami*_*rsh 12

第一个可能应该是:

// amended first version
template<class T>
decltype(auto) max(T&& a, T&& b) {
    return (a < b) ? std::forward<T>(b) : std::forward<T>(a);
}
Run Code Online (Sandbox Code Playgroud)

否则它不适用于临时对象(最初的第一个选项因两个临时对象而失败)。

但即使是现在,使用上面的新版本,它也无法适用于右值和左值的混合:

int i = my_max(3, 5); // ok
i = my_max(i, 15);    // fails
// no known conversion from 'int' to 'int &&' for 1st argument
Run Code Online (Sandbox Code Playgroud)

除此之外,第一个选项的返回值是左值引用或右值引用(带或不带 const,具体取决于参数)。将右值引用返回到临时值并不是最好的主意,如果我们立即从中复制它会起作用,但是最好按值返回,这不是第一个版本所采用的方法。

第二个 const-lvalue-ref 版本的陷阱是,如果向其发送临时值,我们将 const-lvalue-ref 返回到临时值,这很容易出现错误(好吧,不超过返回一个右值引用暂时的,但仍然)。如果你立即复制它,那就没问题,如果你通过引用获取它,你就在 UB 区域:

// second version
template<class T>
const T& max(const T& a, const T& b) {
    return (a < b) ? b : a;
}

std::string themax1 = max("hello"s, "world"s); // ok
const std::string& themax2 = max("hello"s, "world"s); // dangling ref
Run Code Online (Sandbox Code Playgroud)

为了解决上述问题,考虑到左值引用情况的冗余复制成本,我们可以有另一种选择,按值返回:

// third version
template<class T1, class T2>
auto max(T1&& a, T2&& b) {
    return (a < b) ? std::forward<T2>(b) : std::forward<T1>(a);
}
Run Code Online (Sandbox Code Playgroud)

该语言本身采用了std::max的第二个选项,即获取并返回 const-ref。用户应足够小心,不要引用临时变量。


另一种选择可能是在自己的语义中支持右值和左值,并使用两个重载函数

template<class T1, class T2>
auto my_max(T1&& a, T2&& b) {
    return (a < b) ? std::forward<T2>(b) : std::forward<T1>(a);
}

template<class T>
const T& my_max(const T& a, const T& b) {
    return (a < b) ? b : a;
}
Run Code Online (Sandbox Code Playgroud)

通过这种方法,您始终可以获得 const-ref 形式的结果:如果您转到第一个,您将返回一个值并延长其生命周期,如果您转到第二个,您将 const-ref 绑定到 const-参考:

int i = my_max(3, 5); // first, copying
const int& i2 = my_max(i, 25); // first, life time is extended
const std::string& s = my_max("hi"s, "hello"s); // first, life time is extended
const std::string s2 = my_max("hi"s, "hello"s); // first, copying
const std::string& s3 = my_max(s, s2); // second, actual ref, no life time extension
Run Code Online (Sandbox Code Playgroud)

上述建议的问题在于,如果两个参数都是const lvalue-ref,它只会将您带到 lvalue-ref 版本。如果您想涵盖所有情况,则必须实际涵盖所有情况,如下面的代码所示

// handle rvalue-refs
template<class T1, class T2>
auto my_max(T1&& a, T2&& b) {
    return (a < b) ? std::forward<T2>(b) : std::forward<T1>(a);
}

// handle all lvalue-ref combinations
template<class T>
const T& my_max(const T& a, const T& b) {
    return (a < b) ? b : a;
}

template<class T>
const T& my_max(T& a, const T& b) {
    return (a < b) ? b : a;
}

template<class T>
const T& my_max(const T& a, T& b) {
    return (a < b) ? b : a;
}

template<class T>
const T& my_max(T& a, T& b) {
    return (a < b) ? b : a;
}
Run Code Online (Sandbox Code Playgroud)

实现重载并支持所有类型的左值引用(const 和非 const,包括混合)的最后一种方法,而不需要实现左值引用的 4 种组合,将基于 SFINAE(此处介绍为C++20带有约束,使用requires):

template<class T1, class T2>
auto my_max(T1&& a, T2&& b) {
    return (a < b) ? std::forward<T2>(b) : std::forward<T1>(a);
}

template<class T1, class T2>
  requires std::is_lvalue_reference_v<T1> && 
           std::is_lvalue_reference_v<T2> &&
           std::is_same_v<std::remove_cvref_t<T1>, std::remove_cvref_t<T2>>
auto& my_max(T1&& a, T2&& b) {
    return (a < b) ? b : a;
}
Run Code Online (Sandbox Code Playgroud)

如果你能忍耐我最后一次……(希望你现在还没有陷入先生状态的困境)。我们可以在一个函数中实现这一切!并且作为一个附带好处,如果支持,甚至允许在派生和基类之间进行比较,因此以下内容可以正常工作:

A a = 1;
B b = 2; // B is derived from A
// if comparison between A and B is supported, you can do
const A& max1 = my_max(a, b); // const-ref to b
const A& max2 = my_max(a, B{-1}); // const-ref to a temporary copied from a
const A& max3 = my_max(a, B{3}); // const-ref to B{3}
Run Code Online (Sandbox Code Playgroud)

这将是使用单个函数支持此操作的代码:

template<typename T1, typename T2>
struct common_return {
    using type = std::common_reference_t<T1, T2>;
};

template<typename T1, typename T2>
    requires std::is_lvalue_reference_v<T1> &&
             std::is_lvalue_reference_v<T2> &&
             has_common_base<T1, T2> // see code in link below
struct common_return<T1, T2> {
    using type = const std::common_reference_t<T1, T2>&;
};

template<typename T1, typename T2>
using common_return_t = typename common_return<T1, T2>::type;

template<class T1, class T2> 
common_return_t<T1, T2> my_max(T1&& a, T2&& b)
{
    if(a < b) {
        return std::forward<T2>(b);
    }
    return std::forward<T1>(a);
}
Run Code Online (Sandbox Code Playgroud)

上述机械可以在这里找到。

对于这个的可变版本,你可以关注这个 SO post