Skip to main content
GameDev.net gamedev.net
🔒 Locked

Overall Strategy For Move-Semantics? [C++11]

Started by Juliean Jul 13, 2016 at 8:25 PM 26 replies 7.3k views
Original Post
Juliean
Juliean

Hello,

so something I've been wondering for a while. How do you generally account for move-semantics/rvalue-references, overall? What I mean is, in order to take advantage of move-semantics, I've generally been doing the following since now:

1) Make all classes have move-ctors, where possibly and advantageous. Since I'm making havy use of STL wrappers, most classes have functioning default-move-ctors already, with the exceptions being classes making use of std::unique_ptr and the latter.

2) When I know that a function will take an object that is expensive to copy, but I know that its only going to be a temporary in all forseably use cases (ie. a local function that is only called for one-time object initialization/serialization), I declarte it as &&, like this:


class MyClass
{
    using MyVector = std::vector<ComplexClass>;
    void Function(MyVector&& vData)
    {
        m_vData = std::move(vData);
    }

private:

    MyVector m_vVector;
}

However, things get a bit more complicated when I cannot foresee how an variable is to be used, or if I know I will call it with temporaries, as well as have to pass in fixed data members that cannot be moved anyways.

So to all you fellow C++11-users, how dou you take care of this? I bascially see 4 options:

1) Do not account for it at all. Just use


void Function(const MyVector& vData)
{
    m_vData = vData;
}

like you would've done without C++11 and move semantics, and take the additional copies, memory allocations and deletions. It doesn't matter in most cases (so we're basically in premature optimization land), and/or maybe the compiler can figure this out for himself (which I doubt in any case where the compiler will not inline the function, but I'm by far no expert).

2) Write both version for move and no-move operations:


void Function(const MyVector& vData)
{
    m_vData = vData;
}

void Function(MyVector&& vData)
{
    m_vData = std::move(vData);
}

Obviously this will take care of both use cases, but it will require additional work for every function that benefits for move-semantics, and produces code duplication for non-trivial functions. Also it gets really messy once there are multiple parameters that could have move semantics.

3) Write only the move-semantic version


void Function(MyVector&& vData)
{
    m_vData = std::move(vData);
}

and when calling the function with a non-temporary, explicitely create a temporary:


const MyVector vDataFromSomewhere;
object.Function(MyVector(vDataFromSomewhere));

While this is just as efficient for this case than before (the temporary I create will be moved in), it requires additional typing for every non-temporary I pass in (so I now have to specify how I want to pass it in via eigther temporary ctor or std::move for every paramter that there is, ugh).

4) Now the next option was pretty much what sparked the question. I found out that I could do this:


void Function(MyVector vData)
{
    m_vData = std::move(vData);
}

MyVector vTemporaryData;
object.Function(std::move(vTemporaryData));

Which will move vTemporaryData to vData, and then vData to m_vData. So this means that for non-temporaries, I do not have to make additional typing, and it should also be equally efficient. For temporaries, it should also work like with MyVector&&, though in both cases there is an additional move-ctor call that would otherwise be evaded (though I imagine the compiler could be able to optimize this out, plus an move-ctor call is really nothing compared to copying a vector of 1000 elements).

________________________________________________________________________

So now I know this is not the most important problem in the world and I probably shouldn't worry about it, but its just something that I found interesting and wanted some opinions/real world stories on. Do you account for move-semantics in your code (if you generally use C++11, of course). If so, do you use any of the four options I presented, or do you use it on per-case basis like I used to do before (or maybe is there something completely different that I didn't see like option 4 for the longest time)?

I find option 4 the best in this regard, though it has something weird and unusual, to now suddently be passing in all the expensive objects per value instead of reference... though I guess the same applied to before I found out that I could savely return stuff like MyVector from functions due to RVO/move semantics.

thePyro_13
thePyro_13

You could do this by combining Universal References with std::forward if you don't mind adding templates into the mix.

I haven't tested it but something like this should work:


template<class VectorT>
void Function(VectorT&& vData)
{
    m_vData = std::forward(vData);
}

