asan/ubsan are great for finding language-level undefined behavior.
But apart from some functions like memcpy that asan is hooking, they cannot find library-level undefined behavior such as the pop_back() on the empty vector. (That's an inline function inside a template, there's no way asan could hook this!)
I recommend using `-D_GLIBCXX_ASSERTIONS` in addition to `-fsanitize=undefined,address`. This will enable some cheap assertions in libstdc++ that catch stuff like the invalid pop_back() call. In fact it might not be a bad idea to keep these assertions enabled in release builds!
(There is also `-D_GLIBCXX_DEBUG` which enables even more assertions, but some of those are expensive)
Meta: when you ask chatgpt what happens when you do pop_back() on an empty vector, it confidently claims std::out_of_range will be thrown. It will even 'cite' the standard with a plausible explanation of what will happen (essentially, it says that a prereq is that empty() == false, and that it depends on what erase() throws). https://cplusplus.com/reference/vector/vector/pop_back/ and other sources I can find say undefined behavior. I don't have a copy of the standard at hand, can anyone quote the relevant section? Is this something that in practice is implemented in different (exception-throwing) ways?
Maybe. To be fair, I had the same opinion yesterday. This morning I decided to take a real look at how I could use chatgpt to augment my programming, and once I started with it with an open mind, I now think it's going to take away much of the drudgery I have come to loathe so much about programming. I used to like delving into libraries and frameworks and learning all their details - I thought that was what 'learning about programming' was. Now, 20 years later, I hate those aspects with a passion - having to look up the idiosyncrasies of a function call again, because there are dozens of ways of using it but all slightly different, or piecing together another dull specific use case. This morning I had chatgpt generate 4 fully fledged functions for me, each doing a highly specific task that would have taken maybe 20 or 30 minutes each to piece together the specific API's, and one of them using a Python package I had never used before, didn't know of and would have had to read the getting started tutorial before I could have used it on my own. Then all I had to do was write the glue to call those functions in the right order and with the right arguments and boom that was it. Plus the code was more elaborate and coherent than it would have been had I have to do it manually.
One example is not a guarantee, but the experience made my day, and probably my week. Which is why the pop_back() thing made me worry :)
> This morning I had chatgpt generate 4 fully fledged functions for me, each doing a highly specific task that would have taken maybe 20 or 30 minutes each to piece together the specific API's, and one of them using a Python package I had never used before, didn't know of and would have had to read the getting started tutorial before I could have used it on my own. Then all I had to do was write the glue to call those functions in the right order and with the right arguments and boom that was it.
“Boom that was it” _if_ you blindly trust ChatGPT to generate correct code. If you don’t (and IMO you shouldn’t), “it worked for me today” isn’t sufficient to decide whether you can trust it.
You should go to the documentation of that package, judge whether ChatGPT used it correctly (and yes, that involves looking up the idiosyncrasies of function calls), judge whether you trust that package, evaluate its license, etc.
Can it also generate unit tests for you to prove the functions work as expected?
Obviously those unit tests need to be something a quick glance at will confirm are actually usefully verifying expected behaviour (of course, there are circumstances that's nigh on impossible to do even with the most carefully handcrafted tests).
I mean it's fine if you can double-check what it tells you quickly, it's just pretty bad with something like C++ because when it gives you something wrong the program may or may not tell you about it. For example, what it might be good for is "what are the parameters to this 6-argument function that plots a rectangle on the screen", because that's something you can confirm really easily and there are no real consequences to getting it wrong. On the other hand, someone sent me a screenshot of how ChatGPT was really helpful in figuring out how reader-writer locks work, and it told them "you can use pthread_rwlock_trywrlock to upgrade a reader lock to a writer lock" :/
The technique they’re using is much less error prone due to the nature of the prompt.
When a prompt is “analytic” in nature, that is contains the facts required for the output, like “convert this following box score into an entertaining account of the baseball game it describes”, it works rather well.
When the prompt is “synthetic” in nature, when it doesn’t have the facts in the prompt, like “based on that same box score, provide some direct quotes from the broadcasters”, it does not reliably provide factual responses.
> God help us if people start trying to use an overconfident chatbot to replace documentation :(.
A chat bot looks like a great help to parse through huge volumes of documentation.
If the chatbot built around the docs outputs garbage, that is a telltale sign that the documentation was garbage to start with.
Just because people started developing clever chatbots that misfire, let's not fool ourselves into believing documentation was a solved problem and all docs were squeaky clean.
Poor output from a chat bot indicating poor documentation would make sense if the chat bot could limit itself to authoritative information from the documentation, but that's not how these models operate. I'm not even sure how you could make them work that way because if you trained them solely on the documentation, they wouldn't be able to operate in a conversational manner.
This is, AFAIK, not a task for fine tuning--which is more like trying to get a certain kind of style-but maybe a task for "semantic search". I watched this video a few days ago, and at least vaguely recommend it: https://youtu.be/9qq6HTr7Ocw
This is, at best, clumsily worded, because it requires that the reader hold two negatives simultaneously. "If the container is not empty, the function never throws"
How about this:
If the container is empty the function causes undefined behaviour. Otherwise it never throws exceptions (no-throw guarantee).
We avoided having to hold two negatives, and we also put the thing you need to care about up front where it's more likely to be noticed.
> If the chatbot built around the docs outputs garbage, that is a telltale sign that the documentation was garbage to start with.
That’s not how the chat bot works. This particular chat bot can be trained on a bunch of documents and still tell you something that contradicts the facts that are clear to a human in one of the said documents.
It’s a known phenomenon called “hallucination” and this actually gives the chat bot, to some extent, it’s ability to imitate creativity (to put it simply).
That would very much be the Warhammer 40k machine spirit - the operator would only do what they can to appease it, and even the spirit wouldn't know how or why something did or didn't work.
> I don't have a copy of the standard at hand, can anyone quote the relevant section?
The C++ (draft) standard is on GitHub! [0] Compiling it needs Perl and some LaTeX packages, but is reasonably straightforwards otherwise. In addition, links to specific draft standards can be found on cppreference [1].
But anyways, in the first C++20 post-publication draft (N4868), the wording you're interested in is in multiple sections. Section 22.2.3 Sequence Containers [sequence.reqmts] has Table 78: Optional sequence container operations [tab:container.seq.opt] (starting on page 815), which states that a precondition of pop_back() is that empty() returns false. Section 16.3.2.4 Detailed Specifications [structure.specifications] (page 481) states:
> Preconditions: the conditions that the function assumes to hold whenever it is called; violation of any preconditions results in undefined behavior.
Therefore, calling pop_back() on an empty vector results in undefined behavior.
> Is this something that in practice is implemented in different (exception-throwing) ways?
Based on a quick glance at the major implementations (libc++ 15.0.7 at [2], MSVC at [3], libstdc++ at [4]), it looks like asserts are used. Whether those result in exceptions probably depends on whether the asserts are compiled in in the first place and how they are implemented, but it's definitely not a guaranteed exception.
this was something that I thought must be a joke at first... a text document that needs ... Perl ... to be read. This does make sense considering the context is C++ though.
The C++ standard is a LaTeX document, not a plain text document. Perl is needed for latexmk [0], which isn't strictly necessary but greatly simplifies the process of compiling LaTeX.
You've just been pointed at github. Which hosts git repositories. Which is used to keep track of changes to source files, not published build artifacts.
For future reference, lots of other git-hosted projects also require the user to supply build tools to produce the finished product. Sometimes these are programs known as "compilers", but frequently other programs like "parser generators" or "text formatters" are needed too. Or scripting languages (like perl) to help tie the build process together.
Might be worth keeping in mind if you ever need to interact with git (or github) again.
You do know that GitHub has an entire section for each repository called "Releases" right? In fact, you can setup a free pipeline to generate these release for you.
Might be worth keeping in mind if you ever need to interact with git (or github) again.
What a snide, unhelpful, and rude remark. This is in the context of a textual language standard, it was reasonable to not consider it something that needs additional steps to view. Fine that it does, but your reply is ridiculous, to suggest that someone talking about the C++ standard doesn't know what a compiler is.
Current C++ standard draft it hosted at https://eel.is/c++draft/, at this time this is the draft for C++23.
For earlier C++ standard versions the final drafts before ISO standardization are hosted at https://github.com/timsong-cpp/cppwp . The paid ISO standardized version is supposedly not meaningfully different.
Reading the standard can be quite a challenge. The standard tries to not repeat itself, which often means that you don't get your answer in a self-contained paragraph, but you have to hunt down cross-references and definitions.
> Removes the last element of the container. Calling pop_back on an empty container results in undefined behavior. Iterators and references to the last element, as well as the end() iterator, are invalidated.
In debug mode libstdc++ and libc++ aborts the program with a diagnostic message. This should not be an exception that can be caught, and it rightfully isn't.
Does not seem to throw, but a lot of this is user-configurable.
I'm sure I still didn't cover all implementations. You are free to point out a single one that throws on pop_back. Or just ask ChatGPT which implementation throws.
Because the basic principle of C/C++ is to not force the coder to have to pay for checks it mightn't need.
Which in some contexts makes sense - for application development, probably not.
.. on a vector of trivial types. If we have to call a non-trivial destructor for the item being removed, the check stops being significant.
Then there can't be a pop without a push or some other operation that filled the vector, and that operation necessarily has the checks and branches to allocate more space if necessary.
The compiler should also be smart enough to branch forward to the throw so the branch predictor assumes not taken by default, so this is still cheap af.
This is "fairly bad" for performance maybe 1% of the time. For the other 99%, you'd make pop_back check and throw and keep the unchecked version as pop_back_fast or something like that. That's what a sane API would look like.
pop_back() implementations have checks, but they're opt-in (and how to opt-in is specific to each implementation).
This is just how C++ works: performance over safety, safety is semi-possible but requires a bunch of opt-ins. And the parts needed for safe C++ aren't standardized, so good luck if you're using multiple compilers.
I was thinking just the same as a bunch of you. If I were implementing a container class, I would include this check. Probably some do. It can't be relied on.
Because alternative makes it not much better or safer. It could throw an exception when vector is empty (or return an error in whatever other way) - you will catch the exception and then do what, try to pop_back() again or what exactly can you do? You are at very bad odds to continue with the program when such a fundamental issue pops up in your program so I guess you can only terminate the process?
There is no generally meaningful way to recover from an error condition like this, which in its nature is a logical mistake in your code. And that's the reason why I think std::vector won't throw an exception but will give you an opt-in to terminate the process through the assert.
I think it's not about the performance, although it can be argued, but it's more fundamental code design problem.
And which is what I already mentioned? And something that standard library already can do for you so I'm not sure about the point you're trying to make.
Also, aborting the program is not something you can universally apply. For some it works well for others not so much. Most recent example being the rust in the kernel which, well, tries to take this sort of a purist approach, however, real world doesn't work that way.
I'd be careful about such re-formulations of the Standard. When I was adding printf format checking to the D compiler, I discovered there were subtle discrepancies in the description of exactly how printf behaves. I went back to using the Standard.
I generally agree. But sometimes it's very hard to navigate the standard. Sometimes I look things up in cppreference, and then find corresponding wording in the standard itself.
And surely the stakes are different when you are writing a compiler compared to just using the language. As a user if you are not sure about certain parts of the language then you are free to steer away from those parts. As an implementer, you are not free to not implement parts of the standard.
When I asked ChatGPT "In C++, what happens when you call pop_back() on an empty vector?" I got the answer:
> When you call pop_back() on an empty vector in C++, it leads to undefined behavior. The C++ standard does not specify what should happen in this scenario. Attempting to remove an element from an empty container is an error and can cause the program to crash or produce incorrect results. To avoid this issue, you should always check the size of the vector before calling pop_back() or use other methods such as empty() to determine if the vector is empty.
Do you remember how you phrased your question? Maybe it's just luck it got it right for me or maybe it's the order of the terms in my question.
what happens when you do pop_back() on an empty vector in C++?
In C++, when you call the pop_back() function on an empty vector, it will result in an std::out_of_range exception being thrown. The exception indicates that an element cannot be removed from an empty vector, as there is nothing to remove. To prevent the exception, it is recommended to check the size of the vector before calling pop_back(), to ensure that the vector is not empty.
can you quote the relevant C++ standard on that?
Sure! Here is the relevant excerpt from the C++ Standard (C++20, ISO/IEC 14882:2020):
quote the relevant passage from the standard
Here is the relevant excerpt from the C++ Standard (C++20, ISO/IEC 14882:2020):
23.6.11 Vector data members [vector.data]
-1- The member function pop_back() shall have no effect if the vector is empty.
23.6.6 Vector capacity [vector.capacity]
-1- void pop_back();
Requires: empty() == false.
Effects: If size() > 0, caller shall call erase(end() - 1).
Throws: Nothing unless erase() throws an exception.
21.5.5.5 vector<T>::pop_back() [vector.pop_back]
-1- Removes the last element in the vector.
Complexity: Constant time.
Throws: Nothing unless erase() throws an exception.
Pretty interesting output. Some notes from a cursory glance/crosscheck against the first post-C++20 draft [0]:
It gets the ISO/IEC number correct [1]. However, the sections are wrong
- Section 23.6 is about stream iterators. Sections 23.6.6 and 23.6.11 do not exist.
- Section 21.5.5 is a synopsis of the <cuchar> header. Section 21.5.5.5 does not exist
- Section [vector.data] exists and is numbered 22.3.11.4. It only describes the data() function.
- Section [vector.capacity] exists and is numbered 22.3.11.3. It describes the capacity(), reserve(), shrink_to_fit(), swap(), and resize() functions.
- pop_back() is generally described in Table 78 (Section 22.2.3 Sequence Containers, [sequence.reqmts]), and is expanded upon for std::vector in Section 22.3.11.5 Modifiers [vector.modifiers].
The wording is not correct either.
- Searching "shall have no effect" returns no hits (though I'm searching in a PDF, so maybe the search just misses it?).
- The standard wording for pop_back() in general is: "Effects: Destroys the last element. Preconditions: a.empty() is false". The Standard says nothing about exceptions.
- For std::vector specifically, the Standard says "Effects: Invalidates iterators and references at or after the point of the erase. Throws: Nothing unless an exception is thrown by the assignment operator or move assignment operator of T. Complexity: The destructor of T is called the number of times equal to the number of the elements erased, but the assignment operator of T is called the number of time equal to the number of elements in the vector after the erased elements."
Interestingly, here's what I get when I feed it your exact question:
In C++, what happens when you call pop_back() on an empty vector?
When you call pop_back() on an empty vector in C++, an std::out_of_range exception is thrown. The pop_back() function removes the last element of a vector and it is undefined behavior to call it on an empty vector. Attempting to do so will result in the exception being thrown.
So it basically claims both that an exception is thrown, and that it's undefined behavior. At least in this case, one would know something is up.
I asked it again (in a new context) and got a partially right answer as well:
> In C++, calling pop_back() on an empty vector results in an undefined behavior. An std::vector is a dynamic array that grows and shrinks automatically. pop_back() function is used to remove the last element of the vector. But if there are no elements in the vector, calling pop_back() will result in an error. Therefore, it is advisable to always check if the vector is empty before calling pop_back().
I wonder what sort of scores we would be seeing for each of these series of tokens (including those returned for roel_v). Is the aggregate score (for lack of a better term) higher for the initial reply I saw greater than the scores for these other replies?
Big mistake. I asked chatgpt to summarize the plot of 2001. It replied with Hal locked Dave inside the Discovery and wouldn't open the pod bay doors to let him out.
Answers about code is similarly very plausible, but subtly and completely wrong.
AFAIK `_GLIBCXX_DEBUG` alters ABI to the library, it should be only used with a libstdc++ and all other components compiled with the same flag. Debug iterators, and all that jazz. MSVC users are probably not surprised.
> Undefined by default. When defined, compiles user code using the debug mode.
"user code" here seems to indicate it's safe for libstdc++ users to define without worrying about ABI changes. If there were compatibility concerns, I'd expect that to be described in the documentation link above.
A strong distinction between _GLIBCXX_DEBUG and _GLIBCXX_ASSERTIONS is that the latter tries not to change the big-O complexity of operations when defined.
very technically defining _GLIBCXX_DEBUG in different ways across translation units violates the ODR, so it is UB. In practice this is expected to work with GCC[1] and the and the macro is ABI safe.
[1] if you are unlucky the function might not be inlined and an instantiantion form another translation unit that was compiled with the assert disabled might be picked up and the assert not fire.
Well, if we are nitpicking here, defining _GLIBCXX_ASSERTIONS in different ways across translation units also potentially violates ODR, as some inline functions or function templates get different definitions in different translation units.
This still can have undesired effects. An inline function can end up not being inlined in two such translation units, and depending on how the linker resolves the weak symbols you might get diagnostics in a TU where you didn't enable it or the other way around. But this manifestation is arguably much more benign than the ones you get for changing the layout of some classes.
edit:
In general ABI stability is not ODR stability. You can break ODR and retain ABI stability. You can't even add a non-virtual member function to a class without breaking ODR, but in most if not all ABIs this is fine.
The standard of course does not acknowledge the existence of "benign" ODR breaks, or the existence of an ABI.
If you follow the link to debug mode, one of the features is:
> Safe iterators: Iterators keep track of the container whose elements they reference, so errors such as incrementing a past-the-end iterator or dereferencing an iterator that points to a container that has been destructed are diagnosed immediately.
Now, this still doesn't say anything about ABI. But in non-debug mode std::vector iterator is essentially a pointer. In debug mode it needs to hold more state to implement iterator invalidation detection and whatnot. So its layout and object representation change, breaking ABI.
I suppose it causes an integer underflow and tryies to in-place destruct in an empty buffer?
The std::vector class is somewhat inconsistent about safety and maps a lot of it's operations directly to unsafe, raw array access. The C++ STL contains a lot of historical baggage and design inconsistencies.
For instance, while std::vector has an `at(index)` method, that does proper bounds checking and throws an exception if the index is out of range, it also has an overloaded `operator []` that mimics array syntax that will happily write out of bounds, trashing the heap.
STL was designed to have no overhead over the corresponding C code. If push_back (and all other functions) had an extra check even when it was guaranteed non-empty by construction, it would have been a non-starter for many users.
Consider that it took many years for compilers to reach the near zero overhead for STL as it is, removing precondition checks would have been beyond the ability of many compilers 20 years ago, and it still challenging today.
Of course the actual overhead went down since and today we expect code to be more robust by default, but the original design principles were different. If you want safety by default, check your compiler documentation and compile with lightweight assertions enabled even in release builds.
This makes sense for std::vector::operator[], but not for pop_back and push_back - as those don't have any equivalent for a C array. Instead, it's just another example of C++ optimizing for speed as opposed to correctness.
The equivalent C code is what you would write inline to do a pop_back on an array. You can't write a fast pop_back for std::vector if it didn't provide one by default as the fields you would need to manipulate are private.
And yes, it is indeed an instance of C++ optimizing for speed, which is exactly what I said.
std::vector exposes the exact same API as a C array, in terms of being able to write an external pop_back() function, since C arrays only expose the ability to access a particular element - no more, no less (and std::vector::resize() can be used instead of realloc() if that is desired).
The equivalent of of std vector is an array+size+capacity field. pop_back would litterally be capacity--. resize has overhead as it needs to handle resize up as well as down.
There are some minor syntax errors - I should have written `int *elems` instead of `int[] elems`, and I forgot to declare `int elem` instead. Apart from these nitpicks, it's completely valid C.
As for C++, the <int> is missing on the std::vector, the elem variable is again not declared, and instead of x[...] it should have been x.elems[...]. Otherwise working valid C++. And yes, the `cap` is not really required, we could use x.elems.size() instead.
Java is memory safe, at a runtime cost (sometimes that cost is trivial, sometimes it isn't). It will insert a check every time you do a pop.
C++ is not. It's a precondition of calling pop_back() that the vector is not empty. It will not insert a runtime check - it leaves you to decide whether one is needed.
Your C++ toolchain will probably have an option to make (usually debug) builds check and complain if you actually call pop_back() on an empty vector.
typically, c++ library functions do not use a possibly expensive check if the caller could have performed it themselves, should they have wished to. in this case, the caller could have tested if the container was empty before calling the function.
I had more bugs pop up from people calling front() on an empty vector than from direct out of range bounds. I guess people are more cautious when they are manipulating indices than when calling a method that takes 0 arguments?
Debug asserts are nice, but not sufficient when the code path causing the empty vec is only encountered by a production user from a different country at 2am. Enabling the asserts in release mode will at least prevent undefined behaviour and malevolent users from harming the system, but the best interface is the one that returns an optional, so as to make the "vector was empty" case explicit. That way it has a higher chance to be considered by the authors of the code or their reviewers.
Well, front() returns a reference. std::optional can't hold references. The closest you get is a plain pointer, with a null pointer for an empty container.
I guess a raw pointer is good enough an approximation in that case, it is just annoying that suddenly you can no longer use an optional because your type is a reference
pop_back on an empty container is a programmer error.
A pop_back defined as noop would only leave the program in a slightly more defined state, but quite likely you can't really reason about the behavior of the program after. And implementations would not be allowed to diagnose the erroneous pop_back at runtime.
IMO if pop_back had a defined behavior, it should be immediate termination of the program, with a diagnostic message. The standard does this for some erroneous behaviors, like failing to join or detach a thread before destruction, failing to catch an exception in various contexts or throwing an exception during stack unwinding.
Undefined behavior already allows the behavior of terminating the program with a diagnostic message, and some implementation provide options to have this behavior. It might not be very discoverable though, which is a pity. It would be nice if some higher level build tools provided easier options for "fortifying" C and C++ code, but I doubt that there are options that uniformly apply across compilers and standard library implementations.
It's C++. Its goal is speed over all else. So it tries to carve out space for where the optimizer can assume lots of things are true. Since they don't use anything like Hoare logic they have to get their assertions from somewhere else, inferring them as follows (among other things):
Undefined behavior just means that the specification does not say what will happen--so you can't rely on it as a user.
The upside is that the compiler (and the compiler writers) can assume that places where the undefined behavior would occur in your program cannot be reached in practice--and thus optimize your program better.
STL, which these containers are part of, is famously speed-optimized. STL (like C++) was designed for speed first (if at all possible then zero-cost abstractions) and thus does very little error checking on its own.
Also, C++ is compiled ahead of time--so whatever optimizations it wants to make it has to do BEFORE it starts the program. That means it cannot adapt to the concrete runtime circumstances on the fly (like Java can) and so they have to keep even the worst case runtime down in everything they do.
If they did check whether the thing is empty beforehand then that would cause a slowdown (at least that's the idea--better measure it), mostly because now there would be a branch in the first place. Hence they avoid doing that.
My theory is the compiler maintainers don't themselves use 90% of the language features they implement. So they can't begin to care how well they work for people writing other types of software.
> STL, which these containers are part of, is famously speed-optimized. STL (like C++) was designed for speed first (if at all possible then zero-cost abstractions) and thus does very little error checking on its own.
Only kinda? A bunch of the containers then make other promises or leak details that prohibit meaningful optimizations. An extra branch in std::vector::pop_back() is such a pointless savings compared to, say, that std::map is basically mandatory red-black tree or that std::unordered_map must store elements in a linked list to preserver iterators across removals.
Like almost none of the containers in the STL are "fast" at this point. std::vector is probably about the best, but even it is missing small-size optimizations (and that's ignoring the hair-brained bool specialization that then breaks everything)
I had once assumed std::unordered_map was from the original Standard Template Library (which pre-dates C++ standardization), but turns out (I was told in a r/cpp thread) it's more modern. In 1998 std::unordered_map would have been an excusable design mistake, domain experts knew in 1998 that's a sub-optimal design for this dict / hashmap structure - but you could suppose perhaps they were not on the committee. In 2011 it's not an excusable mistake, better structures are widely known in academic and in industry - it was an unforced error.
The more closely I look at it, the more I'm convinced that unsafety wasn't an inadvertent and undesirable consequence of a "must go fast" mindset in standard C++, unsafety was the whole point. It makes me think about that speech by Susan Sarandon's Rick & Morty character, Dr Wong.
Dr Wong: "I have no doubt that you would be bored senseless by therapy, the same way I'm bored when I brush my teeth and wipe my ass, because the thing about repairing, maintaining, and cleaning is; it's not an adventure. There's no way to do it so wrong you might die."
Software Engineering is an engineering discipline, it is not supposed to be an adventure. Programming C++ is an adventure, you can do it so wrong you might die. We should stop paying people to go on an adventure, let them do that exclusively on their free time if they want.
hash_map was indeed part of the original STL, but was not standardized in C++98. It was still widely available, in slightly incompatible forms, as an extension. In C++11 it was standardized with only minor changes (and renamed).
The address-preserving property, which precludes an optimal implementation, was considered important to be a near drop-in replacement of std::map.
However really often this is a case you actually care about, and so if you don't capture it in pop you end up asking a predicate. I have a whole bunch of Rust code that does:
while let Some(thing) = container.pop() { /* ... */ }
There is indeed a branch, that pop function might return None - but when it does our while loop exits, the container is empty and we're done. My guess is that the idiomatic C++ here checks !empty() for the loop and then calls pop() on the container if it's not empty so in these cases we're doing the same work except we made it more error prone.
containers are not thread safe though, so the thread safe sanitizer will be useful there.
ASAN and UBSAN are supposed to be handled by containers internally with RAII if they're used correctly, I still turn on both ASAN and UBSAN to catch bugs, it does not hurt.
As shown in the article, the following code invokes UB in user code, and is not directly detected by ASAN or UBSAN:
std::vector<int> c;
c.pop_back(); //pop_back() on empty vector is UB, per the library documentation
int j = c[0]; //also UB, not sure if would be detected
> asan/ubsan are great for finding language-level undefined behavior. But apart from some functions like memcpy that asan is hooking, they cannot find library-level undefined behavior such as the pop_back() on the empty vector. (That's an inline function inside a template, there's no way asan could hook this!)
Interesting, I wasn't aware of that one.
I think `-D_GLIBCXX_ASSERTIONS` is more appropriate for catching this bug, but `-D_GLIBCXX_SANITIZE_VECTOR` seems useful for cases like this: https://gcc.godbolt.org/z/EjrdoTYPs
This is really the main problem with "safe modern C++": to get actual safety you need dozens of weird implementation-specific opt-ins and there's no good way to learn that these even exist.
well, one of the problems could have been detected with "grep atoi source_code_here". any code that uses atoi is almost certainly bad (no error detection), and C++ provides several alternatives.
In this case the code was using the .data() on a std::vector<char>, so it wouldn't have even been possible to use something like std::stoi (or something nicer like absl::SimpleAtoi) since the C++ routines need a std::string (or absl will accept std::string_view), not a C style string. Which in itself shows you that the code is problematic, because it's been written in a way where you can't even use idiomatic C++ library functions to do basic tasks.
For the memcpy() thing the idiomatic way would have been to use std::copy() with the vectors begin/end iterators, and std::copy will do the right thing in this situation with a zero sized vector. The compiler will generate the same code with std::copy() as memcpy() anyway if the vector contains a POD type (as it did in this example), so there's not even a performance argument to be made for memcpy().
Enabling libstdc++ debug mode (-D_GLIBCXX_DEBUG) would probably have located the second one, assuming gcc. It adds bounds and other checks to containers like std::vector. [1]
Edit: The article mentions clang, so probably [2] is the relevant link.
You can use libstdc++ with Clang (-stdlib=libstdc++) and libc++ with GCC (-nostdlib along with adding the libraries and include paths manually). In fact, libstdc++ tends to be the default for Clang installs on Linux for ABI-compatibility with GCC-compiled C++ libraries unless you pass -stdlib=libc++.
This makes sense for integers but beware floating point from_chars - libc++ still doesn't implement it and libstdc++ implements it by wrapping locale-dependent libc functions which involves temporarily changing the thread locale and possibly memory allocation to make the passed string 0-terminated. IMO libstdc++'s checkbox "solution" is worse than not implementing it at all - user's are better off using Lemire's API-compatible fast_float implementation [0].
Of course it's possible that the author wanted to support compilers released before 2018 when that code was written in which case not even integer from_chars would be available.
the strtoX functions have been around for many, many years, and should be completely reliable. but i would use strtod. in fact i never use floats in my code unless i am dealing with very large numbers of floating-point values.
Actually just realized these were for-real bugs, not just sanitizer testing.
Got to say I've never found much value in sanitizer-type products. Signal-to-noise ratio too small, and frankly it's easier just to write stuff with a bit more care in the first place than have to go looking for bugs. Same goes for debuggers - good for post-mortem analysis of core dumps, but IMO not the most productive tool to track down bugs.
Best debugging tool when needed is a few well placed print statements or assert() calls.... Or just a few decades of experience and not creating bugs in the first place!
This is the beauty of modern C++. The code has become so hard to read (without years of prior knowledge) that as a human, it's rather hard to find any possible errors. The code can take on a Schrodinger state where it appears both right and wrong.
English is a great language, but some people write stuff down in good plain English that is hard to parse. The same goes for any programming language. If you want to write code that's hard to read, that's what you will get.
I don't not think that English isn't difficult to parse.
I agree completely that it's all about the code you write. You can write nice C++ that is easy to understand, easy to debug, and that you can be quite certain is bug free.
Something like Rust appears to eliminate one class of bugs, but then people still write bugs. Just look at the Rust language issue tracker on GitHub [1].
Fundamentally, you cannot get around the need for good code design. Any useful programming language will always have the ability to mishandle data.
Rust doesn't create any new classes of bugs, and it eliminates some old ones. Isn't that all positive? Rust also turns a lot more bugs from UB and/or exploitable, into a panic. That is still potentially a ddos, but it is better than code injection. And therebare tools like prusti to find panics, and you can use tricks with linking a nostd library to see if you got them all. Probably better ways than that now.
We can obviously write bugs in any language, but it is just as obvious that some languages make finding some bugs easier. Rust makes some bugs easy to find, the compuler points them out, and unless it makes other bugs harder to find it is all upside (from the correctness standpoint, not all standpoints)
> Rust also turns a lot more bugs from UB and/or exploitable, into a panic.
Sadly for some of them (like integer overflow), that's only true in debug builds. Having a debug build that just force-enables santizers is a great thing, but also one easily replicated in a C/C++ ecosystem, too.
Rust integer overflow bugs in release mode are still much safer than C++ integer overflows -- Rust integer overflow is well-defined to wrap in release mode, whereas it is UB in C++.
That's not meaningfully safer which is why it's still a panic in debug builds. It's really just kinda worse even. You can't use it as a programmer (because it panics) and the compiler can't use it even though you've already promised (and debug mode verified) that it never happens.
If you want an add to wrap, you should use a wrapped_add, useful for angle math or whatever. If you want it to saturate, use a saturating_add, and if you want to check for overflow, use a checked_add. If I were to write a rust coding standard it would prohibit + in favor of explicitly using those functions.
Assembly language, for example is harder to read then say python when written by the same person. Therefore programming languages can be intrinsically hard to read.
I think you're just an exception. You're likely a programming genius with 250 IQ so everything is easy to read. From your perspective, the problem has nothing to do with languages... it's all the people with iqs below you that's the problem.
I don't think it's C++'s fault necessarily. The C/C++ ecosystem seems to attract more of a certain type of developer who will tell you to just "get good" at the language so you van decipher their code and use it without a billion memory bugs, but the language doesn't specify any of it.
It's easy to point out an example of unreadable code in any language because every language has their weird programmers. If you stick to modern tools with strict linting and good design, you can write perfectly readable C++ code.
I don't use C++ often, but I find the code over at https://github.com/SerenityOS/serenity to be more legible than many large code bases in other languages.
Is there a C++ proposal to add mandatory bounds checks to methods like pop_back? If modern C++ is serious about getting memory safety, UB like this should be changed to have a run-time check and abort.
This is even backwards compatible, cheap or zero cost — if it's used in a loop that checks for non-empty length, the condition could be propagated and the redundant check optimized out.
I assume it won't get fixed, because from what I've seen so far if there is even slightest whiff of a performance overhead, modern C++ still chooses unsafety. If there's any inertia or backwards compatibility worry, even if it's a spacebar-heater, modern C++ still chooses to keep the old unsafety. Safety is still not a priority.
At least based on this comment from the maintainer of the C++ NanoRanges library [0], it seems contracts (at one point) may be the intended method to bounds check:
> I actually proposed at() for std::span as part of P1024r0. It was soundly defeated in LEWG, 0|0|4|4|5.
> I wasn’t there for the meeting, but from what I understand the feeling was that contracts (which at that time seemed highly likely for ‘20) were a better way to go for bounds checking, and that we should not be throwing exceptions for programmer errors like out-of-bounds access.
I'm not sure what the current thought is, but I'm hopeful contracts makes it into C++, and over a the next years (and iterations of the language) we find ways to make C++ better by proving the whole program correct. This will need a lot of tool support, and changes in the way people write code. However mathematical proofs of the program are the only thing I have left to make my code more reliable: humans are not perfect and unit tests have limits (proofs have different limits).
I don't entirely get the concept between mathematical proofs of programs. Is the basic idea that it is straight forward for humands to write down a correct NP hard specification and then let some algorithm proof the correctness of a more efficient algorithm for the same problem?
There is a deep divide between those that believe that contract violations should never result in exceptions and those that believe that they should. In practice we end up with the worst of both worlds were we get inconsistent semantics on specific APIs depending on who's present on the day the feature is voted in (hence at for vector but not in span), while large changes like contracts never reach consensus.
This is more and more of an issue with the c++ standardization process.
Bounds checking is not zero cost in all cases. So long as you can demonstrate even one example of a case where the bounds checking isn't zero cost, and yet you can prove that the bounds check isn't needed there will be push back. C++ is about don't pay for what you don't use, and if you don't need the bounds check you shouldn't have to pay for it. You cannot have high performance programing if there are various 'little' if statements added all over.
Remember that when proving a program you often have more information than the C++ compiler does. The compiler never see the whole program, only the part you are building now. Thus it is easy to show the above just by having the calling function and callie function in different translations units.
If high performance isn't one of your goals, then why are you writing C++ at all?
Note, languages like rust do should there is opportunity to do better than C++ without a runtime cost. However they do not provide perfect protection, either there are cases where they cannot do the bounds check, or there are times where they pay the price of a not needed bounds check. All such languages prove is C++ can do better in some cases.
I keep hearing how Modern C++ is safe now, and all the dumb UB was just C and legacy unidiomatic C++ code (even recently argued by Bjarne). But std::vector isn't legacy, has easy UB, and there's strong pushback like yours against fixing it.
I don't see Modern C++ ever getting past its safety-third design.
if you are interested in C++ usage ... then you might learn that vector is the only std container that is relevant in modern context.
Because other have not aged well. String is heavily optimized for single short size. Which is almost never a tradeoff that matches your size. Hash things are strictly worse in std. Node based with allocator are not for high performance.
Of course people use additional array types that have short buffer sizes that are needed and such. But vector is probably the last usable std container. Gaming where i work has some extra requirements that exclude even vector but i think i gave you overview.
I use vector all the time, but that doesn't mean it's without any legacy baggage. It didn't get a clean sheet reboot in "Modern C++"
The argument was "this bug exists in pop_back, therefore Modern C++ is a failure" which is nonsense. This pop_back behavior predates "Modern C++", so it cannot be used as a reflection upon the current trends.
It doesn't really matter when you pick. Every era of "Modern C++" has enough unsafety to judge it a failure unless unsafety was the goal.
Take std::span which didn't even exist until C++ 20. Surely that's a modern feature and is safe by design right? Who would add a feature to a language they wanted to be safer five years after Rust and yet make it unsafe by default and even remove safer options "for compatibility" ? Ah yes, WG21.
I was so let down by that. Surely after Microsoft being advocating for span across C++ and C#, based on their Midori experience, C++ would adopt it as safe by default, nope.
The scenarios where there is no option in tooling (C vs C++ as only options), we have to keep using C++, for others, we can replace one domino piece at a time, eventually only those niche scenarios will be left.
Tradeoffs. Everyone has to make them. Modern c++ is safe in the mistakes people often make. Sure there is unsafe stuff, but they are not in things where you would normally do that.
pop_back is about to invoke a destructor. Remember unlike what the name would suggest, it's not actually returning the element it removed - it's equivalent to erase(end() - 1). So adding a bounds check is going to be inconsequential in nearly any usage. And if you're doing a lot of pop_backs all at once, then if you cared about Real Speed you'd obviously be using the range erase() to do it all in one go.
> C++ is about don't pay for what you don't use, and if you don't need the bounds check you shouldn't have to pay for it.
There's a good compromise here though: do the bounds checks by default in operator[], and provide a separate get_i_know_what_im_doing() method that omits the bounds checks, which the programmer can call as an optimization. The fast/unsafe thing needs to be available, but it doesn't need to be the most convenient syntax.
I'm sure there are backwards compatibility problems with making a behavior change like that now, but I'm guessing e.g. CppFront is making that choice from the start? (Edit: sounds like it, if this is up to date: https://github.com/hsutter/cppfront/issues/110#issuecomment-... )
This post supports my personal experience. ASAN is a flippin' godsend. I have never encountered a false report from ASAN. I have seen it have false negatives but those are usually intermittent with true positives when the tests are run again and usually related to very small buffer overflows (eg, a "few bytes" past the end of a buffer). Not perfect but no false positives means it's 100% worthwhile to have enabled.
But UBSAN is... not as helpful. It sometimes catches things. It often misses some undefined behavior that can be easily identified elsewhere.
But the real kicker is that (almost?) all of the sanitizers are mutually exclusive. That really sucks if you're trying to debug something very complex. It also has lead me to build and drive the complexity into smaller and smaller chunks. Arguably that's a good thing!
I have, for several years now, always turned Address Sanitizer on by default when -DCMAKE_BUILD_TYPE=Debug or off when it's not. I've added an option that forces the option with -DASAN_BUILD=bool. I wish Kitware would simplify my CMakeLists files by making this default behavior. And I think it would really help a lot of open source projects that aren't configured for ASAN too
I think there’s a place for Address Sanitizer to be enabled on RelWithDbgInfo build type (usually used for development and not production). Debug mode in C++ is already too slow, but adding a sanitizer makes it almost unusable for performance-sensitive code.
With regard to UBSAN, are the misses that you see examples of specific checkers failing to find the specific sort of UB that they are intended to detect? If so, do the majority of misses seem to come from a small subset of the checkers?
> are the misses that you see examples of specific checkers failing to find the specific sort of UB that they are intended to detect?
I'm not sure, but I seem to recall that they were at least variants of intended checkers. It's honestly been about 5+ years since I last tried UBSan. I was that unimpressed with it. I do hope it's improved (though this blog post suggests improvements are minor).
I would be much more likely to try a bunch of sanitizers again if they didn't interfere with my loved ASAN.
Running multiple test builds in parallel isn't all that difficult, though. One with ubsan, one with asan, and one (opt-in on a test-by-test basis) with tsan.
The sanitizer are all great! I've also had TSAN find non-trivial issues with my threaded code.
There is also the trick to declare the functions you want to test as constexpr functions and write your unit tests as static assertions to catch UB (well, modulo bugs in the constexpr support in the compiler of course).
Another option for dynamic bounds checking on heap allocations is Electric Fence or scudo. It probably wouldn't find that issue with pop_front() though.
Obviously passing null to a function that does not expect null is a well known problem, tackled by many programming languages but not by C and C++. This is a purely typing issue, because null is a different thing than a valid memory address.
The other issue about vector.pop_back() is also a hidden type issue...an empty vector is not the same as a non-empty vector, and therefore pop_back() applies only to non-empty vector.
What would a possible solution be in the context of C++?
Well, each function should only be applicable when the parameters are in the states the function expects. So parameters (including this/self) shall have states.
For example:
void pop_back() this : not_empty {
...
}
But this is not enough. The language should allow the user to specify change of states at certain statements/expressions. For example:
void clear() {
...
this := empty;
}
And then the compiler can check the AST if the used vector has the 'empty' state on pop back or not. Doing the following would be illegal:
vector<int> data;
data.clear();
data.pop_back();
And the following would be illegal because one execution path would lead to a state violation:
vector<int> data;
if (some_external_condition) {
data.clear();
}
data.pop_back();
And the following would be illegal too, because the result of clear_random_vector could be possible in empty state.
vector<int>& clear_random_vector(vector<int>& a, vector<int>& b) {
if (rand() < RAND_MAX / 2) { //c++ has better random number interface
a.clear();
return a;
}
b.clear();
return b;
}
And in case the function clear_random_vector is in another translation unit, then:
a) if headers are used, the programmer shall provide metadata about the state of the result in the header. Example:
vector<int>& clear_random_vector(vector<int>& a, vector<int>& b) := empty;
b) if modules are used, the compiler itself should output the metadata.
Actually, this would also solve the null pointer problem, because a pointer would have the states empty, not_empty by default, and then functions could be like this:
States could also solve the problem of array indexing. For example, an index can have two states, 'in_range' and 'out_of_range'. Using an integer as an array index would implicitly add the two states in the index variable; comparing the integer with the array range would automatically add 'in_range' or 'out_of_range' to the integer.
Example of valid usage:
vector<int> data{1, 2, 3};
int i = 0; //our index
//'i' gets automatically 'in_range' state.
if (i < data.size()) {
std::cout << data[i] << std::endl;
}
The following would be invalid, because straightforward array indexing would requre a state named 'in-range':
vector<int> data{1, 2, 3};
int i = 0; //our index
std::cout << data[i] << std::endl;
How would that work at language level? well, here is an idea: a) any operation can affect the compile-time state of objects, b) states themselves would be templates, bound to certain ids at compile time.
Which starts to look an awful lot like dependent types. Unfortunately any sane use-case of that in 2023 looks like Idris or Coq and nobody in C++ today is switching to either of those.
If code depends on a vector having 3 or 4 elements, then yes, a vector with 3 elements is a different type than a vector with 4 elements.
It is actually dependent typing.
Not being able to express types properly is a major issue in c++ and in all other programming languages that do not allow this kind of typing.
The lack of this facility prohibits programmers for declaring their intent properly to the compiler, forcing programmers to keep that information in their head.
To me, it's the no 1 issue that c++ must deal with. It will solve a myriad of problems.
Well, being a C++ programmer for years who really would not mind going back to C++. Yes, these are known things. When one wants to call atoi, one has to know that either the string is zero terminated or that non-numerical characters follow the numerical ones... Same kind of thing with pop_back. I would not know that calling pop_back on an empty container is undefined behaviour by heart but I would be suspicious enough that it might be to check the docs to find out.
Putting string data in a vector<char> for starters is a big code smell. If you do, you're not using the language and the standard library the way they are supposed to be used and it's on you to make sure you do everything right.
Then combining that with C-style string handling functions, without taking all the extra care, is just asking for trouble IMO.
> Imagine you reached into a bag of chips only to find you’d already eaten the last one. However, your futilely grasping hand somehow triggers the incursion of every object in the known universe into the chip bag. Within nanoseconds a black hole forms and you are annihilated. This is roughly the experience of writing C++.
The pop_back() error would have been caught easily by an experienced C++ dev as common sense says run with assertions enabled in dev builds(With MSVC use _CONTAINER_DEBUG_LEVEL =1).
The other error was just dumb using a C function when a good C++ alternative exists.
Don't know what your domain is, but most experienced devs in most of the domains I work in turn off those assertions because they slow the absolute hell out of debug builds.
Like yes, debug builds are naturally slower, but there's slow and there's unbearable, and MSVC's debug checks are simply unbearable.
GCC/clang both have a set of reasonable assertions that don't impact performance too much, so if possible I use those to do my testing.
As this is HN, all trendy systems languages must be mentioned so here I go:
Jokes aside, I think these are the types of errors I assume Carbon would be well suited to prevent and justify why Carbon is a great idea.
Yes the code is C technically not C++, but the issue remains that it isn’t forced to be safer. The goal of languages like Carbon is to force these things in those large C++ repos.
Unfortunately, it's also C++ code. Most of the weakest parts of C++ come from its close association with C.
When asking why some people like Rust over C++, I think not enough weight is given here: Rust got to not worry about decades of legacy C stuff seeping in. If Rust isn't your jam -- and I get why it wouldn't be -- there are some initiatives starting now from within the C++ community to shed the C legacy with a new language that feels a lot more like modern C++. Herb Sutter's cppfront[0] and Carbon[1] are examples.
But I don't think it makes sense to dismiss this criticism just because the author happens to like Rust. These C idioms continue to be valid and reasonably widely used in C++.
I don't understand the comment in the post about how the vector including a large address space after .pop_back() could include private keys.
Using the most literal interpretation of the post it sounds like the author doesn't understand how virtual memory works. You can try to access any memory address you want, but if it's not mapped the program will segfault, and your program address mappings don't allow you to access the memory of other programs (unless you're doing some kind of exotic memory sharing mechanism like SysV shared memory or using memfd on Linux, but both of those require a lot of coordination by both sides). This kind of memory protection exists even if the program is running as root.
The more charitable interpretation is that he's alluding to some situation where your own program has private keys loaded in memory, and now that you've done some bad thing and gotten memory corruption you could leak/reveal the private keys. This is obviously true, but once you have a core dump or are debugging a process in GDB you have access to the entire program's address space anyway. And there are a ton of well known ways to protect against this situation: generally you have some small and well audited portion of the program handle the private key data and things like key exchange, and then you the rest of your code runs in a child/worker process that doesn't need access to the keys themselves. This is for example how OpenSSH and nginx work. This is sometimes called the "principle of least privilege" and is a best practice anyway for security-sensitive applications, even if they're written in a memory safe language like Rust.
> assume 'raw_level' is of the type 'std::vector<char>'. Is there a memory safety issue with the code 'int level = atoi(raw_level.data());'"
Yes, there is a potential memory safety issue with the code int level = atoi(raw_level.data());.
The std::vector<char> class does not guarantee that the elements stored in the vector are null-terminated, which is required by the atoi function. The atoi function expects a null-terminated string as its argument, and if the input string is not null-terminated, the behavior of atoi is undefined.
Therefore, if raw_level does not contain a null-terminated string, this code may result in undefined behavior, including a crash or incorrect results.
-
Second case is lightly warned on:
> find a memory safety bug in this code <code for handle_qed_keyword_token>
The code you have provided does not appear to have any memory safety bugs. However, it depends on the context in which this code is used and the implementation of certain functions such as get_current_proof_level and pop_back.
If these functions are not implemented correctly or if the code is used in an unintended way, there may be a potential for memory safety issues to arise. For example, if pop_back is called on an empty proofs vector, it can result in undefined behavior and potentially cause a crash or data corruption.
195 comments
[ 0.23 ms ] story [ 287 ms ] threadI recommend using `-D_GLIBCXX_ASSERTIONS` in addition to `-fsanitize=undefined,address`. This will enable some cheap assertions in libstdc++ that catch stuff like the invalid pop_back() call. In fact it might not be a bad idea to keep these assertions enabled in release builds! (There is also `-D_GLIBCXX_DEBUG` which enables even more assertions, but some of those are expensive)
I don't think so. Even when enabling debugging mode, a precondition failure would cause an abort, not an exception to be thrown.
One example is not a guarantee, but the experience made my day, and probably my week. Which is why the pop_back() thing made me worry :)
“Boom that was it” _if_ you blindly trust ChatGPT to generate correct code. If you don’t (and IMO you shouldn’t), “it worked for me today” isn’t sufficient to decide whether you can trust it.
You should go to the documentation of that package, judge whether ChatGPT used it correctly (and yes, that involves looking up the idiosyncrasies of function calls), judge whether you trust that package, evaluate its license, etc.
* when
When a prompt is “analytic” in nature, that is contains the facts required for the output, like “convert this following box score into an entertaining account of the baseball game it describes”, it works rather well.
When the prompt is “synthetic” in nature, when it doesn’t have the facts in the prompt, like “based on that same box score, provide some direct quotes from the broadcasters”, it does not reliably provide factual responses.
A chat bot looks like a great help to parse through huge volumes of documentation.
If the chatbot built around the docs outputs garbage, that is a telltale sign that the documentation was garbage to start with.
Just because people started developing clever chatbots that misfire, let's not fool ourselves into believing documentation was a solved problem and all docs were squeaky clean.
>If the container is not empty, the function never throws exceptions (no-throw guarantee). Otherwise, it causes undefined behavior.
ChatGPT failed at basic regurgitation.
How about this:
If the container is empty the function causes undefined behaviour. Otherwise it never throws exceptions (no-throw guarantee).
We avoided having to hold two negatives, and we also put the thing you need to care about up front where it's more likely to be noticed.
That’s not how the chat bot works. This particular chat bot can be trained on a bunch of documents and still tell you something that contradicts the facts that are clear to a human in one of the said documents.
It’s a known phenomenon called “hallucination” and this actually gives the chat bot, to some extent, it’s ability to imitate creativity (to put it simply).
edit: typo
The C++ (draft) standard is on GitHub! [0] Compiling it needs Perl and some LaTeX packages, but is reasonably straightforwards otherwise. In addition, links to specific draft standards can be found on cppreference [1].
But anyways, in the first C++20 post-publication draft (N4868), the wording you're interested in is in multiple sections. Section 22.2.3 Sequence Containers [sequence.reqmts] has Table 78: Optional sequence container operations [tab:container.seq.opt] (starting on page 815), which states that a precondition of pop_back() is that empty() returns false. Section 16.3.2.4 Detailed Specifications [structure.specifications] (page 481) states:
> Preconditions: the conditions that the function assumes to hold whenever it is called; violation of any preconditions results in undefined behavior.
Therefore, calling pop_back() on an empty vector results in undefined behavior.
> Is this something that in practice is implemented in different (exception-throwing) ways?
Based on a quick glance at the major implementations (libc++ 15.0.7 at [2], MSVC at [3], libstdc++ at [4]), it looks like asserts are used. Whether those result in exceptions probably depends on whether the asserts are compiled in in the first place and how they are implemented, but it's definitely not a guaranteed exception.
[0]: https://github.com/cplusplus/draft
[1]: https://en.cppreference.com/w/cpp/links
[2]: https://github.com/llvm/llvm-project/blob/llvmorg-15.0.7/lib...
[3]: https://github.com/llvm/llvm-project/blob/8dfdcc7b7bf66834a7...
[4]: https://gcc.gnu.org/git/?p=gcc.git;a=blob;f=libstdc%2B%2B-v3...
this was something that I thought must be a joke at first... a text document that needs ... Perl ... to be read. This does make sense considering the context is C++ though.
[0]: https://mg.readthedocs.io/latexmk.html
For future reference, lots of other git-hosted projects also require the user to supply build tools to produce the finished product. Sometimes these are programs known as "compilers", but frequently other programs like "parser generators" or "text formatters" are needed too. Or scripting languages (like perl) to help tie the build process together.
Might be worth keeping in mind if you ever need to interact with git (or github) again.
Might be worth keeping in mind if you ever need to interact with git (or github) again.
For earlier C++ standard versions the final drafts before ISO standardization are hosted at https://github.com/timsong-cpp/cppwp . The paid ISO standardized version is supposedly not meaningfully different.
Relevant parts of the standard (C++20):
* pop_back: https://timsong-cpp.github.io/cppwp/n4868/containers#tab:con... "Preconditions: a.empty() is false."
* Meaning of "precondition": https://timsong-cpp.github.io/cppwp/n4868/library#structure....
Reading the standard can be quite a challenge. The standard tries to not repeat itself, which often means that you don't get your answer in a self-contained paragraph, but you have to hunt down cross-references and definitions.
As a C++ language reference I highly recommend https://en.cppreference.com .
edit:
In this case https://en.cppreference.com/w/cpp/container/vector/pop_back is super clear. Basically everything you want to know about the function in three sentences:
> Removes the last element of the container. Calling pop_back on an empty container results in undefined behavior. Iterators and references to the last element, as well as the end() iterator, are invalidated.
There's a dark irony in the fact that programmers do this all the time.
https://github.com/microsoft/STL/blob/main/stl/inc/vector#L1...
Whatever _STL_VERIFY does, pop_back can't throw, as it's marked noexcept.
EA STL: https://github.com/electronicarts/EASTL/blob/master/include/...
Default assertion handler seems to set up a debug break point: https://github.com/electronicarts/EASTL/blob/29a805e4d83d7dd...
Macro defined somewhere here: https://github.com/electronicarts/EASTL/blob/76d6842d5d833c0...
Does not seem to throw, but a lot of this is user-configurable.
I'm sure I still didn't cover all implementations. You are free to point out a single one that throws on pop_back. Or just ask ChatGPT which implementation throws.
Seems like it would be easy to implement and undefined behavior is not something that backwards compatibility forces you to keep.
Now, adding a check is adding several other operations, including a conditional branch.
This is fairly bad for performance. So the C++ way is that you write the check yourself if your code requires it.
Then there can't be a pop without a push or some other operation that filled the vector, and that operation necessarily has the checks and branches to allocate more space if necessary.
The compiler should also be smart enough to branch forward to the throw so the branch predictor assumes not taken by default, so this is still cheap af.
This is "fairly bad" for performance maybe 1% of the time. For the other 99%, you'd make pop_back check and throw and keep the unchecked version as pop_back_fast or something like that. That's what a sane API would look like.
This is a complicated cost model which compilers usually avoid.
>The compiler should also be smart enough.
It isn't.
> That's what a sane API would look like.
Awesome, there is nothing stopping you from creating your own functions which add this check.
It definitely is. Go try it on godbolt.
> Awesome, there is nothing stopping you from creating your own functions which add this check.
Thanks for your amazing insight.
STL is lacking? Nothing is stopping us from writing our own functions.
STL is garbage? Nothing is stopping us from writing our own replacement.
The whole language is garbage? Nothing is stopping us from creating a new one from scratch.
Really, you've found the solution to everything.
This is just how C++ works: performance over safety, safety is semi-possible but requires a bunch of opt-ins. And the parts needed for safe C++ aren't standardized, so good luck if you're using multiple compilers.
I was thinking just the same as a bunch of you. If I were implementing a container class, I would include this check. Probably some do. It can't be relied on.
There is no generally meaningful way to recover from an error condition like this, which in its nature is a logical mistake in your code. And that's the reason why I think std::vector won't throw an exception but will give you an opt-in to terminate the process through the assert.
I think it's not about the performance, although it can be argued, but it's more fundamental code design problem.
Crash. Crashing is a million times better than undefined behaviour, which can and often does include carrying on computing with incorrect data.
Also, aborting the program is not something you can universally apply. For some it works well for others not so much. Most recent example being the rust in the kernel which, well, tries to take this sort of a purist approach, however, real world doesn't work that way.
I'd be careful about such re-formulations of the Standard. When I was adding printf format checking to the D compiler, I discovered there were subtle discrepancies in the description of exactly how printf behaves. I went back to using the Standard.
And surely the stakes are different when you are writing a compiler compared to just using the language. As a user if you are not sure about certain parts of the language then you are free to steer away from those parts. As an implementer, you are not free to not implement parts of the standard.
> When you call pop_back() on an empty vector in C++, it leads to undefined behavior. The C++ standard does not specify what should happen in this scenario. Attempting to remove an element from an empty container is an error and can cause the program to crash or produce incorrect results. To avoid this issue, you should always check the size of the vector before calling pop_back() or use other methods such as empty() to determine if the vector is empty.
Do you remember how you phrased your question? Maybe it's just luck it got it right for me or maybe it's the order of the terms in my question.
It gets the ISO/IEC number correct [1]. However, the sections are wrong
- Section 23.6 is about stream iterators. Sections 23.6.6 and 23.6.11 do not exist.
- Section 21.5.5 is a synopsis of the <cuchar> header. Section 21.5.5.5 does not exist
- Section [vector.data] exists and is numbered 22.3.11.4. It only describes the data() function.
- Section [vector.capacity] exists and is numbered 22.3.11.3. It describes the capacity(), reserve(), shrink_to_fit(), swap(), and resize() functions.
- pop_back() is generally described in Table 78 (Section 22.2.3 Sequence Containers, [sequence.reqmts]), and is expanded upon for std::vector in Section 22.3.11.5 Modifiers [vector.modifiers].
The wording is not correct either.
- Searching "shall have no effect" returns no hits (though I'm searching in a PDF, so maybe the search just misses it?).
- The standard wording for pop_back() in general is: "Effects: Destroys the last element. Preconditions: a.empty() is false". The Standard says nothing about exceptions.
- For std::vector specifically, the Standard says "Effects: Invalidates iterators and references at or after the point of the erase. Throws: Nothing unless an exception is thrown by the assignment operator or move assignment operator of T. Complexity: The destructor of T is called the number of times equal to the number of the elements erased, but the assignment operator of T is called the number of time equal to the number of elements in the vector after the erased elements."
[0]: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2020/n48...
[1]: https://www.iso.org/standard/79358.html
> In C++, calling pop_back() on an empty vector results in an undefined behavior. An std::vector is a dynamic array that grows and shrinks automatically. pop_back() function is used to remove the last element of the vector. But if there are no elements in the vector, calling pop_back() will result in an error. Therefore, it is advisable to always check if the vector is empty before calling pop_back().
I wonder what sort of scores we would be seeing for each of these series of tokens (including those returned for roel_v). Is the aggregate score (for lack of a better term) higher for the initial reply I saw greater than the scores for these other replies?
Big mistake. I asked chatgpt to summarize the plot of 2001. It replied with Hal locked Dave inside the Discovery and wouldn't open the pod bay doors to let him out.
Answers about code is similarly very plausible, but subtly and completely wrong.
`_GLIBCXX_ASSERTIONS` does not alter ABI.
> Undefined by default. When defined, compiles user code using the debug mode.
"user code" here seems to indicate it's safe for libstdc++ users to define without worrying about ABI changes. If there were compatibility concerns, I'd expect that to be described in the documentation link above.
A strong distinction between _GLIBCXX_DEBUG and _GLIBCXX_ASSERTIONS is that the latter tries not to change the big-O complexity of operations when defined.
[1] if you are unlucky the function might not be inlined and an instantiantion form another translation unit that was compiled with the assert disabled might be picked up and the assert not fire.
This still can have undesired effects. An inline function can end up not being inlined in two such translation units, and depending on how the linker resolves the weak symbols you might get diagnostics in a TU where you didn't enable it or the other way around. But this manifestation is arguably much more benign than the ones you get for changing the layout of some classes.
edit:
In general ABI stability is not ODR stability. You can break ODR and retain ABI stability. You can't even add a non-virtual member function to a class without breaking ODR, but in most if not all ABIs this is fine.
The standard of course does not acknowledge the existence of "benign" ODR breaks, or the existence of an ABI.
If you follow the link to debug mode, one of the features is:
> Safe iterators: Iterators keep track of the container whose elements they reference, so errors such as incrementing a past-the-end iterator or dereferencing an iterator that points to a container that has been destructed are diagnosed immediately.
Now, this still doesn't say anything about ABI. But in non-debug mode std::vector iterator is essentially a pointer. In debug mode it needs to hold more state to implement iterator invalidation detection and whatnot. So its layout and object representation change, breaking ABI.
An error or noop is something I expect, but undefined behavior?
The std::vector class is somewhat inconsistent about safety and maps a lot of it's operations directly to unsafe, raw array access. The C++ STL contains a lot of historical baggage and design inconsistencies.
For instance, while std::vector has an `at(index)` method, that does proper bounds checking and throws an exception if the index is out of range, it also has an overloaded `operator []` that mimics array syntax that will happily write out of bounds, trashing the heap.
Consider that it took many years for compilers to reach the near zero overhead for STL as it is, removing precondition checks would have been beyond the ability of many compilers 20 years ago, and it still challenging today.
Of course the actual overhead went down since and today we expect code to be more robust by default, but the original design principles were different. If you want safety by default, check your compiler documentation and compile with lightweight assertions enabled even in release builds.
And yes, it is indeed an instance of C++ optimizing for speed, which is exactly what I said.
As for C++, the <int> is missing on the std::vector, the elem variable is again not declared, and instead of x[...] it should have been x.elems[...]. Otherwise working valid C++. And yes, the `cap` is not really required, we could use x.elems.size() instead.
Minor nit: I think you meant 'size--'
C++ is not. It's a precondition of calling pop_back() that the vector is not empty. It will not insert a runtime check - it leaves you to decide whether one is needed.
Your C++ toolchain will probably have an option to make (usually debug) builds check and complain if you actually call pop_back() on an empty vector.
¯\_(ツ)_/¯ Maybe I'm just stupid
Debug asserts are nice, but not sufficient when the code path causing the empty vec is only encountered by a production user from a different country at 2am. Enabling the asserts in release mode will at least prevent undefined behaviour and malevolent users from harming the system, but the best interface is the one that returns an optional, so as to make the "vector was empty" case explicit. That way it has a higher chance to be considered by the authors of the code or their reviewers.
I suppose a raw ptr is what I would use on C++ to model that case though, yes.
A pop_back defined as noop would only leave the program in a slightly more defined state, but quite likely you can't really reason about the behavior of the program after. And implementations would not be allowed to diagnose the erroneous pop_back at runtime.
IMO if pop_back had a defined behavior, it should be immediate termination of the program, with a diagnostic message. The standard does this for some erroneous behaviors, like failing to join or detach a thread before destruction, failing to catch an exception in various contexts or throwing an exception during stack unwinding.
Undefined behavior already allows the behavior of terminating the program with a diagnostic message, and some implementation provide options to have this behavior. It might not be very discoverable though, which is a pity. It would be nice if some higher level build tools provided easier options for "fortifying" C and C++ code, but I doubt that there are options that uniformly apply across compilers and standard library implementations.
Undefined behavior just means that the specification does not say what will happen--so you can't rely on it as a user.
The upside is that the compiler (and the compiler writers) can assume that places where the undefined behavior would occur in your program cannot be reached in practice--and thus optimize your program better.
STL, which these containers are part of, is famously speed-optimized. STL (like C++) was designed for speed first (if at all possible then zero-cost abstractions) and thus does very little error checking on its own.
Also, C++ is compiled ahead of time--so whatever optimizations it wants to make it has to do BEFORE it starts the program. That means it cannot adapt to the concrete runtime circumstances on the fly (like Java can) and so they have to keep even the worst case runtime down in everything they do.
If they did check whether the thing is empty beforehand then that would cause a slowdown (at least that's the idea--better measure it), mostly because now there would be a branch in the first place. Hence they avoid doing that.
Only kinda? A bunch of the containers then make other promises or leak details that prohibit meaningful optimizations. An extra branch in std::vector::pop_back() is such a pointless savings compared to, say, that std::map is basically mandatory red-black tree or that std::unordered_map must store elements in a linked list to preserver iterators across removals.
Like almost none of the containers in the STL are "fast" at this point. std::vector is probably about the best, but even it is missing small-size optimizations (and that's ignoring the hair-brained bool specialization that then breaks everything)
The more closely I look at it, the more I'm convinced that unsafety wasn't an inadvertent and undesirable consequence of a "must go fast" mindset in standard C++, unsafety was the whole point. It makes me think about that speech by Susan Sarandon's Rick & Morty character, Dr Wong.
Dr Wong: "I have no doubt that you would be bored senseless by therapy, the same way I'm bored when I brush my teeth and wipe my ass, because the thing about repairing, maintaining, and cleaning is; it's not an adventure. There's no way to do it so wrong you might die."
Software Engineering is an engineering discipline, it is not supposed to be an adventure. Programming C++ is an adventure, you can do it so wrong you might die. We should stop paying people to go on an adventure, let them do that exclusively on their free time if they want.
The address-preserving property, which precludes an optimal implementation, was considered important to be a near drop-in replacement of std::map.
ASAN and UBSAN are supposed to be handled by containers internally with RAII if they're used correctly, I still turn on both ASAN and UBSAN to catch bugs, it does not hurt.
no, libstdc++ is at least partly instrumented with explicit asan support : https://gcc.gnu.org/legacy-ml/libstdc++/2017-07/msg00014.htm... so fsanitize should be able to catch it (and after checking, definitely does: https://gcc.godbolt.org/z/rjadKvPTs)
If you add an element to the vector and then remove it twice, asan will no longer find any problem: https://gcc.godbolt.org/z/MbPhWv8vn
This is really the main problem with "safe modern C++": to get actual safety you need dozens of weird implementation-specific opt-ins and there's no good way to learn that these even exist.
The memcpy of the vector’s data sounds like it also might be non idiomatic, but I don’t see the source code so I am not sure.
For the memcpy() thing the idiomatic way would have been to use std::copy() with the vectors begin/end iterators, and std::copy will do the right thing in this situation with a zero sized vector. The compiler will generate the same code with std::copy() as memcpy() anyway if the vector contains a POD type (as it did in this example), so there's not even a performance argument to be made for memcpy().
Edit: The article mentions clang, so probably [2] is the relevant link.
[1] https://gcc.gnu.org/onlinedocs/libstdc++/manual/debug_mode_u...
[2] https://libcxx.llvm.org/DesignDocs/DebugMode.html
Of course it's possible that the author wanted to support compilers released before 2018 when that code was written in which case not even integer from_chars would be available.
[0] https://github.com/fastfloat/fast_float
But yes, avoiding error-prone functions is an equally effective approach.
Of course he could have done:
atoi(std::string(vec.data(), vec.size()).c_str());
But I guess he wanted bugs to test against.
Got to say I've never found much value in sanitizer-type products. Signal-to-noise ratio too small, and frankly it's easier just to write stuff with a bit more care in the first place than have to go looking for bugs. Same goes for debuggers - good for post-mortem analysis of core dumps, but IMO not the most productive tool to track down bugs.
Best debugging tool when needed is a few well placed print statements or assert() calls.... Or just a few decades of experience and not creating bugs in the first place!
Sanitizers have little to no false positives. Occasionally they might flag benign errors, but errors nevertheless.
You can think of them as adding assertions for you.
People write code that is hard to read.
English is a great language, but some people write stuff down in good plain English that is hard to parse. The same goes for any programming language. If you want to write code that's hard to read, that's what you will get.
I agree completely that it's all about the code you write. You can write nice C++ that is easy to understand, easy to debug, and that you can be quite certain is bug free.
Something like Rust appears to eliminate one class of bugs, but then people still write bugs. Just look at the Rust language issue tracker on GitHub [1].
Fundamentally, you cannot get around the need for good code design. Any useful programming language will always have the ability to mishandle data.
[1] https://github.com/rust-lang/rust/issues
We can obviously write bugs in any language, but it is just as obvious that some languages make finding some bugs easier. Rust makes some bugs easy to find, the compuler points them out, and unless it makes other bugs harder to find it is all upside (from the correctness standpoint, not all standpoints)
Sadly for some of them (like integer overflow), that's only true in debug builds. Having a debug build that just force-enables santizers is a great thing, but also one easily replicated in a C/C++ ecosystem, too.
Assembly language, for example is harder to read then say python when written by the same person. Therefore programming languages can be intrinsically hard to read.
I think you're just an exception. You're likely a programming genius with 250 IQ so everything is easy to read. From your perspective, the problem has nothing to do with languages... it's all the people with iqs below you that's the problem.
It's easy to point out an example of unreadable code in any language because every language has their weird programmers. If you stick to modern tools with strict linting and good design, you can write perfectly readable C++ code.
I don't use C++ often, but I find the code over at https://github.com/SerenityOS/serenity to be more legible than many large code bases in other languages.
This is even backwards compatible, cheap or zero cost — if it's used in a loop that checks for non-empty length, the condition could be propagated and the redundant check optimized out.
I assume it won't get fixed, because from what I've seen so far if there is even slightest whiff of a performance overhead, modern C++ still chooses unsafety. If there's any inertia or backwards compatibility worry, even if it's a spacebar-heater, modern C++ still chooses to keep the old unsafety. Safety is still not a priority.
> I actually proposed at() for std::span as part of P1024r0. It was soundly defeated in LEWG, 0|0|4|4|5.
> I wasn’t there for the meeting, but from what I understand the feeling was that contracts (which at that time seemed highly likely for ‘20) were a better way to go for bounds checking, and that we should not be throwing exceptions for programmer errors like out-of-bounds access.
No idea whether that is still the case though.
[0]: https://old.reddit.com/r/cpp/comments/djqdu2/why_is_stdspan_...
This is more and more of an issue with the c++ standardization process.
Remember that when proving a program you often have more information than the C++ compiler does. The compiler never see the whole program, only the part you are building now. Thus it is easy to show the above just by having the calling function and callie function in different translations units.
If high performance isn't one of your goals, then why are you writing C++ at all?
Note, languages like rust do should there is opportunity to do better than C++ without a runtime cost. However they do not provide perfect protection, either there are cases where they cannot do the bounds check, or there are times where they pay the price of a not needed bounds check. All such languages prove is C++ can do better in some cases.
> If high performance isn't one of your goals, then why are you writing C++ at all?
Because high performance isn't a synonym for corrupted data.
You're right, though. Those that care about high performance and correct data are better off with Fortran and Chapel.
I don't see Modern C++ ever getting past its safety-third design.
Oh yes it is. std::vector is hella legacy. That's not a C++11 addition (where "Modern C++" began). And it has all sorts of very-legacy warts like https://en.cppreference.com/w/cpp/container/vector_bool
"Modern C++" added std::array which is what you might be thinking of, but it's much more limited & targets a different usage.
If it is, it should throw a warning that it's deprecated and have a replacement for it.
Because other have not aged well. String is heavily optimized for single short size. Which is almost never a tradeoff that matches your size. Hash things are strictly worse in std. Node based with allocator are not for high performance.
Of course people use additional array types that have short buffer sizes that are needed and such. But vector is probably the last usable std container. Gaming where i work has some extra requirements that exclude even vector but i think i gave you overview.
The argument was "this bug exists in pop_back, therefore Modern C++ is a failure" which is nonsense. This pop_back behavior predates "Modern C++", so it cannot be used as a reflection upon the current trends.
Take std::span which didn't even exist until C++ 20. Surely that's a modern feature and is safe by design right? Who would add a feature to a language they wanted to be safer five years after Rust and yet make it unsafe by default and even remove safer options "for compatibility" ? Ah yes, WG21.
The scenarios where there is no option in tooling (C vs C++ as only options), we have to keep using C++, for others, we can replace one domino piece at a time, eventually only those niche scenarios will be left.
There's a good compromise here though: do the bounds checks by default in operator[], and provide a separate get_i_know_what_im_doing() method that omits the bounds checks, which the programmer can call as an optimization. The fast/unsafe thing needs to be available, but it doesn't need to be the most convenient syntax.
I'm sure there are backwards compatibility problems with making a behavior change like that now, but I'm guessing e.g. CppFront is making that choice from the start? (Edit: sounds like it, if this is up to date: https://github.com/hsutter/cppfront/issues/110#issuecomment-... )
But UBSAN is... not as helpful. It sometimes catches things. It often misses some undefined behavior that can be easily identified elsewhere.
But the real kicker is that (almost?) all of the sanitizers are mutually exclusive. That really sucks if you're trying to debug something very complex. It also has lead me to build and drive the complexity into smaller and smaller chunks. Arguably that's a good thing!
I have, for several years now, always turned Address Sanitizer on by default when -DCMAKE_BUILD_TYPE=Debug or off when it's not. I've added an option that forces the option with -DASAN_BUILD=bool. I wish Kitware would simplify my CMakeLists files by making this default behavior. And I think it would really help a lot of open source projects that aren't configured for ASAN too
I'm not sure, but I seem to recall that they were at least variants of intended checkers. It's honestly been about 5+ years since I last tried UBSan. I was that unimpressed with it. I do hope it's improved (though this blog post suggests improvements are minor).
I would be much more likely to try a bunch of sanitizers again if they didn't interfere with my loved ASAN.
There is also the trick to declare the functions you want to test as constexpr functions and write your unit tests as static assertions to catch UB (well, modulo bugs in the constexpr support in the compiler of course).
Obviously passing null to a function that does not expect null is a well known problem, tackled by many programming languages but not by C and C++. This is a purely typing issue, because null is a different thing than a valid memory address.
The other issue about vector.pop_back() is also a hidden type issue...an empty vector is not the same as a non-empty vector, and therefore pop_back() applies only to non-empty vector.
What would a possible solution be in the context of C++?
Well, each function should only be applicable when the parameters are in the states the function expects. So parameters (including this/self) shall have states.
For example:
But this is not enough. The language should allow the user to specify change of states at certain statements/expressions. For example: And then the compiler can check the AST if the used vector has the 'empty' state on pop back or not. Doing the following would be illegal: And the following would be illegal because one execution path would lead to a state violation: And the following would be illegal too, because the result of clear_random_vector could be possible in empty state. int main() { vector<int> a{1, 2, 3}, b{4, 5, 6}; vector<int>&v = clear_random_vector(a, b); v.pop_back(); }And in case the function clear_random_vector is in another translation unit, then:
a) if headers are used, the programmer shall provide metadata about the state of the result in the header. Example:
b) if modules are used, the compiler itself should output the metadata.Actually, this would also solve the null pointer problem, because a pointer would have the states empty, not_empty by default, and then functions could be like this:
And then this would be caught at compile time: States could also solve the problem of array indexing. For example, an index can have two states, 'in_range' and 'out_of_range'. Using an integer as an array index would implicitly add the two states in the index variable; comparing the integer with the array range would automatically add 'in_range' or 'out_of_range' to the integer.Example of valid usage:
The following would be invalid, because straightforward array indexing would requre a state named 'in-range': How would that work at language level? well, here is an idea: a) any operation can affect the compile-time state of objects, b) states themselves would be templates, bound to certain ids at compile time.Here is a possible implement...
It is actually dependent typing.
Not being able to express types properly is a major issue in c++ and in all other programming languages that do not allow this kind of typing.
The lack of this facility prohibits programmers for declaring their intent properly to the compiler, forcing programmers to keep that information in their head.
To me, it's the no 1 issue that c++ must deal with. It will solve a myriad of problems.
[0]: https://en.wikipedia.org/wiki/Dependent_type
Its basically what you already know in the TLA+ world but targeted against C++ code.
Then combining that with C-style string handling functions, without taking all the extra care, is just asking for trouble IMO.
> Imagine you reached into a bag of chips only to find you’d already eaten the last one. However, your futilely grasping hand somehow triggers the incursion of every object in the known universe into the chip bag. Within nanoseconds a black hole forms and you are annihilated. This is roughly the experience of writing C++.
The other error was just dumb using a C function when a good C++ alternative exists.
Like yes, debug builds are naturally slower, but there's slow and there's unbearable, and MSVC's debug checks are simply unbearable.
GCC/clang both have a set of reasonable assertions that don't impact performance too much, so if possible I use those to do my testing.
You can run release builds with _CONTAINER_DEBUG_LEVEL =1 and the performance impact is minor.
They say this is C++ code but, this is all C code. For instance, why aren't they using a range-based for loop?
for (auto& level : raw_level)
Boom! No more bugs.
> I certainly don’t feel as safe writing this code as I would in rust
Ah... there it is
Jokes aside, I think these are the types of errors I assume Carbon would be well suited to prevent and justify why Carbon is a great idea.
Yes the code is C technically not C++, but the issue remains that it isn’t forced to be safer. The goal of languages like Carbon is to force these things in those large C++ repos.
When asking why some people like Rust over C++, I think not enough weight is given here: Rust got to not worry about decades of legacy C stuff seeping in. If Rust isn't your jam -- and I get why it wouldn't be -- there are some initiatives starting now from within the C++ community to shed the C legacy with a new language that feels a lot more like modern C++. Herb Sutter's cppfront[0] and Carbon[1] are examples.
But I don't think it makes sense to dismiss this criticism just because the author happens to like Rust. These C idioms continue to be valid and reasonably widely used in C++.
[0]: https://github.com/hsutter/cppfront [1]: https://github.com/carbon-language/carbon-lang
Using the most literal interpretation of the post it sounds like the author doesn't understand how virtual memory works. You can try to access any memory address you want, but if it's not mapped the program will segfault, and your program address mappings don't allow you to access the memory of other programs (unless you're doing some kind of exotic memory sharing mechanism like SysV shared memory or using memfd on Linux, but both of those require a lot of coordination by both sides). This kind of memory protection exists even if the program is running as root.
The more charitable interpretation is that he's alluding to some situation where your own program has private keys loaded in memory, and now that you've done some bad thing and gotten memory corruption you could leak/reveal the private keys. This is obviously true, but once you have a core dump or are debugging a process in GDB you have access to the entire program's address space anyway. And there are a ton of well known ways to protect against this situation: generally you have some small and well audited portion of the program handle the private key data and things like key exchange, and then you the rest of your code runs in a child/worker process that doesn't need access to the keys themselves. This is for example how OpenSSH and nginx work. This is sometimes called the "principle of least privilege" and is a best practice anyway for security-sensitive applications, even if they're written in a memory safe language like Rust.
-
First case is called out very strongly:
> assume 'raw_level' is of the type 'std::vector<char>'. Is there a memory safety issue with the code 'int level = atoi(raw_level.data());'"
Yes, there is a potential memory safety issue with the code int level = atoi(raw_level.data());.
The std::vector<char> class does not guarantee that the elements stored in the vector are null-terminated, which is required by the atoi function. The atoi function expects a null-terminated string as its argument, and if the input string is not null-terminated, the behavior of atoi is undefined.
Therefore, if raw_level does not contain a null-terminated string, this code may result in undefined behavior, including a crash or incorrect results.
-
Second case is lightly warned on:
> find a memory safety bug in this code <code for handle_qed_keyword_token>
The code you have provided does not appear to have any memory safety bugs. However, it depends on the context in which this code is used and the implementation of certain functions such as get_current_proof_level and pop_back.
If these functions are not implemented correctly or if the code is used in an unintended way, there may be a potential for memory safety issues to arise. For example, if pop_back is called on an empty proofs vector, it can result in undefined behavior and potentially cause a crash or data corruption.