Sharing implementations for const lvalue (const T&) and rvalue (T&&) overloads: just like what is done for const and non-const overloads

932 Views Asked by At

Background

The following code block appears in Scott Meyers' famous book "Effective C++" Item 3:

class TextBlock {
public:
    ...
    const char& operator[](std::size_t position) const
    {
        ...    // do bounds checking
        ...    // log access data
        ...    // verify data integrity
        return text[position];
    }
    char& operator[](std::size_t position)
    {
        ...    // do bounds checking
        ...    // log access data
        ...    // verify data integrity
        return text[position];
    }
    ...
private:
    std::string text;
};

The author states that in the above implementation, the contents of the const and the non-const overloads are essentially the same. In order to avoid code duplication, it could be simplified like this:

class TextBlock {
public:
    ...
    const char& operator[](std::size_t position) const    // the same as before
    {
        ...
        ...
        ...
        return text[position];
    }
    char& operator[](std::size_t position)        // now just calls const op[]
    {
        return                                    // cast away const on
          const_cast<char&>(                      // op[]'s return type;
            static_cast<const TextBlock&>(*this)  // add const to *this's type;
              [position]                          // call const version of op[]
          );
    }
    ...
private:
    std::string text;
};

Questions

My questions are:

  • When shall we need an overload for const T& and another for T&&? (Here, T may be a template parameter or a class type, so T&& may or may not mean a universal reference) I can see that in the standard library, many classes provide both overloads. Examples are constructors of std::pair and std::tuple, there are tons of overloads. (Okay, I know that among the functions, one of them is the copy constructor and one of them is the move constructor.)

  • Is there a similar trick to share the implementations for the const T& and T&& overloads? I mean, if the const T&& overload returns an object that is copy-constructed, and the T&& overload returns something that is move-constructed, after sharing the implementation, this property must still hold. (Just like the above trick: const returns const and non-const returns non-const, both before and after implementation sharing)

Thanks!

Clarifications

The two overloads I'm referring to should look like:

Gadget f(Widget const& w);
Gadget f(Widget&& w);

It is nothing related to returning by rvalue references, that is:

Widget&& g(/* ... */);

(By the way, that question was addressed in my previous post)

In f() above, if Gadget is both copy-constructible and move-constructible, there's no way (except from reading the implementation) to tell whether the return value is copy-constructed or move-constructed. It is nothing to deal with Return Value Optimization (RVO) / Named Return Value Optimization (NRVO). (See my previous post)

References

Effective C++

std::pair::pair

std::tuple::tuple

When is it a good time to return by rvalue references?

1

There are 1 best solutions below

5
On

• When shall we need an overload for const T& and another for T&&?

Basically, when moving gives you a performance gain, there should be also a move constructor. The same holds for functions in which you'd otherwise needed an expensive copy.

In your example, where you return a reference to a char, it is however not advisable to also set up a function which returns an rvalue reference. Rather, return by value and rely on the compiler's ability to apply RVO (see e.g. here)

•Is there a similar trick to share the implementations for the const T& and T&& overloads?

I often found it useful to set up a constructor or function using a universal reference (I'm lazy), i.e. something like

struct MyClass
{
    template<typename T /*, here possibly use SFINAE to allow only for certain types */>
    MyClass(T&& _t) : t(std::forward<T>(_t)) {}
private:
    SomeType t;
};

EDIT: Regarding your update: if you have an expensive copy of Widget in your function f, it is adviceable also to provide an overload taking a Widget&&.

Gadget f(Widget const& w)
{
    Widget temp = w;  //expensive copy
}
Gadget f(Widget&& w)
{
    Widget temp = std::move(w);  //move
}

You could combine both function using a function template like this

template<typename WidgetType
       // possibly drop that SFINAE stuff
       // (as it is already checked in the first assignment)
       , typename std::enable_if<std::is_convertible<std::remove_reference_t<WidgetType>, Widget>::value> >
Gadget(WidgetType&& w)
{
    Widget temp = std::forward<WidgetType>(w);
    //or
    std::remove_reference_t<WidgetType> temp2 = std::forward<WidgetType>(w);
}

... I didn't say it's nicer ;-).


EDIT 2: See also this thread, which addresses your question much more thouroughly.