If universal references work the way i understand them too, then it should accept both kinds of references, and forward to the assignment operator preserving the reference type.

You could add a static_assert with a is_vector type_trait style check to make sure you get a clean error message rather than the template type mismatch if you pass the incorrect type.

If you'd prefer not to have templates then i'd go with the multiple functions with overloads. it's cleaner for calling code:


object.Function(vDataFromSomewhere); // reference will copy data
object.Function(std::move(vDataFromSomewhere)); // move will move data
BitMaster
BitMaster
When I am in this situation (which does not happen really often) I have tried a few approaches since C++11 and ended up settling on (4).

I don't think thePyro_13's approach is useful though because it pretty much throws type safety out the window and prevents you from hiding any implementation details in its own compilation unit. It's obviously fine when you already write generic template code but I cannot see it work reasonably for the concrete stuff.
BloodyEpi
BloodyEpi

If you use universal references(which sounds like what you need) you can also add an enable_if to limit the parameter type instead of a static_assert.

Juliean
Juliean

Ah, I quess universal references is option 5) then. Though I really do not like this option in the cases I'm discussing here, since this would mean adding templates to a whole lot of functions, with all the downsides bitmaster already mentioned. I'm already using them in some places where it makes sense, but I think I'll stick to 4) when going for a broad-scale solution.



If you'd prefer not to have templates then i'd go with the multiple functions with overloads. it's cleaner for calling code:

That is one of the reasons why I disliked option 3), however as I've written 4) also has this property, without requiring additional overloads. Also imagine if I had a function like this:


void SetExtentionData(const std::wstring& stExtention, std::string&& stData, ExtentionData::VariableMap&& mAttributes);

In this case I've already accounted for what is likely to be moveable and what not, but if I wanted it to be generic, I had to write 9 different overloads to account for all different semantic attribute combinations (which is why I don't like 2) very much eighter).

Kylotan
Kylotan
Option 4 looks a bit messy since it implies the caller has to think about this problem. I'd probably prefer to use option 2, migrating there from option 1 as and when profiles are showing the copies to be expensive. But it depends a lot on whether you're writing new code or maintaining old code, and whether you can assume everyone using the code will understand all the performance implications.
Servant of the Lord
Servant of the Lord

Option 4 is promoted by some of the standard committee members. My mind has accepted the reasoning, but my fingers refuse to type it. :P

Mostly I just pass by const ref, and use r-value ref when needed.

However, const-ref for when copies aren't needed, and pass-by-value when copies are needed are technically the correct way. My only "problem" with it is that it prevents me from forward-declaring classes (since pass-by-value requires the full class definition).

Option 4 looks a bit messy since it implies the caller has to think about this problem.

Assume you have a member function like this:


void MyClass::SetText(std::string text)
{
     this->text = std::move(text);
}

The requirement is, the class needs its own variable that it can modify without affecting external variables (i.e. a reference/pointer/handle to an externally owned variable is for some reason unacceptable in this circumstance - we'll assume the class' requirements have actually been designed well).

Since a copy is going to be made anyway, caller behavior is thus:

99.9% of the time, the caller doesn't need to think. If he isn't explicitly giving up ownership of his copy, the function then makes its own copy.


myClass.SetText(variableWeStillWantToUse); //Does a copy - like normal.

.

If the caller happens to call it with a literal or otherwise short-lived value (i.e. an 'r-value'), then the function silently does a move instead of a copy.


myClass.SetText("variable that is a literal"); //Does a move - a small optimization.

variableWeStillWantToUse = "The time is now %time%";
myClass.SetText(variableWeStillWantToUse.format(getTime())); //Does a move on the return-value of the format() call (if it's a temporary).

.

But, if the caller knows that he's not going to use the variable anymore, he can explicitly choose to give up ownership:


myClass.SetText(std::move(variableWeStillWantToUse)); //Does a move.

This last one would be "premature optimization" in most cases; callers generally, 99.9% of the time, shouldn't be calling std::move().

Callers never have to think about this, unless they want to make that micro-optimization.

