My favourite C++ footgun
dustri.org
dustri.org
> The second one is there since this is C++, priority() might raise an exception, meaning that new Widget will be called, but never passed to std::shared_ptr, and thus never deleted!
Is now impossible (thank God!). See a full SO discussion here[1]. Stuff like this makes me so happy I don't write C++ anymore, but the gist of it is (from the standard):
> For each function invocation F, for every evaluation A that occurs within F and every evaluation B that does not occur within F but is evaluated on the same thread and as part of the same signal handler (if any), either A is sequenced before B or B is sequenced before A.
[1] https://stackoverflow.com/questions/38501587/what-are-the-ev...
It doesn't.
Exercise for the reader: detect evaluation interleaving without exceptions.
> If your code throws so many exceptions that not freeing the memory allocated during that code point becomes a problem
It need not manifest itself as a mere memory leak.
Exercise for the reader: make something happen beside a memory leak.
Yes it does. Of course you can detect different evaluation orders using side effects, but the fact that evaluation order is unspecified isn't generally a problem: if you write code like `a() + b()` and a() and b() has side-effects that depend on the order they're evaluated in, that is just a bad way of writing code. In any language.
The point here is that the compiler is allowed to interleave subexpressions of the two different arguments which causes a memory leak in seemingly safe code. That is a very unreasonable thing for the compiler to do, which is why it is a footgun. But not that it can ONLY happen if the second function throws in between the construction of the object with "new" and the construction of the shared_ptr: if the second function doesn't throw an exception, the construction of shared_ptr is guaranteed to happen.
Incidentally: those "exercise for the reader" comments are incredibly obnoxious.
generally is the key word here.
> if you write code like `a() + b()`
The code doesn't have to look like that. Add e.g. some arguments...
f(**x, g());
The number of stars evaluated before 'g()' (0, 1 or 2) varies between compilers, and the program I was working on would break if only one * was evaluated before g.> Certain other aspects and operations of the abstract machine are described in this International Standard as unspecified (for example, order of evaluation of arguments to a function). Where possible, this International Standard defines a set of allowable behaviors. These define the nondeterministic aspects of the abstract machine.
> Certain other operations are described in this International Standard as undefined (for example, the effect of dereferencing the null pointer). [ Note: this International Standard imposes no requirements on the behavior of programs that contain undefined behavior. —end note ]
Unspecified means that you can't rely on the order of evaluation. "Anything" may not happen; the compiler controls the sequencing.
Another favorite “criticism” I have of C++ goes along the lines of “but someone could overload operator+ to be division!” I have only seen that as a contrived critic’s example, or a joke.
Actually, that’s what you are supposed to do, as this is the only way for a constructor to let the caller know that something went wrong.
The factory could return std::optional<Foo> or std::unique_ptr<Foo> or std::variant<Foo, Error> or absl::StatusOr<Foo> .
It's an improvement in terms of code reuse but not in semantic simplification basically.
I'm not sure what you mean. Here's an example with the output from std::optional being unwrapped and put in a struct member: https://godbolt.org/z/Ms4Kar18j
Also there's opt.value_or() which can use a default value in the case opt is nullopt.
>It also replaces an automated and enforced action by the compiler (exception being thrown) by a need for a manual check which feels very 1975
I think remram's goal was to avoid exceptions. So this is a criticism of remram's goal rather than the method to achieve that goal.
Also, std::optional can integrate with exception-producing code because opt.value() throws an exception if opt is nullopt.
And now you have to do this for every type that have some amount of preconditions. I don't understand in which universe this is sane - more code == more bugs.
e.g. here's what one would write with exceptions: https://godbolt.org/z/evMsh6383 (and, to be honest, if one is writing e.g. a command-line tool and not a reactive gui app, most likely https://godbolt.org/z/eW8YM5Gz4 as your OS will catch the thing anyways and then it's just a `coredumpctl gdb` / "open in debugger" away) ; you get the same guarantee of never having an invalid Foo but at much less mental cost.
It also prevents you of having aggregates as all the "parent" owner class will need their own wrapping in optional + private ctor, in case a sub-sub-sub-sub field would fail.
e.g. given
struct DomainObject {
Foo mainFoo;
Foo secondaryFoo;
int whatever;
};
struct GameState {
std::vector<DomainObject> objects;
};
now you can't just do DomainObject{initForMainFoo, initForSecondaryFoo, 123}; or DomainObject{.mainFoo = "", .secondaryFoo = "", .whatever = 123}; anymore, even less GameState{{DomainObject{...}, DomainObject{...}}; which is how modern C++ is meant to be usedOn the other hand return values (using something like a try/either monad) have the property of forcing the developer to think about the unhappy path in every layer. Maybe a bit more code, but the quality of diagnostics - priceless.
IMHO exceptions/panics should be reserved for bugs only (eg assertions), and return values for all the other error handling caused by user input or environment.
but it's the whole point ! that's exactly why exceptions are good !
> to a place where there is so little context that the final error message presented to the user is near useless.
there are only two valid cases: there's an error that you know you are able to recover from, you did recover, and the users does not see a message and never knows that an error happened, OR the user sees the message "The program has encountered an error. A backup of your data has been saved in c:/foo/. The program will now exit.".
Anything else makes software terrible to use. If you have logging and recovery to do, use your logging architecture and core dumps, don't misuse exceptions for that.
> On the other hand return values (using something like a try/either monad) have the property of forcing the developer to think about the unhappy path in every layer. Maybe a bit more code, but the quality of diagnostics - priceless.
Except what happens in practice is that people just put panics everywhere because most of the time there is no meaningful action one can take at the layer where the implementation happens. Actual examples in servo: https://pastebin.com/0DAFU9vS
I also invite you to run the following command:
$ cd ~/.cargo
$ rg panic! | wc -l
I get 2987 as a result. Choosing to program like this instead of using exceptions which at least can be recovered by the person who will use your lib, is literally hateful towards users.I've not seen Go code to be meaningfully different either.
> IMHO exceptions/panics should be reserved for bugs only (eg assertions), and return values for all the other error handling caused by user input or environment.
Sure, this is the community consensus in C++. It's the Python people who use exceptions to get out of for loops :)
But the most important rule is that - invalid state must not be representable in the program. There must be no way to get an "invalid" object (unless the object being invalid is part of its possible domain, but this is exceedingly rare when one isn't interfacing with a 1985 C API)
Good for what? For crashing the program with a stacktrace? Probably. But for regular error handling - that's a terrible thing. They break normal control flow by adding a parallel / alternative control flow. Now with them you have to think on what happens if any of the code you call suddenly throws an exception and the number of potential control flow paths increases drastically. They are not referentially transparent and it is nearly impossible to do functional programming with them (they introduce side effects everywhere). You can also end up in a really bad situation when a destructor throws (typically crash).
> I get 2987 as a result. Choosing to program like this instead of using exceptions which at least can be recovered by the person who will use your lib, is literally hateful towards users.
But panics are Rust equivalent of exceptions. That's the whole point of them. They are used for exceptional cases, which are impossible to recover from (which means the only way to deal with is to dump core and terminate) and most of the time preventable (you can always avoid dividing by zero or going out of array bounds).
It looks like we agree they should not be used for regular errors, like e.g. file not found. I'd not like my word processor to terminate with a core dump when it couldn't load the file from the disk. In this case return values are way better error handling mechanism.
> invalid state must not be representable in the program
Returning an option/try/either strikes that goal just as well as exceptions.
No, not really. Either the allocation followed the happy path, or one of the expected failure modes kicked in.
And the supposed footgun example boiled down to failing to handle one of the failure modes.
I mean, think about it: std::make_shared is a factory method, isn't it?
However mind that if you don't throw from C++'s perspective the object is fully created, so that the destructor will run.
If you throw an exception from the contractor, the object won't be fully created.
Yeah there's definitely criticisms you can throw at C++, but this always struck me as stupid. You could also write an `add` method that divides in Java.
I've never liked the idea of 'operators' in the first place, tbh. Scheme opened my eyes in that they're all just procedures. And then with Smalltalk and Scala I saw that they can all just be methods as well.
I don't see how that is so. C++ 17 allows new Widget to complete and then the priority() call to execute and throw before both are passed to shared_ptr(), thus creating a leak. Your cited example [1] doesn't leak because both arguments are shared_ptr. Seems to me that C++ 17 does indeed solve this latter case but not the former.
> evaluations of A and B are indeterminately sequenced: they may be performed in any order but may not overlap: either A will be complete before B, or B will be complete before A. The order may be the opposite the next time the same expression is evaluated.
And:
> 21) Every expression in a comma-separated list of expressions in a parenthesized initializer is evaluated as if for a function call (indeterminately-sequenced)
Emphasis mine. What the above basically says is that in the case of some function `f(A, B)`, the arguments `A`, and `B`, are what's known as "indeterminately-sequenced" -- this mean that their execution cannot be interleaved (overlap) -- but they still individually execute in a non-deterministic order (A before B, and sometimes B before A)!
With that said, the good news is that B can now never throw in the middle of A, which is precisely what we have in OP's example.
Looks like footguns.com is taken but footguns.org is still available.
* Start with C++20 unless you have a very good reason not to. Not only does it obviate crufty old things like SFINAE (concepts!), but it includes a ton of usability fixes (e.g. reflexive operator== -> I only have to write the code for MyType == MyOtherType in order to get both that version and MyOtherType == MyType). A lot of lambda behavior has really been cleaned up too (e.g. capturing `this` has fewer corner cases).
* start with Google's coding guidelines [0], as they've been developed to avoid many footguns (the bottom line depends on it). Once you understand things better, keep removing these rules (e.g. about exceptions, const& parameters; many of these rules are good for big teams, but annoying to follow for individuals)
* The CppCon "back to basics" talks are absolute goldmines, and the speakers usually tell you if some technique has been outdated as of the recording. Some things in certain books, although valuable, may be outdated based on later revisions to the language.
* The best notes to take are usually some structure list followed by a bunch of godbolt links with examples, e.g. this one I made to demonstrate the reflexive operator== behavior [1]
[0] https://google.github.io/styleguide/cppguide.html [1] https://godbolt.org/z/scPdcaWT7
priority() is not passed to shared_ptr(), only to processWidget().
processWidget(new Widget, priority());
I think I saw the above code elsewhere.
However in the Effective C++ example, the `shared_ptr` constructor gave a false sense of security as it seemed the `new`-ed `Widget` was always managed by the smart pointer from its allocation.
Another example is when undefined behavior occurs, like (iirc) signed integer overflow. Compilers can technically do anything they want, including weird things. But how often does that actually happen?
int a;
...
int b = a
if (a + 1 > b){
foo();
}
Then compilers will use the fact that integer overflow is undefined behavior. This allows the compiler to assume the `if` statement will always be true. And hence it removes this check.This isn't just the compiler removing a check the programmer wanted. This could occur because of constants, where other constants make the check meaningful. It could also occur after in-lining a specific function call.
Really, the only case where this optimization would be bad is if the author was trying to detect overflow. Sadly, detecting if something would cause undefined behavior in C code is hard. Because you cannot try the thing.
Turning optimizations on doesn't suddenly undefine any behavior, it just changes what the compiler does when encountering some of those behaviors (among other, non-UB based optimizations). A non-optimizing compiler and an optimizing compiler both have to conform to the standard.
It is simply not possible to write an optimizing compiler in which the evaluation order is truly indeterminate yet always the same.
{
auto& a = <expression>
auto& b = <expression>
foo(a, b);
}
To foo(<expression>, <expression>);
Could be an optimization.Edit: modified the code a bit to make the two samples more similar.
[0] http://open-std.org/JTC1/SC22/WG21/docs/papers/2016/p0145r3....
[1] http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p040...
> We do not believe that such a nondeterminism brings any substantial added optimization benefit, but it does perpetuate the confusion and hazards around order of evaluations in function calls.It perpetuates unnecessary confusion around brace-initialization vs. direct initialization using parenthesis.
> We found that some entries in the benchmark suite ran slower, others ran faster compared to the scenario where the evaluation of the argument list is left unspecified. The variation is between -4% and +4%. It is worth noting that these results are for the worst case scenario where the optimizers have not yet been updated to be aware of, and take advantage of the new evaluation rules and they are blindly forced to evaluate function calls from left to right. It is clear that the left-to-right evaluation strategy is triggering new optimization paths (different inlining decisions and different register allocation) affecting the variations in the benchmark performance. It appears those opportunities have not traditionally been exploited, even though permitted under the unspecified order regime.
Unfortunately, they seem to have been overruled by the Core Working Group [1], for reasons I have not been able to dig up.
[0] http://open-std.org/JTC1/SC22/WG21/docs/papers/2016/p0145r3....
[1] http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p040...
> In this case, it's pretty easy to see that 3 will have to be printed after both 1 and 2, but it's unclear whether 1 or 2 will be printed first. The reason for this is evaluation order: in order to evaluate one + two, we'll need to evaluate both one and two. But there is nothing telling GHC which of those thunks should be evaluated first, and therefore GHC is at full liberty to choose whichever thunk to evaluate first.
Note that in Haskell evaluation order is not guaranteed anywhere, even between separate statements, whereas in C++ it is basically guaranteed everywhere except function calls.
As for paranoia about undefined behavior? Compilers won't emit a program to call you a pizza or format your disk. There are risks, but those aren't real ones.
how is that different from undefined?
Unspecified behavior is bounded both in space and time. Unspecified behavior may only affect program semantics within the statement in which it is engendered and may only be observed within the region of memory that is written to or read from.
It is often said that undefined behavior is invalid, and one can sympathize with that statement as it's commonly repeated, but it's not true. All the standard says about undefined behavior is that the standard imposes no requirements about program behavior, it does not state that the program is invalid or that the program must behave in some kind of invalid manner. Almost all non-trivial C++ programs engender undefined behavior in a manner documented by the compiler and do so in a safe and principled way.
It certainly is the case that most compiler writers have decided this is okay. And I personally tend to agree with them.
There's lots of cases where the object itself is big, though. Think of objects with big fixed arrays, "god objects" with a bajillion pointers, or objects which themselves allocate data in-line.
That's why there's also a std::make_unique even though it doesn't have a control block (although it was added later, but that was just an oversight).
I believe this is correct. Here's [0] some old Boost documentation on Boost's make_shared which inspired the C++ standard's make_shared. It mentions both reasons.
[0] https://www.boost.org/doc/libs/1_67_0/libs/smart_ptr/doc/htm...
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.> It is fundamental tenet of C's philosophy that the programmer is always right, even when they are wrong.
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.
> by not deleting
I think there's a problem here.
C++ fails at this pretty spectacularly.
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.
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".
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.
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.
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.
I mean, literally. The iterator of an std::vector<T> (not a bool vector) is a T*.
static_assert(std::is_same_v<T\*, std::vector<T>::iterator>)
Link provided below for convenience [1].Furthermore just like a cat is animal does not imply an animal is a cat, a pointer being an iterator does not imply that an iterator is a pointer and holding such a confusing thought is the source of many bugs and poor C++ code. An iterator not only represents a form of indirection to an object, it also represents traversal through a collection of values.
unordered_map guarantees that the objects it stores will remain alive until the unordered_map is destroyed or the object is removed from the map. However, unordered_map, as the name once again suggests, does not provide any guarantee about the ordering of objects. Hence, if you have an iterator to an object owned by such a map, and you add or remove objects to that map, the iterator becomes invalid not because the object the iterator references gets destroyed, but because the iterator loses information about where it's located within the unordered collection.
For further information, please review the following link [2], specifically the quote "References and pointers to either key or data stored in the container are only invalidated by erasing that element, even when the corresponding iterator is invalidated.".
[1] https://godbolt.org/z/Y5f4d74Kb
[2] https://en.cppreference.com/w/cpp/container/unordered_map
And an animal is a generalization of a cat, but that doesn't mean an animal is a cat. What is true of a particular doesn't have to be true of the general.
> But, while implementation defined, a vector iterator must behave identically to a pointer
No it does not. MSVC implements a vector iterator with additional runtime safety guarantees in DEBUG mode and while those guarantees are not mandatory as per the standard, they are fully in compliance, so saying they have to behave identically is patently false as MSVC's behavior is a superset of the behavior provided by a pointer.
> suggesting that holding this opinion is a sign of inadequacy is condescending.
If you care about writing correct code, then do not mix the very concept of pointers, arrays, references, and iterators with one another. They are all related to one another but have very important differences to the point that calling them literally identical to one another is a sign of inadequate understanding. It happens far too often and inevitably leads to poor code, bugs, and a fundamentally poor understanding of what these things represent conceptually.
Second of all, nobody is saying that pointers, arrays, references, and iterators are identical in general. They are clearly related concepts but differ in fundamental ways. However, my point was that a vector iterator needs to behave like a pointer would and can in fact be implemented as a naked pointer. Pointing to MSVC's implementation having safety checks does not change that fact, because pointing to what an implementation does on undefined behavior is clearly out of scope when discussing specified behavior. I'm not going to bring address sanitizer into this discussion to show how pointers can actually be a superset of "pointer behavior" because that would just be irrelevant. The fact remains that iterators were originally meant to act like pointers, and in many cases still do. Believing this does not mean I have a poor understanding of the language.
>Second of all, nobody is saying that pointers, arrays, references, and iterators are identical in general.
The comment I replied to said, and I quote:
"I mean, literally. The iterator of an std::vector<T> is a T*."
That is what I originally replied to before you felt it necessary to interject yourself into the conversation to police my tone.
>However, my point was that a vector iterator needs to behave like a pointer would and can in fact be implemented as a naked pointer.
No, you said it is required that they behave identically, which is false, once again, I quote you:
"a vector iterator must behave identically to a pointer"
There is no such requirement that they behave identically, and there is a demonstrable example of a compiler where they do not behave identically. Furthermore your talk about it being undefined behavior is false on the basis that in general, two pointers of type T* may compare with one another, for example the following is valid:
auto v1 = new int();
auto v2 = new int();
v1 == v2; // This is valid.
However, this is undefined behavior. auto v1 = vector_a.begin();
auto v2 = vector_b.begin();
v1 == v2; // This is undefined behavior.
Only iterators to the same vector may be compared, which is not true of pointers.>Believing this does not mean I have a poor understanding of the language.
I did not say you have a poor understanding of the language as a whole, I said if you believe that an iterator to a vector is identical to pointer as you have claimed, then you have a poor understanding of iterators.
I stand by that statement and I would suggest that you not be so sensitive about being wrong about a corner case of the language and instead simply accept this to be a fact, learn something new, and move on with your day. I learn new things about C++ all the time, sometimes from polite people, sometimes from jerks, it's all the same because what matters is the learning. Something doesn't become wrong just because the person who told it to you said it in a way you disapprove of.
Having said that, all the best to you Sir and thank you for engaging in this discussion. I have said all I think is appropriate given the topic.
Reminder that I was not the one who passed up the opportunity to reply to 'einpoklum with a simple "iterators can behave like pointers, but vector's iterators don't have to be pointers. You can see this here: https://godbolt.org/z/Y5f4d74Kb". I'm just annoyed that you went after this person for coming to an incorrect conclusion from what is clearly a correct position, which is that iterators were intended to "morally" be pointers, especially in cases where they iterate over contiguous containers. You are correct that I claimed that vector iterators must behave identically to pointers, but I really meant to say that this is only in cases where the iterator has behavior defined on it: I alluded to it previously when I mentioned that GCC and Clang wrap pointers in their own custom iterator classes to prevent people from accidentally using operations on them that are not legal. There's a bunch of other things that the iterator doesn't guarantee, like being directly casted to an integer.
But, stepping back a bit: do you really disagree with a claim that a vector iterator is meant to be a little pointer into the internal buffer it has, with a bit of additional restrictions on top that are reasonable to add? This seems to be a very strange thing to disagree with, and I would like to hear more about why you hold that position.
This being C++, being pedantic is a Good Thing.
The distinction between reference and pointer invalidation, and the requirements and axioms of the various iterator concepts are all valid things to point out, especially in a thread about C++ footguns.
You say that as if you think that makes it better.
https://en.cppreference.com/w/cpp/container/vector
It _may_ be a T, or anything satisfying LegacyRandomAccessIterator.
> just like a cat is animal does not imply an animal is a cat,
It's not "just like" that. Among other differences between the pairs of concepts: The concept of an animal didn't arise as a generalization of cats.
Most of the costs are not counted as runtime for your process, so benchmarks invariably appear to show GC as costing less overhead than it does. Among the costs, as with all the myriad varieties of caching done in modern systems, is that GC makes it hard to know the costs of design choices you make. Many of the costs are in making the caches less effective.
Most languages either don't have the concept of classes or ensure that invalidating references is an explicit operation (such as calling the destructor).
It's not as much as "feeling free" as it is having to reallocate the array because you added elements beyond it's capacity and they had to be stored somewhere.
So in my case the problem was very much that it took the liberty of doing something I wasn't expecting it to.
vector<int> v = {42};
...
auto x = v[0]
v.clear();
// if x was deduced as reference, using x at this point
// would be UB
std::cout<< x;But vector is usually what one should reach for unless one has a good reason not to. The only danger really comes when the lifetime of iterators and the thing they point to become a bit decoupled, even due to insertion!
If anyone is looking to write a custom iterator, I definitely recommend Boost's new stl interfaces: https://github.com/boostorg/stl_interfaces
https://www.boost.org/doc/libs/1_76_0/doc/html/boost/contain...
> 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
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 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.
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.
Even the notable experts get things wrong when it comes to how complex and bloated C++ is.
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.
https://dreamsongs.com/Cadillac.html
"Lucid Energize Demo VHS 1993"
https://www.youtube.com/watch?v=pQQTScuApWk
VisualAge C++ Version 4.0
http://www.edm2.com/0704/vacpp4/vacpp4.html
https://books.google.de/books?id=ZwHxz0UaB54C&pg=PA206&redir...
However they were too resource hungry for the hardware of those days, then Java and .NET took over the spotlight, and until clang/LLVM came into the scene, the industry forgot of what was already possible.
OK, I tried this in Notepad, and it did not work.
That variant has issues if getRef() actually returns a temporary object. You may want auto&&.
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.
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.
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&& .)
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.If you write C++ it might be obvious what the problem is.
If you don't, this will absolutely ruin your entire day.
The worst part is, 95% of the time, it will probably work without issue.
But eventually, pushing a new item to the Vector will trigger a relocation of the whole vector, which will invalidate your reference and bring down production. Have fun debugging that.
Valgrind memcheck is your friend.
It does. Some collections have a "collection.retain" method.
collection.retain(|v| true_if_we_want_to_keep(v))
which goes through the collection in linear time and deletes any items for which the test is false. This is O(N).Some collections ("multi_map", which isn't used much, comes to mind) don't have that. You have to make a read pass over the collection, construct a list of things to delete, then go through the delete list and delete those items. Can't read and change with the same iterator. That's slower. But sound.
I'm OK with that. It avoids subtle bugs.
struct OrderedSet {
deque<string> storage;
set<string_view> vals;
}
The deque holds the string contents, and the set holds views of those contents. This is safe because the deque has iterator stability, and it's pleasant because the `vals` set can just be a regular set.As you say Rust does not allow this; its answer is IndexSet, and it is implemented differently, like:
struct OrderedSet {
Vec<String> buckets;
RawTable<usize> hashToBucketIndex;
}
RawTable is a hash table that doesn't know how to compute a hash or determine equality. Instead, you provide it with closures to do those tasks, but at the call site (not construction time).Basically Rust achieves the same thing through a layer of indirection involving indexes, which is a common pattern.
If you wanted to do the same in Rust it would be Vec<Box<T>>. Mutating the collection won't invalidate the pointers.
Rust still won't let you mutate a Vec<Box<T>> while you hold a &T or &mut T borrowed from the Vec.
Perhaps it would be sound to do so unsafely; Rust would let you mutate a Vec<Rc<T>> while you hold a Rc<T> cloned from the Vec. But I'm not clear on whether Stacked Borrows allows moving the Box while you have a &mut T pointing to the same memory as the Box points to. It definitely does not allow dereferencing the Box. (I heard Stacked Borrows will be updated to make self-referential types sound, and I don't know if it will affect this situation.)
In the case of Rc<T>, cloning the Rc creates a second owner of the heap allocation, so the Vec dropping its copy won't deallocate. Though I believe one complication to this would be that the lifetime any references created through the Vec's copy of the Rc will be linked specifically to that copy, so would result in a borrow check error when it's dropped.
If the Vec is storing references, you can borrow the thing behind the reference and still mutate the Vec because that reference stays valid even if mutating the Vec drops its reference to the item.
let (a, b, c, d) = (1, 2, 3, 4);
let mut v = vec![&a, &b, &c, &d];
// If you change the type here to &&u32, or let the compiler infer the type
// then you'll get a borrow check error because the outer reference
// borrows the Vec.
let borrowed_b: &u32 = &v[1];
v.remove(1);
println!("{}", borrowed_b);
println!("{:?}", v);
https://play.rust-lang.org/?version=stable&mode=debug&editio...[1]
auto& r = *some_vector[0];From memory:
In MSVC, dequeue will allocate memory for every single element if your elements are > 8 bytes. This will never be changed, due to ABI compatibility.
Clang and gcc have batching sizes of 1K and 4K (i.e. you throw out a whole page of memory even if your dequeue contains only 1 element).
https://github.com/gcc-mirror/gcc/blob/47749c43acb460ac8f410...
Furthermore, depending on the size of T, the performance penalty of the extra heap allocations is amortized over the cost of resizing the vector. That is vector reallocation is significantly faster for a unique_ptr<T> than it is for T when T is large and almost all memory allocators are tuned to allocate objects close together in space when they are allocated close together in time, so you don't lose the cache locality or need to worry about memory fragmentation.
(Though it's still pretty brittle, so you may want to add a ton of comments to warn yourself in the future...)
I'm not going to pretend that I understand everything on that site (particularly anything about complex templates and things like SFINAE) but often there's comprehensible stuff in there. It can be really helpful.
The big issue is that becoming involved in the C++ standardization process is purposely arcane, requiring people to physically travel to remote locations, miss time off work, and spend a lot of money. There are literally substantial language features added to C++ that are nothing more than the work of maybe 3-4 people. These features could have benefited enormously from having 100s, if not 1000s of potential developers exercising use cases and contributing suggestions, but the standardization process is heavily gated.
9/10 times when there is resistance to a particular feature it's because implementing it breaks the ABI.
> requiring people to physically travel to remote locations, miss time off work, and spend a lot of money.
It's a different kind of barrier to entry, but I would be surprised if the attendees of the standards meetings aren't being compensated for it.
In this context ABI breaking changes are not viable due to the proliferation of pre-existing compiled binaries. What the standard says is only half the story...
That is why I can code on the go in C++ on my travel netbook, while I mostly use Rust when plugged, or have to do a full build before leaving.
1. Backwards compatibility with already existing binaries? 2. Speed speed speed? 3. Ergonomics?
The problem is similar in C as well.
`printf("%d, %d", i++, i++);` will give you different results depending on the compiler.
[0] https://stackoverflow.com/a/7693775/
[1] The final bullet-point in the Notes section of https://en.cppreference.com/w/cpp/memory/shared_ptr/make_sha...
[2] https://stackoverflow.com/questions/38501587/what-are-the-ev...
struct Dummy {
Dummy(int) {}
};
struct World {
World(const Dummy &) {}
void hello() {}
};
int main() {
int value = 42;
World world(Dummy(value)); // fails to compile
// World world(Dummy(42)); // works
world.hello();
return 0;
}In 99.9% of cases, it's just a surprising compiler error message that you learn to avoid. However, there's a related version of this problem which can lead to accidentally declaring a function prototype rather than acquiring a lock. You'll get compiler warnings from -Wshadow, but the seriousness of the problem might not be obvious.
That said, it's a very rare problem these days, due to the introduction of uniform initialization in C++11.
World world(Dummy(value));
Can be interpreted as the following function declaration: World world(Dummy value);
As mentioned in child comment. This is sometimes called the most vexing parse: https://en.wikipedia.org/wiki/Most_vexing_parseIt originates because C++ allows superfluous parentheses around function parameter declarations.
Basically, the lifetime aspects of r/l-values are improperly coupled to the "can I scavenge the internals?" aspect (via const). R-values should not have been promoted to const-ref (I think it was a legacy behavior?) because then this const-ref can be passed around (e.g. returned) as if it were going to outlive the original reference's frame!
This was the first time I realized what a mess C++ can be (although I still use it a lot). This is a subtle error that I have seen seasoned experts miss in code review (especially in templated code). You can't work around it like you can the underperforming parts of the stdlib (Abseil and Folly help out a lot!). This promotion choice of auto&& -> auto const& is really baked into the language and I don't see a path to change it.
The other "biggest foot cannon" has gotta be the incredibly subtle ways one can violate the ABI, e.g. executable A linking against libs B and C, each of which bundle an ABI-incompatible implementation of the same parent dep D (versions 1.1 and 2.1). You never know which type of object you're really passing around with D::SomeType!
[0] https://quuxplusone.github.io/blog/2020/03/04/rvalue-lifetim...
why is it always C++ people that come up with those dramatic-sounding names
Not just clothes can be fashionable; vocabulary, too.
> When a function is called, each parameter ([dcl.fct]) shall be initialized ([dcl.init], [class.copy], [class.ctor]) with its corresponding argument. [ Note: Such initializations are indeterminately sequenced with respect to each other ([intro.execution]) — end note ]
So it's only about initialisation of parameters from arguments, not to the evaluation of arguments. (For example if foo() takes a std::string parameter, and bar() returns a const char*, then in the expression foo(bar()) the above quote refers to the call to the string constructor, not to the call to bar().)
The more relevant part is 5.2.2/8:
> [ Note: The evaluations of the postfix expression and of the argument expressions are all unsequenced relative to one another. All side effects of argument expression evaluations are sequenced before the function is entered (see [intro.execution]). — end note ]
Herb Sutter disagrees. [0] The C++ legalese you've quoted seems decisive, unfortunately Sutter's post doesn't mention it.
See also [1][2].
edit quietbritishjim beat me to it, and with a better informed comment to boot.
[0] https://herbsutter.com/2013/05/29/gotw-89-solution-smart-poi... (ctrl-f for known, and see also the comment thread at the bottom of the page)
That one blew my mind when I found out. You can rip off a chunk of an object by accident... and nothing happens whatsoever. There's no warning from the compiler.
You can stuff a Derived into a std::list<Base>, it'll work just fine, and if Derived happened to say, hold a pointer, it'll happily vanish into the ether.
Besides the amount of confusion that can ensure from this, it means you can have problems if you want to introduce inheritance into a place that didn't have it before.
Especially if it's some sort of external component. You think you're clever inheriting from a framework class and adding some data on top? Nope, the internal structure you don't control doesn't want to cooperate with that plan.
Slicing is a bad name for this phenomenon, because what you've described is what the name implies, but that is not what is happening. The original object is completely unchanged. Nothing was ripped off of it. Constructors create new objects.
You can use clang-tidy to warn / error on that: https://clang.llvm.org/extra/clang-tidy/checks/cppcoreguidel...
What is OpenMW? click link. No info. Oh, FAQ.
> "What is OpenMW?"
"OpenMW is a free, open source, and modern engine which re-implements and extends the 2002 Gamebryo engine for the open-world role-playing game The Elder Scrolls III: Morrowind."
Maybe, just maybe, that shouldn't be a FAQ but right on the main page.
Louis Brandy's Curiously Recurring Bugs at Facebook presentation from CppCon17 has somewhat of the same feeling, though it only covers a few bugs: https://youtu.be/lkgszkPnV8g
i think they should add some template argument to determine if an atomic counter is used or not, exactly for the cases where the shared_ptr object is used exclusively from a single thread; but they didn't do that in the standard.
A reasonable solution in my case could be to use std::weak_ptr<> instead but that wouldn't be useful here.
How so? Do you mean if you do exactly what the article says when initialising the capture variables? Or something lambda specific?
On reflection, I suspect the parent comment was referring to creating circular dependencies using lambdas and std::function.
For example, a class Inner wants allow notifying its owner when events happen, so it has a std::function member variable it uses for the callback function, and a class Outer has an instance of Inner as a member variable. But then passes as a callback:
m_inner.set_callback([self=this->shared_from_this()](Param p){ self->handle_inner(p); });
Now Outer owns Inner (directly) and Inner holds a shared_ptr to Outer, so Outer will never be destroyed (and therefore neither will Inner). A.operator=(B)
Hardly any different that knowing what func(A, B) does.First you need to figure out the types of A and B and their "modifiers" (like const or references) to start figuring out which operator=() could potentially be called. That might still be very hard to do if for example there are implicit conversions from B to some other type involved. Such conversions can depend on code which might be nowhere close to where the operator=() is defined.
Or if we take an Algol derived language, like Pascal or Modula-2, func might have been defined as,
type
my_complex_type = ....
procedure func(var a, b:my_complex_type);
begin
end;
Raising the same kind of questions.Yes there are issues with C++ approach as you describe, however sometimes it seems C++ hate kind of blinds the people that similar issues occur in other languages to some extent.
I understand the practical reasons for it, but it feels very wrong.
I've not encountered a single instance of slicing in 10+ years of C++.
Jesus christ, what an utter mess of a language.
Also some of the C++ horrors are caused by the consequences to stay copy paste compatible with said documents and tooling across all platforms where a C compiler exists.
The C++ spec is horrendous, even when implemented 100% correctly.
Go read some WG14 stuff then come back to us.
You are a absolutely exhausting person to talk to. You stand out in every single conversation on this site that you take part in as being really pointlessly hostile and unpleasant.
This is really adding nothing whatsoever to the conversation other than making people dislike you.
If you do, here is a tip for next time, be more constructive than "horrific mess", and understang that a big deal of that mess is a consequence to bundle C as part of the package.
void frob(std::shared_ptr<T>);
main() {
frob(std::make_unique<T>());
}If something takes a foo, give it a foo.