219

I'm learning C++ at the moment and try avoid picking up bad habits. From what I understand, clang-tidy contains many "best practices" and I try to stick to them as best as possible (even though I don't necessarily understand why they are considered good yet), but I'm not sure if I understand what's recommended here.

I used this class from the tutorial:

class Creature
{
private:
    std::string m_name;

public:
    Creature(const std::string &name)
            :  m_name{name}
    {
    }
};

This leads to a suggestion from clang-tidy that I should pass by value instead of reference and use std::move. If I do, I get the suggestion to make name a reference (to ensure it does not get copied every time) and the warning that std::move won't have any effect because name is a const so I should remove it.

The only way I don't get a warning is by removing const altogether:

Creature(std::string name)
        :  m_name{std::move(name)}
{
}

Which seems logical, as the only benefit of const was to prevent messing with the original string (which doesn't happen because I passed by value). But I read on CPlusPlus.com:

Although note that -in the standard library- moving implies that the moved-from object is left in a valid but unspecified state. Which means that, after such an operation, the value of the moved-from object should only be destroyed or assigned a new value; accessing it otherwise yields an unspecified value.

Now imagine this code:

std::string nameString("Alex");
Creature c(nameString);

Because nameString gets passed by value, std::move will only invalidate name inside the constructor and not touch the original string. But what are the advantages of this? It seems like the content gets copied only once anyhow - if I pass by reference when I call m_name{name}, if I pass by value when I pass it (and then it gets moved). I understand that this is better than passing by value and not using std::move (because it gets copied twice).

So two questions:

  1. Did I understand correctly what is happening here?
  2. Is there any upside of using std::move over passing by reference and just calling m_name{name}?
FrodoB
  • 2,182
  • 3
  • 8
  • 7
  • 7
    With pass by reference, `Creature c("John");` makes an extra copy – user253751 Aug 07 '18 at 05:46
  • 2
    [This link](https://www.bfilipek.com/2018/08/init-string-member.html) might be a valuable read, it covers passing `std::string_view` and SSO, too. – lubgr Aug 07 '18 at 07:34
  • 5
    I've found `clang-tidy` is a great way to get myself obsessing over unnecessary microoptimisations at the expense of readability. The question to ask here, before anything else, is how many times do we *actually* call the `Creature` constructor. – c z Jul 07 '20 at 14:41

4 Answers4

255
/* (0) */ 
Creature(const std::string &name) : m_name{name} { }
  • A passed lvalue binds to name, then is copied into m_name.

  • A passed rvalue binds to name, then is copied into m_name.


/* (1) */ 
Creature(std::string name) : m_name{std::move(name)} { }
  • A passed lvalue is copied into name, then is moved into m_name.

  • A passed rvalue is moved into name, then is moved into m_name.


/* (2) */ 
Creature(const std::string &name) : m_name{name} { }
Creature(std::string &&rname) : m_name{std::move(rname)} { }
  • A passed lvalue binds to name, then is copied into m_name.

  • A passed rvalue binds to rname, then is moved into m_name.


As move operations are usually faster than copies, (1) is better than (0) if you pass a lot of temporaries. (2) is optimal in terms of copies/moves, but requires code repetition.

The code repetition can be avoided with perfect forwarding:

/* (3) */
template <typename T,
          std::enable_if_t<
              std::is_convertible_v<std::remove_cvref_t<T>, std::string>, 
          int> = 0
         >
Creature(T&& name) : m_name{std::forward<T>(name)} { }

You might optionally want to constrain T in order to restrict the domain of types that this constructor can be instantiated with (as shown above). C++20 aims to simplify this with Concepts.


In C++17, prvalues are affected by guaranteed copy elision, which - when applicable - will reduce the number of copies/moves when passing arguments to functions.

Vittorio Romeo
  • 90,666
  • 33
  • 258
  • 416
  • For (1) the pr-value and xvalue case are not identical since c++17 no? – Oliv Aug 06 '18 at 11:55
  • 2
    Note that you don't *need* the SFINAE to perfect forward in this case. It's only needed to disambiguate. It's *plausibly* helpful for the potential error messages when passing bad arguments – Caleth Aug 06 '18 at 15:57
  • @Oliv Yes. xvalues need to be moved, while prvalues can be ellided away :) – Rakete1111 Aug 07 '18 at 04:59
  • in (1), why do you need the std::move(rname)? std::move is a cast to a rvalue, rname is a xvalue; shouldn't the xvalue be moved anyway even without the explicit cast? – Ant Aug 07 '18 at 09:19
  • @Ant: the rvalue reference itself is accessed through a name, which means it's an lvalue. You need to cast it to an rvalue to propagate its temporariness – Vittorio Romeo Aug 07 '18 at 14:54
  • @VittorioRomeo Just because it has a name doesn't mean it's an lvalue - isn't that precisely the difference between xvalues and lvalues? – Ant Aug 07 '18 at 15:44
  • @Ant: **value categories** (e.g. *xvalues* and *lvalues*) are properties of **expressions**. The expression `name` is an *lvalue*, the expression `std::move(name)` is an *xvalue*. It doesn't matter if `name` is an *rvalue reference*, the expression itself is still an *lvalue*. – Vittorio Romeo Aug 07 '18 at 16:40
  • 7
    Can we write : `Creature(const std::string &name) : m_name{std::move(name)} { }` in the **(2)**? – skytree Feb 19 '19 at 04:13
  • 18
    @skytree: you cannot move from a const object, as moving mutates the source. That will compile, but it will make a copy. – Vittorio Romeo Feb 19 '19 at 14:11
  • @Vittorio Romeo Thanks for your explanation.How do you catch it? It is really not easy to find out `make a copy`.Shall we use eclipse GDB such kind of tool to single step it and check the `std::move` inner design?(Just very curious about this exploration process)In fact,my original concern is what if the case without `const`:`Creature(std::string &name) : m_name{std::move(name)} { }`? I miss the `const` at the beginning. This is a synthesized case, I know we'd better use `const` and don't change the `name`.I am curious about whether we can apply `std::move` to a `lvalue reference` here or not. – skytree Feb 19 '19 at 16:15
  • 3
    @skytree: a non-`const` lvalue reference can be moved from, but it is potentially dangerous and misleading as the moved-from object is not a temporary, and the caller of your function might expect to reuse it. I do not know any easy way of detecting "moves that don't move", unfortunately. Be careful :) – Vittorio Romeo Feb 19 '19 at 16:46
  • I'm currently using c++11/14 and will not move to 17 soon. Do you think 17 style(3) would be the recommended style when we move to 17/20? Isn't it a lot more complicated? @VittorioRomeo – user2189731 Apr 08 '19 at 08:56
  • @user2189731: (3) is implementable in C++11 as well, I don't see anything C++17-specific that doesn't have a trivial alternative in 11 (e.g. `uncvref` -> `decay`). It is complicated and should only be used when you want to squeeze all possible performance (e.g. you're writing a library, not an application) or when moves are known to be expensive (e.g. `std::array`, or unknown types in templates) – Vittorio Romeo Apr 08 '19 at 10:16
  • Can someone let me know what's the term for "A passed rvalue is moved into name" for case 1? Is that (optimization, or copy elision) defined in C++ standard? Thanks – dragonxlwang Jan 05 '21 at 22:48
  • Can I write: ```Creature(std::string name) : m_name(std::move(name)) { }``` ? I was wondering why did people started using curly parentheses over round ones. – Investing TS Sep 13 '22 at 12:41
  • @InvestingTS: of course you can. The choice of initialization syntax is orthogonal from the problem described here, and every syntax has its pros and cons. – Vittorio Romeo Sep 13 '22 at 16:01
  • @VittorioRomeo Good answer, I can see the advantage of pass-by-value in this case, if the given argument is a rvalue, at best we can get 2 moves and 0 copies. However, this is relying upon the fact that std::string supports move. In general, what if the parameter type T does not support move? In that case, pass-by-value will incur 1 extra copy every time, compared with pass-by-reference (if we omit copy elision). Is my understanding correct? – torez233 Jan 11 '23 at 09:06
  • @torez233: Yes, your understanding is correct. – Vittorio Romeo Jan 17 '23 at 08:41
67
  1. Did I understand correctly what is happening here?

Yes.

  1. Is there any upside of using std::move over passing by reference and just calling m_name{name}?

An easy to grasp function signature without any additional overloads. The signature immediately reveals that the argument will be copied - this saves callers from wondering whether a const std::string& reference might be stored as a data member, possibly becoming a dangling reference later on. And there is no need to overload on std::string&& name and const std::string& arguments to avoid unnecessary copies when rvalues are passed to the function. Passing an lvalue

std::string nameString("Alex");
Creature c(nameString);

to the function that takes its argument by value causes one copy and one move construction. Passing an rvalue to the same function

std::string nameString("Alex");
Creature c(std::move(nameString));

causes two move constructions. In contrast, when the function parameter is const std::string&, there will always be a copy, even when passing an rvalue argument. This is clearly an advantage as long as the argument type is cheap to move-construct (this is the case for std::string).

But there is a downside to consider: the reasoning doesn't work for functions that assign the function argument to another variable (instead of initializing it):

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

will cause a deallocation of the resource that m_name refers to before it's reassigned. I recommend reading Item 41 in Effective Modern C++ and also this question.

lubgr
  • 37,368
  • 3
  • 66
  • 117
  • That makes sense, especially that this makes the declaration more intuitive to read. I'm not sure I fully grasp the deallocation part of your answer (and understand the linked thread), so just to check If I use `move`, the space gets deallocated. If I don't use `move`, it only gets deallocated if the allocated space is too small to hold the new string, leading to improved performance. Is that correct? – FrodoB Aug 06 '18 at 12:36
  • 1
    Yes, that's exactly it. When assigning to `m_name` from a `const std::string&` parameter, the internal memory is re-used as long as `m_name` fits in. When move-assigning `m_name`, the memory must be deallocated beforehand. Otherwise, it was impossible to "steal" the resources from the right hand side of the assignment. – lubgr Aug 06 '18 at 12:38
  • When does it become a dangling reference? I think initialization list use deep copy. – Li Taiji Apr 01 '20 at 08:02
  • **"The signature immediately reveals that the argument will be copied"** hits the nail on the head. Micro-optimisations described in other answers are nice to know, but shouldn't be a compelling reason unless you really need those microseconds. – c z Nov 02 '21 at 09:17
1

How you pass is not the only variable here, what you pass makes the big difference between the two.

In C++, we have all kinds of value categories and this "idiom" exists for cases where you pass in an rvalue (such as "Alex-string-literal-that-constructs-temporary-std::string" or std::move(nameString)), which results in 0 copies of std::string being made (the type does not even have to be copy-constructible for rvalue arguments), and only uses std::string's move constructor.

Somewhat related Q&A.

LogicStuff
  • 19,397
  • 6
  • 54
  • 74
0

There are several disadvantages of pass-by-value-and-move approach over pass-by-(rv)reference:

  • it causes 3 objects to be spawned instead of 2;
  • passing an object by value may lead to extra stack overhead, because even regular string class is typically at least 3 or 4 times larger than a pointer;
  • argument objects construction is going to be done on the caller side, causing code bloat;
user7860670
  • 35,849
  • 4
  • 58
  • 84
  • Could you clarify why it would cause 3 objects to spawn? From what I understand I can just pass "Peter" as a string. This would get spawned, copied and then moved, wouldn't it? And wouldn't the stack be used at some point regardless? Not at the point of the constructor call, but in the `m_name{name}` part where it gets copied? – FrodoB Aug 06 '18 at 12:43
  • @Blackbot I was referring to your example `std::string nameString("Alex"); Creature c(nameString);` one object is `nameString`, another is function argument, and third one is a class field. – user7860670 Aug 06 '18 at 17:57