Home > Back-end >  C passing by const ref vs universal ref
C passing by const ref vs universal ref

Time:01-13

So I've recently learned about universal references and reference collapsing.

So let's say I have two different implementations of a max function like such.

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;
}

One version takes its arguments by const reference and other takes them by universal reference.

What confuses me about these is when should either be used. Passing by const is mainly used so you can bind temporaries to it.

In the stl there's different uses as well. std::move takes universal reference where std::max takes by const ref.

What's the situations on where either should be used??

Say if I wanted to avoid the copy when it returns or keep a reference on return. Would it makes sense to have a const and nonconst version of the function etc

CodePudding user response:

The first one should probably be:

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

Otherwise it wouldn't work for temporaries (the original first option fails with two temporaries).

But even now, with the new version above, it cannot work for a mix of an rvalue and an lvalue:

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

The pitfall with the second, the const-lvalue-ref version, is that if a temporary is sent to it, we return a const-lvalue-ref to a temporary which is bug prone, if you copy it immediately you are fine, if you take it by-ref you are in the UB zone:

// 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

To solve the above problem, with the cost of redundant copying, we can have another option:

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

The language itself took the 2nd option for std::max, i.e. getting and returning const-ref. And the user shall be careful enough not to take a reference to temporaries.


Another option might be to support both rvalue and lvalue in their own semantic, with two overloaded functions:

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;
}

With this approach, you can always get the result as a const-ref: if you went to the first one you get back a value and extend its lifetime, if you went to the second one you bind a const-ref to a const-ref:

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

The problem with the above suggestion is that it only takes you to the lvalue-ref version if both arguments are const lvalue-ref. If you want to cover all cases you will have to actually cover them all, as in the code below:

// 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;
}

A last approach to achieve the overloading, and supporting all kind of lvalue-ref (const and non-const, including a mixture), without the need for implementing the 4 combinations for lvalue-ref, would be based on SFINAE (here presented with C 20 with a constraint, using 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_cv<T1>, std::remove_cv<T2>>
auto& my_max(T1&& a, T2&& b) {
    return (a < b) ? b : a;
}
  •  Tags:  
  • Related