The *default* is business as usual with automatic micro-optimizations taking place on variables that the function already knows are temporary (like literals, or the results of function calls like (25 + 17) or something.format(str)).

Being able to call std::move() explicitly is an added bonus (or a distraction) for callers, but one they don't have to think about. Essentially, move-semantics was added to provide automatic micro-optimizations, with no change necessary in caller-behavior, and only (opt-in) changes for class/function writers.

You'd still use const-ref for variables you don't need copies of, and references/pointers/handles for variables you want shared or owned elsewhere.

Ofcourse, compilers will still do RVO to avoid even the moves, when possible, but when not possible, the moves are equal or superior to copies (a move is essentially a 'shallow-copy' with transfer of ownership, whereas a copy is a 'deep-copy', if the class has (for example) pointers to allocated memory or file handles or whatever).

The only problem with pass-by-value is that you can't forward-declare your variables, which makes it a no-go for me most of the time. :P

Juliean
Juliean



Option 4 looks a bit messy since it implies the caller has to think about this problem. I'd probably prefer to use option 2, migrating there from option 1 as and when profiles are showing the copies to be expensive. But it depends a lot on whether you're writing new code or maintaining old code, and whether you can assume everyone using the code will understand all the performance implications.

True, the caller having to think about it is something that I didn't really see. I'm not sure I'd agree that its really that messy, since it allows the user not to think about it in the general case, and if they eigther measure that its a performance problem or are overly paranoid, they can just add std::move().

Its mostly about me using my own code, though. Its a mixture of maintaining old code and writing new one (since my codebase is now ~3 years old and is still being extended, though I also do heavy refactoring with stuff like this here). So I think outside of my plugin-API, the user having to think about when to apply move is not much different to the writer of the function since its the same person, so I guess I'll stick with option 4, though I see that option 1/2 might be beneficial for a widely used API.



However, const-ref for when copies aren't needed, and pass-by-value when copies are needed are technically the correct way. My only "problem" with it is that it prevents me from forward-declaring classes (since pass-by-value requires the full class definition).

Ah, true, thats something I didn't see. Now in most of my use cases it doesn't matter because I'm using STL/templated classes here, which means they are included in the header anyways. Outside of that, I will mostly store the moved variables as class members anyways, so yet another reason why their definition is already included in the header. Still a good catch and something I might look out for at times (I'm not a fan of scrapping forward declarations myself).

Kylotan
Kylotan

Assume you have a member function like this:




void MyClass::SetText(std::string text)
{
     this->text = std::move(text);
}
The requirement is, the class needs its own variable that it can modify without affecting external variables (i.e. a reference/pointer/handle to an externally owned variable is for some reason unacceptable in this circumstance - we'll assume the class' requirements have actually been designed well).

Since a copy is going to be made anyway, caller behavior is thus:
99.9% of the time, the caller doesn't need to think.

I think I'd expect it to go more like this:

  • They want to call MyClass::SetText
  • They see that the interface takes a copy instead of a const reference
  • They then have to dig into the implementation to see whether this is going to create unnecessary copies or not, because they're used to passing const references to functions like this
  • If they find the implementation then they realise it's not a problem.
  • If the implementation is hidden then they have to ask around and find out more, or just hope it's not an issue.

Whereas with option 2, the process is:

  • ?They want to call MyClass::SetText
  • They see the interface has an overload with && in it
  • They either:
    • a) recognize that it's an r-value reference that will do the right thing, so they're happy
    • b) wonder what the hell this double ampersand business is about, read up on it, and then they're happy

This is from my experience where gamedev teams typically contain a majority of both legacy code and (legacy) coders that predate C++11 and do not use typically it, but do use older idioms designed to improve performance (eg. never passing complex objects by value).

For my own personal stuff this seems like a situation that doesn't arise often - I rarely create temporaries that then get copied into something else. Again this is probably just an old habit where, in a world where all we had was auto_ptr and we were told to avoid it anyway, we were very careful about what created an object, how they got passed on, and about not copying them unnecessarily.

