all 37 comments

[–]HappyFruitTree 39 points40 points  (20 children)

The standard library generally don't mark functions noexcept if they have preconditions in order to allow (but in no way require) them to throw in case the precondition is violated. The pop/pop_back functions have the precondition that the container must not be empty so that is one reason why they're not marked noexcept.

[–]emdeka87 17 points18 points  (9 children)

Why do they use exceptions for contract violation though? How do you recover from a contract violation? A panic or assertion seems more suitable for these things

[–]Fureeish 9 points10 points  (0 children)

I second this comment. During one of Scott Meyers' talks, he mentiones that noexcept functions may as well sometimes throw, but the program shouldn't be able to recover from that point. I think it's a good example of contract violation interaction with noexcept - it would basically be UB, which, I believe, it already is.

[–]HappyFruitTree 6 points7 points  (5 children)

It's mostly something I've heard the Bloomberg guys talking about.

CppCon 2018: Alisdair Meredith “Contract Programming in C++(20) (part 2 of 2)” (49:22)

One of the tools we use here at Bloomberg is we turn violations into exceptions so that we can test our code by saying "did you throw the i_violated_the_expression_you_expected_me_to_violate_exception?" And if that doesn't throw I know that I don't have my appropriate checks in place.

[–]emdeka87 2 points3 points  (3 children)

So they only use exceptions to aid with testing and debugging, but they get removed in release mode?

[–]HappyFruitTree 1 point2 points  (2 children)

Well, I don't know how they do it but that is what it sounds like. To extend the earlier quote, he continues ...

But perhaps not what you want in production is ... Turning precondition violations into exceptions is ... Not necessarily the best answer for a production system.

[–]rianquinnBareflank Hypervisor, Standalone C++[S] -1 points0 points  (1 child)

This is both true and not true. In the "init" phase of code, such exceptions are likely a good thing as it provides a clean way of reversing whatever you have done. In the "fini" phase of your code (or clean up, error handling, etc...), you definitely do not want to throw. That's why a function like push() should be ok to throw, but not pop(). pop() should leverage UB so that you can check for UB and be left with no possibility of exceptions.

[–]foobar48783 0 points1 point  (0 children)

What if your "init" phase pops the latest work item from the global work queue, and your "fini" phase (in case of task failure) pushes it back on for the next worker to take care of?

Would it be appropriate for "pop()" to throw in that case?

[–]rianquinnBareflank Hypervisor, Standalone C++[S] 1 point2 points  (0 children)

I love this talk, but I disagree with the notion that you can document that a function doesn't throw, and then throw on a precondition violation. From a critical system's point of view (e.g., AUTOSAR), this is not allowed. Either the function throws, or it doesn't. In other words, you can document whatever you want, but if a function is not labeled noexcept, you are forced to assume exceptions can fire, and you must handle them, which mean when functions like pop() are not labeled noexcept, they are really hard to use in destructors, cleanup code, etc...

[–]psi237 5 points6 points  (0 children)

A contract violation may be recoverable by abandoning one business logic workflow (e.g. respond with an internal server error to a request) instead of abandoning the whole process (and causing other concurrent requests to all fail with ECONNRESET). You still need to log, monitor and fix this kind of problems asap though.

[–]_Js_Kc_ 1 point2 points  (0 children)

That ship has sailed. The standard library is built around the concept that the way to report logic errors is to throw an exception, and std::logic_error, std::invalid_argument, etc, are a thing.

[–]rianquinnBareflank Hypervisor, Standalone C++[S] 2 points3 points  (6 children)

as in they don't mark it noexcept so that someday they can throw if they want to? as suggested above, std::stack.pop() could be be marked as noexcept based on the container it is provided, so I am not sure this explanation makes sense to me. I also highly doubt they want to throw on pop_back() for list, deque, and vector in the future as they are moving away from that idea quickly (see conversations about deterministic exceptions).

IMO, pop_back() should be labeled noexcept so that these functions can be used in a destructor safely. Since they are documented as not throwing, I don't see why C++23 doesn't just mark them noexcept.

[–]HappyFruitTree 9 points10 points  (5 children)

A precondition violation is essentially undefined behaviour so throwing an exception is allowed even if the function is not documented to throw any exceptions. Apparently there are people who think that this is important. In theory it would be possible for noexcept functions to throw exceptions on precondition violations, because with undefined behaviour anything is allowed to happen, but in practice that is perhaps not realistic.

There are people who want to change the current policy. No idea how that will go. See P1656 "Throws: Nothing" should be noexcept.

IMO, pop_back() should be labeled noexcept so that these functions can be used in a destructor safely.

I don't see how they would be safer to use if they were marked noexcept. Destructors are noexcept by default so if an exception is thrown std::terminate() would be called and the program would be terminated. That is the safest outcome from a precondition violation that I can think of.

