Search code examples
c++pass-by-valueforwarding-referencepass-by-rvalue-reference

Pass by value or universal reference


I want to develop a small polymorphic class with type erasure and I wonder which version of the templatized constructor is better and should be used.

We can pass by value:

class A
{
    ...
    template< typename T >
    A( T t ) { /* create the underlying model via std::move */ }
    ...
};

or we can use a universal reference:

class A
{
    ...
    template< typename T >
    A( T &&t ) { /* create the underlying model via std::forward */ }
    ...
};

(The universal reference has to be enabled if for the case that T is not the class itself and the class is not copied). Any ideas? Both version look equal to me.


Solution

  • These are not equivalent and sometimes one is desired over the the other one. A stellar talk by Nicolai Josuttis is an hour worth of talking just about this. I highly recommend watching it at least once.

    Personally, unless you encounter a special case, where conversions are expensive and you want to avoid temporaries as much as possible, I would suggest just passing by value and std::moveing the argument.

    Case where T&& is more efficient:

    struct foo {
        std::string bar;
    
        template <typename T>
        foo(T&& t)
            :bar(std::forward<T>(t)){}
    };
    

    versus:

    struct foo {
        std::string bar;
    
        foo(std::string t)
            :bar(std::move(t)){}
    };
    

    when you do:

    int main() {
        foo f("some char*");
    }
    

    In the first case (perfect forwarding) you simply construct a std::string with const char* argument. In the second case, you construct one temporary (t from "some char*") and one empty std::string object, then you apply one move operation. It's not the end of the world, but the first version is simply more efficient.

    To be absolutely clear about performance:

    • The first version uses 1 allocation

    • the second version uses 1 allocation and 1 move

    and by move I don't mean std::move, since it generates no code (it's just a cast). By move I mean the code that needs to be execuded that actually moves from the string, which is a part of std::string(std::string&&).

    Once again - the above example was based on the talk I linked at the beginning of the answer. It's really worth watching.