1

I'm trying to understand the performance implications of using WidgetURef::setName (URef being a Universal Reference, the term coined by Scott Meyers) vs WidgedRRef::setName (RRef being an R-value Reference):

#include <string>

class WidgetURef {
    public:
    template<typename T>
    void setName(T&& newName)
    {
        name = std::move(newName);
    }
    private:
        std::string name;
};

class WidgetRRef {
    public:
    void setName(std::string&& newName)
    {
        name = std::move(newName);
    }
    private:
        std::string name;
};

int main() {
    WidgetURef w_uref;
    w_uref.setName("Adela Novak");

    WidgetRRef w_rref;
    w_rref.setName("Adela Novak");
}

I do appreciate that with universal references one should be using std::forward instead, but this is just an (imperfect) example to highlight the interesting bit.

Question In this particular example, what is the performance implications of using one implementation vs the other? Although WidgetURef requires type deduction, it's otherwise identical to WidgetRRef, isn't it? At least in this particular scenario, in both cases the argument is an r-value reference, so no temporaries are created. Is this reasoning correct?

Context The example was taken from Item25 of Scott Meyers' "Effective Modern C++" (p. 170). According to the book (provided that my understanding is correct!), the version taking a universal reference T&& doesn't require temporary objects and the other one, taking std::string&&, does. I don't really see why.

banach-space
  • 1,781
  • 1
  • 12
  • 27
  • 8
    `std::move`ing from an universal reference doesn't do what you think it does. Use `std::forward` instead. – HolyBlackCat May 29 '18 at 10:08
  • Why does Mr. Meyers think it's necessary to coin new terms when the ones in the standard are more than sufficient? – Bathsheba May 29 '18 at 10:09
  • `std::string` is not the default type of a character array. – JHBonarius May 29 '18 at 10:31
  • 2
    @Bathsheba I think he coined the term long before the standard did so. – Jodocus May 29 '18 at 10:36
  • 1
    There is more to this than parameter passing. You should also consider inlining, the use of [small string optimization](https://stackoverflow.com/questions/10315041/meaning-of-acronym-sso-in-the-context-of-stdstring), and the actual [cost of constructing a string](https://stackoverflow.com/a/11639305/597607). Often low-level optimization attempts turn out to be premature. – Bo Persson May 29 '18 at 12:19
  • That's true, thanks @BoPersson! But in this particular example I'm only really interested in whether a temporary is being created. Sorry, I should've been a bit more clearer about it. – banach-space May 29 '18 at 19:35

4 Answers4

5

setName(T&& newName) with argument "Adela Novak" gets T duduced as const char (&)[12] which is then assigned to std::string.

setName(std::string&& newName) with argument "Adela Novak" creates a temporary std::string object which is then move assigned to std::string.

The first one is more efficient here because there is no moving involved.

Maxim Egorushkin
  • 131,725
  • 17
  • 180
  • 271
  • 1
    [`std::string&&` inlines better](https://godbolt.org/g/eNHgdH). I doubt the moving have much to do with efficiency – Passer By May 29 '18 at 13:11
  • @PasserBy Basically, you claim that `char const(&)[12] -> string -> string` is faster than `char const(&)[12] -> string`, is that right? – Maxim Egorushkin May 29 '18 at 13:50
  • I meant after optimizations, it isn't the deciding factor. Especially considering string moves is likely copying 16 bytes. ([It's not](https://godbolt.org/g/9hNAfK), wow that assembly is horrible) – Passer By May 29 '18 at 13:54
  • Thank you @MaximEgorushkin, once you mentioned what T is being deduced as I immediately realised what was the mistake in my reasoning! – banach-space May 29 '18 at 20:00
4

In this particular example, what is the performance implications of using one implementation vs the other?

Universal references, as Scott Meyers calls them, are not primarily there for performance reasons, but, loosely speaking, to treat both L- and Rvalue references in the same manner to avoid countless overloads (and for being able to propagate all type information during forwarding).

[...] so no temporaries are created. Is this reasoning correct?

Rvalue references do not prevent temporaries from being created. Rvalue references are the kind of references that are able to be bound to temporaries (apart from const lvalue references)! Of course, in your example, there will be temporaries, but the rvalue reference can bind to it. The universal reference first has to undergo the reference collapsing but in the end, the behaviour will be identical in your case:

// explicitly created temporary
w_uref.setName(std::string("Adela Novak")); 
// will create temporary of std::string --> uref collapses to rvalue ref
// so is effectively the same as
w_rref.setName("Adela Novak");

By using the rvalue reference on the other hand, you force a temporary implicitly as std::string&& cannot bind to that literal.

w_rref.setName("Adela Novak"); // need conversion

So the compiler will create a temporary std::string from the literal the rvalue reference then can bind to.

I don't really see why.

In this case, the template will be resolved to const char(&)[12] and thus, no std::string temporary will be created in contrast to the case above. This therefore is more efficient.

Toby Speight
  • 27,591
  • 48
  • 66
  • 103
Jodocus
  • 7,493
  • 1
  • 29
  • 45
  • No, `w_uref.setName("Adela Novak")` won't create a temporary: it will pass C-style string as an argument and then call `operator=(const char*)` – yeputons May 29 '18 at 10:27
  • @yeputons Never said that, note there is an explicit temporary to illustrate the fact that in this case, Uref and Rvalue ref behave the same. – Jodocus May 29 '18 at 10:29
  • Cheers @Jodocus, that's spot on! Much appreciated! – banach-space May 29 '18 at 20:01
2

Scott himself says that WidgetURef "compiles, but is bad, bad, bad!" (verbatim). These two classes behave differently as you use std::move instead of std::forward: setName therefore can modify its argument:

#include <string>
#include <iostream>

class WidgetURef {
    public:
    template<typename T>
    void setName(T&& newName)
    {
        name = std::move(newName);
    }
    private:
        std::string name;
};

int main() {
    WidgetURef w_uref;
    std::string name = "Hello";
    w_uref.setName(name);
    std::cout << "name=" << name << "\n";
}

can easily print name=, meaning that the value of name was changed. And indeed it does on ideone at the very least.

On the other hand, WidgetRRef requires that the passed argument is a rvalue-reference, so the example above wouldn't compile without explicit setName(std::move(name)).

Neither WidgetURef, nor WidgetRRef require creating extra copies if you pass std::string as an argument. However, if you pass something which std::string can be assigned from (such as const char*), then the first example will pass that by reference and assign it to string (without any copies except for copying data from C-style string into std::string), and the second example will first create a temporary string, and then pass it as an rvalue reference to the method. These properties preserve if you replace std::move(newName) with a correct std::forward<T>(newName).

yeputons
  • 8,478
  • 34
  • 67
0

Assuming the arguments as stated in the question

template<typename T>
void setName(T&& newName)
{
    name = std::forward<T>(newName);
}

Will invoke the std::string assignment operator for the data member name with a const char * argument

void setName(std::string&& newName)
{
    name = std::move(newName);
}

Invokes std::string constructor to create a temporary, to which the Rvalue Ref can bind to.

Invokes std::string move assignment / constructor for the data member name with a std::string&& argument

Invokes std::string destructor to destroy the temporary, from which we moved the data.

clickMe
  • 993
  • 11
  • 33