BitMaster
BitMaster

[...]


I feel the need to disagree. Back when I first got my hands on a C++11-capable compiler I was working on a tool with a legitimate use of injecting expensive to copy object into something else.

Obviously, I started out with the obvious: pass by rvalue reference and all was good. At least for a while, but the program evolved and sometimes I did truly need to copy instead of move out of. Of course the first time that happens you can just create a temporary and move out of that. Then you need to do it more than once and you add the overload. You proceed to notice your overload looks somehow like this:
void myFunc(const Expensive& expensive)
{
   Expensive tempCopy(expensive);
   myFunc(std::move(tempCopy));
}
Ideally at this point you immediately realize that is a rather pointless overload and with a simple pass by value you can have the exact same effect, just without the overload-mania.

This becomes even more clear once you become aware it is also a very common pattern to specify copy/move assignment operators in one pass-by-value implementation as well.

Here is the thing: C++11 is not the new kid on the block. It's half a decade old. If it were an actual kid it would be getting bored with kindergarten and preparing to move on the more interesting pastures. It's even older when you consider how long some of these core concepts have been there in the C++0x-era when everyone still believed in the '0' of 0x.

If you show so extremely little interest in your most basic of tools at your disposal that standard idioms like that elude after half a decade, then you really should not call yourself a C++ programmer. Or if you do, at least make sure someone who went to the trouble of actually investing skill points is close by.
Servant of the Lord
Servant of the Lord

I think I'd expect it to go more like this:

  • They want to call MyClass::SetText
  • They see that the interface takes a copy instead of a const reference
  • They then have to dig into the implementation to see whether this is going to create unnecessary copies or not, because they're used to passing const references to functions like this
  • If they find the implementation then they realise it's not a problem.
  • If the implementation is hidden then they have to ask around and find out more, or just hope it's not an issue.
[...]
This is from my experience where gamedev teams typically contain a majority of both legacy code and (legacy) coders that predate C++11 and do not use typically it, but do use older idioms designed to improve performance (eg. never passing complex objects by value).

Certainly.

"What's the best practice" vs "What's the best practice for legacy codebases" doesn't always have the same answer.
"What's the best practice" vs "What's the best practice without having to explicitly teach existing programmers new language features" also puts limitations on your coding standard.

But, assuming a new code-base, and assuming the programmers understand the features of the language they are using - two assumptions that might be overly idealistic - then what's the best practice?

I'd be open to either option: (const ref and r-value ref) or (const ref and by-value), as long as it's consistent in the code-base.

If throughout the codebase, passing-by-value always meant that the function needed a copy internally, I'd be fine with that. But explicitly requiring r-value refs also appeals to me.

If explicit r-value ref is indeed the "best" option, one could conveniently create explicit copies in-place in the function call:


std::string blah = "text;
FuncTakingRValue({{blah}}); //Copies 'blah';

