9 ms·
My (least) favorite footgun is "auto" when it comes to references. If you want to get a pointer from something, you would write this: auto fooPtr = mywidg
by overgard 5y ago
My (least) favorite footgun is "auto" when it comes to references.
If you want to get a pointer from something, you would write this:
auto fooPtr = mywidget.getPtr();
fooPtr->doCoolStuff();
But we all know references are better than pointers right? So we should just write...
auto fooRef = myWidget.getRef();
fooRef.doCoolStuff()
The problem is... this is valid and will probably not do what you want. It will make a copy. What you want is actually
auto& fooRef = myWidget.getRef();
I once spent an entire day debugging a bizarre crash because of that. I was asking for a reference to a scene graph, and what was actually happening is I was getting a clone of the scene graph (which was an object that couldn't be safely copied), and then destroying a bunch of shared pointers when the function returned. Fun times.
- bregma 5y agoWait, you had an object that was unsafe to copy, but you told the computer it was safe by not deleting the copy constructor? I guess it should have done what you wanted, not what you told it.
- secondcoming 5y agoNo, everyone gets LHS auto wrong at the start. They don't realise it makes a copy. I have to point it out every other code review.
- jcelerier 5y agoThe rule is simple: auto behaves like a template parameter
- Koshkin 5y agoAh, templates... Universal references... Good stuff.
- Kranar 5y agoYep, except now we're supposed to call them forwarding references because the motivation Scott Meyers had for naming them universal references turned out to be incorrect in subtle ways. Even the notable experts get things wrong when it comes to how complex and bloated C++ is.
- Kranar 5y agoThere is nothing simple about that (not to mention it's not actually true either).
- jcelerier 5y agoYes it is ? Quoting the standard: > If the placeholder-type-specifier is of the form type-constraint auto, the deduced type T' replacing T is determined using the rules for template argument deduction. https://eel.is/c++draft/dcl.spec.auto https://eel.is/c++draft/dcl.spec.auto Now if you program in C++ regularly and don't know that in template<typename T> void f(T t) { } f(something); does a copy of something, I don't know what to say - I have never met any professional c++ programmer not knowing the language works that way
- Kranar 5y agoYou need to continue reading that paragraph to the very end and note the exception for when an initializer list is used. The following are not equivalent nor do they apply the same rules: template<typename T> void f(T) {} f({1, 2, 3}); auto x = {1, 2, 3}; >Now if you program in C++ regularly and don't know that in... I program in C++ daily and no, I didn't know that a copy is made. What I do know, as an actual professional C++ programmer, is that whether a copy is performed is incredibly complex and depends on numerous factors such as whether a move constructor exists and "something" is an lvalue but not an rvalue reference, or if "something" is an rvalue (which is not the same as an rvalue reference) and T defines a move constructor. And finally if something is the same type as T and T has a copy constructor, then the final question is whether copy elision will be performed, which the standard specifies is a valid optimization even if said optimization would change the observable behavior of the program. That's what I... as a professional C++ programmer know but I fully admit that C++ is such a complex beast of a language that I am almost definitely missing a few corner cases.
- jcelerier 5y ago> You need to continue reading that paragraph to the very end and note the exception for when an initializer list is used. Exceptions don't mean that the general rule does not apply ? I don't know how it is possible to be more explicit that "the deduced type T' replacing T is determined using the rules for template argument deduction.". That'd be like saying that "priority to the right" when driving isn't a rule because there is sometimes a "stop". > I program in C++ daily and no, I didn't know that a copy is made. you're kidding > whether a copy is performed is incredibly complex and depends on numerous factors such as whether a move constructor exists and "something" is an lvalue but not an rvalue reference, or if "something" is an rvalue (which is not the same as an rvalue reference) and T defines a move constructor. but `something` cannot be a rvalue in f(something); maybe in f(something()); or f(some + thing); but just a variable named "something", as is, passed to a template argument or auto will necessarily lead to the creation of a new value of the same type. Whether it is copied, moved, materializes three different types because people forgot to mark their constructors explicit or whatever else frankly does not matter much in my experience (after all people survived for 30 years without move semantics) - what matters is whether a new value is created (which I personally and improperly call a "copy" no matter what - I should have been more precise but it's really the most useful distinction to make in practice imho as it means that some function will be called) or just a reference to existing value which is always a no-op no matter what. > And finally if something is the same type as T and T has a copy constructor, then the final question is whether copy elision will be performed, I've been at it for 15 years now and I don't remember one time in non-toy-example-code where copy elision taking place or not did matter. Thinking about is is a self-inflicted problem ; expect that a copy happens, if it's slow, profile and if it's not, be happy.
- suprfsat 5y agoSimply by using C++, you tell the computer all sorts of things, such as "I know the entire language by heart", and "I have a death wish".
- owl57 5y agoWhile C++ is a dangerous tool indeed, ignoring the rule of 5 is analogous to ignoring the safety guidelines, not simply using the tool.
- MaxBarraclough 5y agoI'm reminded of a quote about C, sadly I forget where it's from: > It is fundamental tenet of C's philosophy that the programmer is always right, even when they are wrong.
- overgard 5y agoHonestly I think that's what makes it (and C++) a great tool, despite the frustrations. I'd rather have a footgun than a nerf gun.
- MaxBarraclough 5y ago> I'd rather have a footgun than a nerf gun. That's not really the choice we're faced with though. Ada and Rust are both far safer than C, and they're also plenty powerful.
- contravariant 5y agoYeah I learned that the hard way when I found out that std::vector apparently feels free to move your objects around in memory for you, no matter if you point to them from somewhere else. I mean in hindsight it's obvious, but still not exactly what I wanted to happen. For reference, what I did does actually work, temporarily.
- tines 5y ago> Yeah I learned that the hard way when I found out that std::vector apparently feels free to move your objects around in memory for you In standard terminology, this is described as "invalidating iterators". There are a bunch of member functions in std::vector that either do or don't invalidate iterators, e.g. push_back(...) does but size() doesn't. And as the name implies, if you call a function that invalidates iterators, all your existing iterators/pointers/references become invalid.
- Kranar 5y agoIterator invalidation is different from this. Iterator invalidation, as the name suggests, only applies to iterators. The problem OP was having had to do with dangling references. Some containers guarantee references won't dangle on mutation such as unordered_map. An unordered_map may invalidate iterators if objects are added or removed, but will never result in dangling references (unless the object is removed). That is, it is safe to have a pointer to an object owned by an unordered_map and continue using that pointer even after an iterator to that same object is invalidated.
- tines 5y agoAh, interesting. Even within that dichotomy, I'd think that if references aren't invalidated on mutation, then dereferencing iterators would be valid, but incrementing them would not be.
- einpoklum 5y agoIterators are historically a generalization of pointers. A pointer is a kind of iterator. I mean, literally. The iterator of an std::vector<T> (not a bool vector) is a T*.
- Dylan16807 5y ago> told the computer it was safe > by not deleting I think there's a problem here.
- junon 5y agoProbably one of the only common language-lawyer pieces of criticism of C++ I fully agree with. I'm a pretty firm believer that a language should be as constrained as possible with the ability to make it explicitly less-constrained. C++ fails at this pretty spectacularly.
- initplus 5y agoRule of three is such a common C++ concept though, I would be surprised if anyone with more than a few weeks experience with the language is still running into such issues.
- overgard 5y agoI've been coding C++ off and on since 1998. But not all the code I use is written by me... not even most of it. I'd love it if everyone followed best practices... but they don't.
- rualca 5y ago> I think there's a problem here. There is a problem indeed, which is OP's failure to implement exception-safe code. If someone opts to use exceptions, they have the responsibility of writing their code in a way that complies with scenarios involving throwing exceptions. Failing to support a scenario by not following the most basic principles is something that's on the person writing the code, not the language. It would make as much sense to blame java for the problems you create by not initializing objects in some code paths, because your code started to throw null pointer exceptions.
- Dylan16807 5y agoAre you talking about the same thing as everyone else? auto fooRef = myWidget.getRef(); vs. auto& fooRef = myWidget.getRef();? That's not a problem caused by exceptions. If you're talking about the article, the standards committee seems to think it's a confusing interpretation that never should have existed, and it's pretty ridiculous to say it's "basic principles".
- overgard 5y agoWell, the scene graph code wasn't code I wrote, and this was back in 2014. But sure, obviously it was an error on my part... we are talking about footguns afterall, I'm the one that pulled the trigger.
- decker 5y agoSounds like the root cause was due to not obeying the rule of five: https://en.wikipedia.org/wiki/Rule_of_three_(C%2B%2B_programming) https://en.wikipedia.org/wiki/Rule_of_three_(C%2B%2B_program...
- overgard 5y agoProbably! I don't remember the code exactly, this was back in 2014 and I think most of the code I had to consume was primarily written by a 24 year old out of college (smart of course, but C++ takes a long time to get good at).
- ncmncm 5y agoNowadays, Rule of Zero. Let the compiler generate all the special members implicitly, when the members can each do their own cleanup. This turns out to be quite often.
- leetcrew 5y agoI too have fallen victim to this and similar traps. it's a good idea to use a deleted copy constructor when you know it's not safe to copy the object. you can't completely stop others (or yourself) from doing bad things, but you can at least give them a moment to reflect on what they are doing.
- secondcoming 5y agoIt's not about safety, it's about efficiency. It's usually an unintended copy. For example, if your function `foo` returns a `const std::string&` the code `auto x = foo()` creates a copy.
- leetcrew 5y agowell I don't really see a way of solving that. I may or may not want to copy the object, depending on the object's lifetime and what I plan to do with it. warning on copies would be a step too far imo. I would immediately suppress that warning.
- vips7L 5y agoA copy of the reference or a copy of the string?
- secondcoming 5y agoThe string,
- nyanpasu64 5y agoI wish C++ copy constructors were explicit by default.
- gpderetta 5y agoThe problem is that structures are implicitly (and trivially) copyable in C and requiring adding an explicit copy constructor would break interoperability. It would also break trivial copiability which is a different issue. So, yes, backward compatibility and historical baggage. Note that if you use explicit wrappers for all object-owning pointers you would get either the right behavior or a compilation error.
- jchw 5y agoAs others have pointed out, the idiomatic solution here is to delete the copy constructor if an instance is unsafe to copy — however, I suspect the reason why auto behaves differently from other inference in requiring this to be explicit is probably something along the lines of making it harder to have a dangling reference. It’s shockingly easy to wind up with a dangling reference with fairly innocuous code, something like QString().toUtf8().data() so maybe this makes sense. (Doesn’t help for that case since it’s a pointer to raw data, but you get the picture.)
- nyanpasu64 5y agoWorse yet is an object which is safe to copy in some cases, but you intended to not copy it at the moment, and you create a dangling reference to a field within. I wish C++ copy constructors were explicit by default. (But declaring the copy constructor explicit turns off aggregate initialization, so I can't do that.)
- overgard 5y agoI think, all things considered, the way it works is probably the best way, since you might want to use "auto" to actually make a copy. It's just very surprising, because when I have explicit types I'm used to looking for that sort of error, but with an auto I wasn't (at the time) used to looking for that.
- criddell 5y agoIf you develop on Windows and are using Visual Studio 2019 "auto fooRef = myWidget.getRef()" will get a little squiggle under it to warn you that you are making a copy.
- Koshkin 5y ago> and aren't using Visual Studio OK, I tried this in Notepad, and it did not work.
- malkia 5y agoyeah, same with WordPad, but see Word would give you some squiggles :)
- overgard 5y agoI am very happy tooling is helping to make these sorts of mixups easier to find, although sadly C++ is such a hard language to write tooling for that those kinds of nice features are pretty rare. I remember back when I had to work in Xcode a few years ago I could rarely even get basic intellisense to work.
- cjaybo 5y agoIt's not that these features are rare for C++, it's that Xcode is generally a pretty bad C++ IDE (imo, of course). In both Visual Studio and CLion, these types of features work well, in addition to plenty of more advanced features like address/thread sanitizers, fuzz testing, etc.
- overgard 5y agoYou're too nice, Xcode is an atrocity. Still though, trying to unambiguously parse C++ without compiling miles of headers is not a simple thing... I find my typescript and C# tooling is so much nicer when C++ is the language I need that magic most for. VS is nice but compared to what I get with like resharper or intellij its a bit disappointing.
- amluto 5y ago> What you want is actually > > auto& fooRef = myWidget.getRef(); That variant has issues if getRef() actually returns a temporary object. You may want auto&&.
- Asooka 5y agoIf it returns a temporary, you'll get a compile-time error, as the temporary cannot be bound to a non-const ref. I would prefer "const auto&" as you shouldn't be modifying temporaries, or "auto x{std::move(myWidget.getRef())}" if I want a mutable value and to avoid copying the return value of getRef.
- einpoklum 5y agoSo, if I write: int x; auto y = x; do you expect y to be a reference? Surely not. No, my friend, auto is for values, not references. You want a reference - you have to say so. (And same goes for rvalue references - auto&& .)
- overgard 5y agoMy point was simply that auto to assign pointers doesn't cause them to lose their pointiness, but using auto to assign a reference causes them to lose their referenceness. Yes I understand why it works the way it does. I'm not saying it's wrong just that it's been an occasional footgun.
- Thorrez 5y agoI think it would be a footgun the other way too. I tend to think of pointer a as a general type aspect whereas reference is specifically on the variable that it's on and doesn't transfer. Sort or like how auto doesn't copy the static specifier. Also the rules for auto are the same as for template argument deduction. template<typename T> std::vector<T> MakeVec(T val) { std::vector<T> vec; vec.push_back(val); return vec; } int main() { int x = 1; int& y = x; std::vector<int> vec = MakeVec(y); } Notice how in this case MakeVec(y) makes a std::vector<int> not a std::vector<int&>. If you want your template argument function to take a reference, use T& not T (or use T&& which is a whole new complication). Similarly use auto& not auto.
- einpoklum 5y agoOk, that's a fair point. It's easy to make the distinction when you think about how in C++, things become references or lose their reference'iness pretty easily, but nothing ever implicitly becomes a pointer.
- jdashg 5y agoReference is a type qualifier, pointer is a type. Best to just internalize it, even if you'd rather it be different.
- gpderetta 5y agoauto by default removes top level const, reference qualification, and decays arrays and function types to pointer, same as for template argument deduction [1]; this is also true when auto is used as for return type deduction. You can use decltype(auto) to preserve the actual type of the rhs. [1] as discussed elsewhere initializer lists behave differently, but initializer_list itself was a mistake to start with.