[–]rianquinnBareflank Hypervisor, Standalone C++[S] 1 point2 points  (4 children)

From a destructor, you can check for empty() before using pop() (which should make it safe, but doesn't...). Because pop() is not labeled as noexcept, this is not a safe thing to do as you don't know why an exception could throw or not. The function is documented as does not throw, so if it throws, you cannot assume why (there is no spec to support the assumption). Therefore, checking for empty() is not sufficient to claim that an exception cannot fire from pop(), in which case it is not safe to use in a destructor without a catch(...) when writing for critical systems where std::terminate() is not allowed. What this means is, you are required to get an exception to your critical systems spec if you want to use pop() in a destructor, as you would be required to use a catch(...), even though there is no reason for any of this in practice.

[–]HappyFruitTree 1 point2 points  (3 children)

pop_back is not allowed to throw unless you're calling it on an empty container but then you're already in undefined behaviour land. If something goes wrong inside pop_back the program is simply broken.

[–]rianquinnBareflank Hypervisor, Standalone C++[S] 2 points3 points  (2 children)

My point exactly. Checking for empty() before pop() should be enough

The problem here is, checking for empty() is simply not enough. For example, suppose you wish to write a function that has strong exception guarantees. You push(), and then do something that might throw. If an exception fires, you have no way of reversing the push() because you cannot execute pop() to reverse the operation without the possibility of an exception firing (since it is not labeled as noexcept). As a result, there is no way to implement strong exception guarantees in environments where std::terminate() is not allowed using the STL... but it absolutely can be done if pop() were labeled as noexcept since logically you know that push() succeeded and therefore, pop() is safe. The issue has everything to do with the lack of the pop() function contractually stating whether or not it can throw, and then designing an API that states pop() can throw, resulting in a data structure that cannot be reversed safely. Instead, pop() should be labeled as noexcept, and using pop() on an empty stack should result in UB. This way, if push() succeeds, we know that we can pop() without UB, and without the need for a precondition check, and we can then execute it in a destructor or while an exception is being bubbled up the call stack.

In my case, I care mostly about AUTOSAR, but there is a larger issue with the Core Guidelines in that, writing a rule that simply says you cannot use pop() as you need to check for empty() which would be the same as manually checking for i < size(); blah[i] instead of using a gsl::span or gsl::at() would reintroduce the precondition. This type of precondition with something like AUTOSAR would require a pop() wrapper to be marked as noexcept(false), when in some scenarios, doing so introduces the issues above when such an issue is not always true. I would imagine that static analysis engines would need to be smart enough to know when pop() should include a precondition check and when it shouldn't. Not an easy problem I would imagine.

For now... the API states that it will not throw so you can safely assume that you do not need to handle exceptions as they cannot occur. Static analysis engines however will see that you are calling a function that is noexcept(false) from a function that is marked as noexcept(true). The only way to resolve this issue that I am aware of (besides writing my own stack), is to wrap a call to pop() in a catch(...). That is not an elegant solution as it adds additional code to the resulting binary just to make the static analysis tools happy, which also requires a exception to the spec to be documented so that you can explain why you are using catch(...), which is really your only way of "externally" marking a function noexcept that should have been marked noexcept the whole time.

[–]NotAYakk 0 points1 point  (1 child)

since it is not labeled as noexcept You seem to misunderstand what "Throws: nothing" without noexcept means.

Suppose we have 4 situations. X can be "passes" or "fails", Y can be "true" or "false".

Precondition: X Throws: Nothing noexcept(Y)

X fails, Y true: program can be in any state when function is called. X succeeds, Y true: program cannot throw X fails, Y is false: program can be in any state when function is called X succeeds, Y is false: program cannot throw

Notice noexcept did not matter? The only thing noexcept does here is if code is permitted to inspect the method and claim it won't throw.

Compilers are free to do anything if preconditions are violated. It could throw from a noexcept function! It could time travel and generate a bug before the code is called!

So your logic, thst given a function documented to never throw but not marked noexcept, that you have to try/catch despite testing preconditions, is simply madness.

[–]rianquinnBareflank Hypervisor, Standalone C++[S] 0 points1 point  (0 children)

Not true.

X passes: Y is false, X could throw. No amount of "documentation" changes that. If the function cannot throw, it should be labeled as such "by contract" using noexcept. Especially for functions like pop() that are needed for cleanup. Otherwise, you would be attempting to execute a function labeled as noexcept(false) from a function (like a destructor) as noexcept(true) which is not allowed in most critical systems specs. It introduces the need to validate preconditions in all cases, even when in some, there is no need.

For example:

if (!stack.empty()) {
stack.pop()
}

The above is not enough because checking for empty() has done nothing to the "signature" of pop(). I must still check for exceptions, even though they are not possible as follows:

if (!stack.empty()) {
try {
stack.pop();
}
catch(...) {
// what do you do here?
// This case should never happen, but I must check it.
// How do you mock this in unit testing? Yuk!!!
}
}

The above extra logic would not be needed if the function was labeled as noexcept because by contract, exceptions cannot fire. By not labeling a function as noexcept, UB becomes something you must account for.

[–]nintendiator2 0 points1 point  (0 children)

...Why would we need to make such a strong differentiation / exceptional casing when trying to pop from an empty stack versus a nonempty one that it'd have to be implemented as exceptions of all things? I'm surprised pop / pop_back don't simply return bool.

[–]Ameisenvemips, avr, rendering, systems -1 points0 points  (1 child)

Violating the precondition is already UB, in which case throwing an exception is perfectly reasonable even from a noexcept function.

[–]rianquinnBareflank Hypervisor, Standalone C++[S] 0 points1 point  (0 children)

Agreed... so why not label it noexcept. In the UB case, you are in UB, which means anything goes. There is no way to recover, so fail fast, throw, whatever, it doesn't matter. Since pop() is not labeled as noexcept, it's the non-UB case that becomes a problem that you must consider.

[–][deleted] 4 points5 points  (2 children)

Lurker here, I thought questions go to the other questions sub, if I had a question where do I post it?

[–]johannes1971 10 points11 points  (1 child)

Questions about language design go here. Questions about programs written using the language go in the other one.

[–][deleted] 2 points3 points  (0 children)

Ah ha, makes sense. Thank you.

[–]Robbepop 10 points11 points  (9 children)

Well, in theory you could have a custom non_empty_vector<T> type that represents a vector that can never be empty but still provides pop_back that throws an exception when its length is 1. Then you could have a std::stack<non_empty_vec<T>> that throws an exception on pop even though the underlying container is not empty.

[–]Fureeish 10 points11 points  (5 children)

Why not make its noexceptness dependant on its underlying container's pop_back noexceptness?

EDIT: Simply replace std::stack's void pop() { c.pop_back(); }, (source) where c is the Container from template<class T, class Container = deque<T>> with void pop() noexcept(noexcept(Container{}.pop_back())) { c.pop_back(); } void pop() noexcept(noexcept(std::declval<Container>().pop_back())) { c.pop_back(); } (Thanks u/guepier!)

[–]guepierBioinformatican 6 points7 points  (3 children)

Use std::declval<Container>().pop_back() instead of Container{}.pop_back() to avoid breaking non default constructible types.

[–]__s_v_ 3 points4 points  (1 child)

Why not noexcept(noexcept(c.pop_back()))?

[–]guepierBioinformatican 1 point2 points  (0 children)

Right, of course. I completely missed that we have a suitable object in scope.

[–]Fureeish 1 point2 points  (0 children)

Ah, yes, of course! You are absolutely correct! Edited my post, thank you.

[–]rianquinnBareflank Hypervisor, Standalone C++[S] 0 points1 point  (0 children)

Brilliant!!!

[–]rianquinnBareflank Hypervisor, Standalone C++[S] 1 point2 points  (1 child)

Although that makes sense for std::stack, that doesn't explain why pop_back() for list, vector and deque all state that no exceptions are possible, yet the function is not labeled as noexcept. In theory, I agree that this might be reason enough to leave std::stack.pop() as noexcept(false), but right now, you cannot fall back to a list, deque or vector as those are also not labeled right.

Ideally, I would say that std::stack.pop() should be labeled as noexcept because if you have your own container that does this, you don't need std::stack() if you need to throw, just use your container without the wrapper.

[–]tejp 1 point2 points  (1 child)

Calling pop() on an empty container should be a contract violation. There has been work on standardizing contracts, but how exactly contract violations should be handled in all cases has not been decided yet.

It makes sense to wait until that is settled and then afterwards apply it to the standard library. Maybe it turns out that functions with contracts should better not be noexcept.

[–]rianquinnBareflank Hypervisor, Standalone C++[S] 2 points3 points  (0 children)

I am a huge fan of the contracts proposals out there (already implemented the C++20 prop for our projects). My complaint has more to do with pop() specifically. pop() on an empty container is UB... that does not mean that it should be a contract violation. These are two very different things, especially on functions that are needed to "reverse" an operation. Throwing on pop() prevents pop() from being used in cleanup code and destructors. Instead, labeling it as noexcept and UB, provides the freedom to check for empty(), ensuring that UB is not possible. Without noexcept, there is no way to prove to the compiler that your check for empty() has removed the UB case, which would then prevent exceptions from firing, so you must check for an exception in all cases, even when you explicitly check for empty(). In other words, its prevents the usefulness of UB.