And if both versions of the function are needed (though I'd question why), once could just implement one using the other:


void MyClass::DoSomething(const Type &type)
{
    this->DoSomething({{type}});
}

void MyClass::DoSomething(Type &&type)
{
    //...real implementation...
}
Hodgman
Hodgman

How do you generally account for move-semantics/rvalue-references, overall?

By default, I make every new class non-copyable/non-movable, and only implement those features when required (YAGNI principle).

Generally speaking, it's only extremely reusable library code that has to worry 100% about this stuff, as it has no idea about what other objects it will be interacting with and their behaviors.

Kylotan
Kylotan

Here is the thing: C++11 is not the new kid on the block. It's half a decade old. If it were an actual kid it would be getting bored with kindergarten and preparing to move on the more interesting pastures. It's even older when you consider how long some of these core concepts have been there in the C++0x-era when everyone still believed in the '0' of 0x.

If you show so extremely little interest in your most basic of tools at your disposal that standard idioms like that elude after half a decade, then you really should not call yourself a C++ programmer. Or if you do, at least make sure someone who went to the trouble of actually investing skill points is close by.

I'm not going to debate this with you on every thread, but C++ has always had far more features than a typical competent programmer is going to know or be comfortable with, and this is doubly true of features introduced with recent compilers. Half a decade is new by large software project standards. It's one-and-a-half, maybe two shipped games, probably made with the same libraries that someone wrote for the company even further back. Nobody's going to be taking the time to go through that code and replace all their in-house ProprietaryPtr with std::unique_ptr because they're busy implementing features and because ProprietaryPtr already does the job. And even if they did, they'd also have to spend the time to educate the other members of their team, whose jobs are not to continually study C++ but to use what they already know to make games.

The company I was at up to 2010 did not use C++0x features. The company I contracted for back in 2013/2014 did not use C++11 (although we did, in our code). The company I work for now, again, doesn't use C++11 (but hey, we're replacing NULL with nullptr when we come across it). This is not new or strange. Go back 10 years and lots of gamedevs were still avoiding the standard library. Go back 15 years and plenty still viewed C++ with suspicion when C worked perfectly fine. Keeping on top of compiler changes and new language features is low priority relative to (a) successfully shipping games using tech that is already well understood, and (b) keeping up with new tech that can't be safely ignored, such as rendering tech, new input hardware, new consoles, etc. Discussions of new language features have to be made in that context.

BitMaster
BitMaster
Edit: This was written with a bit too much emotion on my part and it just strayed into territory I did not intend to. It's obviously still in the history of this post but does not really help this thread anyway.
Kylotan
Kylotan

I'm giving real examples from real industry. We can all think of ways in which we wish it were different. But it's not. In large projects we write code for the other team members first, and the compiler second. In that situation, the best practice is the approach which will cause the least confusion without ditching the benefits of the feature entirely, which I say is option 2 above.

Hodgman
Hodgman

I had to fix that for you. A competent programmer does not ignore half the language or refuse to even read up on things. Even when the current work environment does not allow it, a competent programmer spends a bit time reading up on things and trying them out. Ideally officially as part of their job in formal or informal training or if push comes to shove in their hobby projects.

I don't know if you realise if you're being quite insulting or not... But you started an offensive tangent in this thread with your opinions of people who didn't jump immediately at the latest compilers.

Some of the best programmers that I know are still new to C++11. There's certain points in a career and life where you simply don't have the luxury of hobby projects, let alone hobbies at all.

e.g. Maybe you get three hours of sleep a night, get to see your spouse and kids for an hour a day if you're lucky, and are spending overtime maintaining an language back end written in x86/x64/PPC assembly across the calling conventions of six operating systems, a git continuous integration server, a C# content build pipeline, a million line C++03 game engine codebase that's stuck at that level due to one of the five compilers that you're forced to use being terribly old, and multiple game code-bases along with providing input to the timeline and budget management production meetings... And yet you still manage to debug memory corruption bugs, train junior programmers, direct a tech-art team and micro-optimize the GPU skinning code in your "spare time" because you're a damn competent programmer.
...but someone on the internet thinks that you're just "typical" because you sleep on the train instead of reading the latest C++ spec... And regardless of the fact that now in 2016 you've finally managed to drop that old C++03 SKU and are picking up modern C++ idioms instantly...

That's a true story of one of the best programmers I know, and you're saying he's incompetent :p


We can have this discussion without needing to insult people...


On the topic of legacy code, if there really was a true requirement to cheaply move an expensive object, this was possible in C++98 too. You typically had your own member functions to achieve it, or an overload of swap. C++11 hasn't invented the idea, it's just given us a new standardised syntactic sugar for it.

The point that was trying to be made above though, is that almost no large project uses "C++" -- everyone uses a particular subset of C++.
e.g. Virtual inheritance might be banned, exceptions might be banned (either from a technical/performance standpoint, or a code maintainability standpoint), operator overloading might be banned, STD might be banned, new might even be banned!
E.g. In airplane systems, typically you can't call new after take-off!

New language features aren't automatically permitted into large projects. A company with existing idioms for moving objects may stick with them rather than using the new syntax -- e.g. They may argue that their explicit syntax is clearer and thus more maintainable :|
BitMaster
BitMaster
I'm well aware of compromises you have to accept in a working environment. I could live with saying "we do not use C++11 features in our codebase".

