12 ms·
The Sad Truth About C++ Copy Elision
- zabzonk 6y agoSo, don't do non-trivial things in copy constructors. In fact, try your best to avoid writing copy constructors at all - I can't remember the last time I wrote one. Unless you are an experienced library writer you should think very, very hard about writing copy or move constructors (and even then, think again).
- CyberRabbi 6y agoCopy constructors are usually necessary when wrapping data on the heap or an OS handle. I agree this is relatively rare in user code but what’s not necessarily rare is having one of those types as members in an otherwise trivial type. std::string is a good example.
- zabzonk 6y ago> std::string is a good example Yeah, so USE std::string in your classes, then no copy constructors written by you will be needed. If you find yourself heap-allocating things explicitly with "new", then you are almost certainly doing something wrong.
- rrss 6y agoThis seems mostly irrelevant to the example in the post. Why does it matter who wrote the copy constructor? In the author's example, they did not write a copy constructor, they USEd std::string.
- CyberRabbi 6y agoYes but this article is not about the hazards of using new/delete. It’s about the hazards of custom copy constructors. Simply using a common type like std::string is sufficient to trigger that hazard even if you never write a custom copy constructor yourself.
- zabzonk 6y agoPerhaps you could point out the hazards? The generated code is longer, but so what, so long as it is semantically correct - longer code is not a hazard. And without knowing how the code was built, with which compiler, with which options etc. it's very hard to comment.
- roblabla 6y agoBigger ASM means you can fit less code in cache, which makes your code slower. If this happens in a hot loop, it may be the difference between "my code fits entirely in L1 cache" and "my code needs to be fetched from L2/L3". This matters in high-performance scenarios. The fact that you can introduce a performance issue simply by creating a temporary is absolutely bonkers, and makes writing high-performance code more complicated than it needs to be (and god knows it's already very complicated).
- DoofusOfDeath 6y agoDepending on the processor, it can also be the difference between instructions in a tight loop only needing to be decoded once (because of micro-op caching) vs. decoded each loop iteration.
- swolchok 6y ago> without knowing how the code was built The article links out to Compiler Explorer (https://godbolt.org/ https://godbolt.org/), which shows the exact compiler version and options and lets you play with different ones. It shouldn’t matter which compiler — the standard prohibits optimizing this.
- klyrs 6y agoC++ will make implicit copy constructors for you, for simple classes. But those can he deleted, if you really want to be sure they won't be used
- CyberRabbi 6y agoWhen I learned about copy elision, I learned about it in the context of learning about RVO, which is a special case in C++. Otherwise I had no reason to generally assume the compiler would elide copies. I don’t understand what caused the author to assume that a compiler would elide the copy when passing a value argument to a function. That seems like an unfounded assumption to me and I don’t think there is anything sad about it not being true. To avoid the copy in all cases the “consume” function should probably accept a reference argument. That would be idiomatic C++.
- krona 6y agoThis. The compiler is doing the least surprising thing here - I don't understand the author's confusion.
- PartiallyTyped 6y agoBy accepting a reference argument, you introduce pointer deref. Wouldn't it be better to use Widget && temp = complicated_long_function(...); consume(temp); Am i missing something?
- AzN1337c0d3r 6y agoYour proposed example code does exactly what GP said, pass as a reference (in your case, an rvalue reference).
- CyberRabbi 6y agoNot quite, I suggested changing the signature of “consume” to accept a reference. His example doesn’t do anything to invoke copy elision, since his named rvalue reference will be function cast to a normal reference and then copy-constructed into the stack value passed to “consume”.
- AzN1337c0d3r 6y agohttps://gcc.godbolt.org/z/qd5qnhPT6 https://gcc.godbolt.org/z/qd5qnhPT6 How does any copy-construction happen here?
- tcbawo 6y agoNot optimizing when creating a temporary variable is unfortunate. I sometimes create those for debugging purposes, give them a descriptive name, then leave it in for documentation purposes after the debugging is complete. Typically for me, this isn't the sort of code that executes in a tight loop, so is unlikely to impact performance. Edit: I agree with the other commenter about taking in a Widget by reference instead of by value in the consume function.
- MontagFTB 6y agoIf the routine is truly going to consume the widget, then you’re paying for a copy within the routine if you take the parameter by reference. Unfortunately that’s just kicking the can down the road. I’d prefer code like this use the move operation to make the code reasonably efficient and maintainable. Then, only after its proven the line is on a critical performance path, it can be further optimized later.
- pjmlp 6y agoThis is why I never try to do clever things in C++. Stay on the happy path of not having to be an ISO expert.
- wffurr 6y agoThe problem is when you try to write boring code, but the compiler or library decides to be clever for you. Now you have a weird bug in otherwise innocuous code that requires an ISO expert to understand.
- pjmlp 6y agoIndeed, I won't believe anyone that asserts they can fit on their head all UB usecases that are documented on the standard (> 200). Nowadays I just use it for native library wrappers, as it is better than plain C, but still hoping for FFI improvements on the languages I use that will reduce that need even further.
- gumby 6y agoIn my experience that essentially never happens. One of the complaints about c++ is how many obscure features it has. Most of those obscure features are for authors of the standard library implementations (or other libraries intended for widespread use), not for user code. If you look at the standard library source it’s very hard to read because it’s full of things to transparently handle weird corner cases in order to avoid user surprise. But in my code I don’t use any of that stuff. There’s a pretty straightforward modern language in the middle of that, and not even a huge one.
- rubyn00bie 6y agoPardon me, and of course you have no obligation to answer (I'll be googlin' a fury after I post this), but could you perhaps share a resource or two for c++? I ask because this: > There’s a pretty straightforward modern language in the middle of that, and not even a huge one. made me literally mutter "woah... really?" I've always had the impression, and granted it is based primarily in ignorance and group think (looks around), that learning C++ is an endeavor that takes years because the language is so complex. And so, I would love any resources that make it more tractable... or does it really boil down to don't use "obscure" features you don't need?
- MaxBarraclough 6y ago> If compilers were to unpredictably remove copies, and thus remove pairs of copy/move constructor & destructor calls, they might break your code. But they are permitted to do exactly this. This is one of the unusual things about copy-elision in C++. The standard says that under certain circumstances, the compiler is permitted (but not required) to elide a copy operation even if this causes a change in observable program behaviour. * https://en.wikipedia.org/wiki/Copy_elision https://en.wikipedia.org/wiki/Copy_elision * https://en.cppreference.com/w/cpp/language/copy_elision https://en.cppreference.com/w/cpp/language/copy_elision (the article referenced by the post)
- ncmncm 6y agoThey are permitted in certain cases, not in others. It is proposed to widen the former list for C++23. All this is an argument for "rule of zero": write your classes in a way that the default copy and move constructors and destructor do the right thing. That gives the compiler latitude to do the right thing in places that it otherwise could not. Some code obliged to do extra work will be able to do less of it when compiled with a compiler that knows C++23, provided you have followed rule of zero.
- pjmlp 6y agoThis is the kind of nuances that make it increasingly hard to look at C++ code and be sure of its semantics, without diving into compiler flags and standardese.
- MaxBarraclough 6y agoNot to mention the paywalled C++ standards documents. People say A few hundred dollars is nothing to a professional software engineer, but few C++ developers actually buy it, so the legalese they consult is often not the letter of the law but a proxy like cplusplus.com, replete with the occasional error.
- pjmlp 6y ago
- logicchop 6y ago"The Sad Truth" is that you have to be a little bit cautious/knowledgable in order to take advantage of a potential optmization?
- nly 6y agoC++ is a value semantic language. Copies are implicit and references are explicit (expressed in the syntax). Many other languages are reference oriented, and references are implicit and copies are explicit. Both have pros and cons, and in both you sometimes want to cross the aisle. When you consider that references in C++ need to be explicit you have the following solution: https://gcc.godbolt.org/z/PGbc46cxK https://gcc.godbolt.org/z/PGbc46cxK
- opportune 6y agoCopy elision should only be thought of in terms of prvalues. As soon as you give something an l-value, all bets are off. And I don’t really like the “consume” example since if you want to avoid copies into functions like that you should really be defining the function as taking Widget& and not rely on what you think is copy elision.
- nightowl_games 6y agoThis blog post is great, but I think it ends too quickly. It goes into a lot of detail, but then skims over this "few circumstances where copy elision will happen" without the same level of detail or examples. And it doesn't provide examples of copy constructors, or explain anything about what happens if you delete the copy constructor. This blog post was just getting good, then it stopped!