When C++11 or above is allowed however (completely or selected features), one should strive for the best practices. I see absolutely no point in obsessive overloads and code duplication for the rest of eternity just to compensate for some hypothetical least competent allowed programmer. Especially since (2) from above is more or less the worst scenario. An unobservant user can still easily do an expensive copy, just as in (4). I'd much have (3) in that case. At least then only a move is allowed and a copy requires explicit, clearly visible extra work (both for the writer and the reader).

It removes the possibility of an accidental copy and avoids excessive overloading (which just makes an interface very hard to read). If we have to guard against horrible team members (3) should be the way to go. Another way would be to go with a simple old-school reference and documenting the content on return is somehow "unspecified but valid", but considering we only did that for bad users, I'd much rather have (3) because an std::move rings warning bells much more efficiently compared to a note in the (probably unread) documentation.
BitMaster
BitMaster


I had to fix that for you. A competent programmer does not ignore half the language or refuse to even read up on things. Even when the current work environment does not allow it, a competent programmer spends a bit time reading up on things and trying them out. Ideally officially as part of their job in formal or informal training or if push comes to shove in their hobby projects.

I don't know if you realise if you're being quite insulting or not...



It was probably a bit sharp but Kylotan just keeps rubbing me entirely the wrong way and it's getting hot here.

I should probably have said that a competent programmer can easily pick up on C++11 which relatively little work. Sure, if your work does not use any C++11 real life might no allow you to get proficiency there. But part of being 'competent' is also being able to read up quickly once things become relevant. And honestly, just reading the C++11-page of Wikipedia already takes you a very long way.

And most importantly, a competent programmer strives for best practices. I explained in #17 above why I think the solution proposed by Kylotan is actually the worst of both worlds.

Edit: Additionally, the only real use case I have seen for the issue we are arguing about here was in heavy-duty preprocessing tools. Telling the new guy on that part of the project "by the way, considering the nature of our data we make heavy use of move-semantics. If you haven't read up on that, here is a link" does not seem to be much of a stretch. Especially since the code probably already contains lots of std::move then which should ring additional warning bells even for the uninitiated.
BitMaster
BitMaster
Anyway, I seem to not take well to the heat and I have become too emotionally invested in the issues here for some reason. I will remove the offending piece. I still believe there is a point to be made but I have neither the time nor energy anymore and it simply detracts from the core issues of the thread.

To reiterate the core points relevant to this thread: from a C++11 viewpoint, (4) is the best practice in my opinion. If you are worried about team members being up-to-date on move-semantics, (3) is a reasonable alternative at the cost of a little bit of boilerplate when you need to invoke it with a true copy but should flag most unintended copies as a compile error. I do not like (2) at all since it combines the potential of the unintended misuse of (4) with code duplication and an untidy interface.
Edit: However, if (4) is not advisable I would probably rather favor distinctly named functions which clearly describe the semantics (like for example moveXInto and copyXInto) and are backed up by the compiler (rvalue reference and const reference, respectively).
Pink Horror
Pink Horror

The company I was at up to 2010 did not use C++0x features. The company I contracted for back in 2013/2014 did not use C++11 (although we did, in our code). The company I work for now, again, doesn't use C++11 (but hey, we're replacing NULL with nullptr when we come across it). This is not new or strange. Go back 10 years and lots of gamedevs were still avoiding the standard library. Go back 15 years and plenty still viewed C++ with suspicion when C worked perfectly fine. Keeping on top of compiler changes and new language features is low priority relative to (a) successfully shipping games using tech that is already well understood, and (b) keeping up with new tech that can't be safely ignored, such as rendering tech, new input hardware, new consoles, etc. Discussions of new language features have to be made in that context.

I wonder what percentage of new C++ games are being made with exceptions and RTTI turned off.

Topic Locked

This topic has been locked by a moderator. New replies are not allowed.

Sign in to reply to this topic.