3

I'm implementing a queue, and I was wondering, what should I do when a user misuses the container?

For example I have two methods, Front and Pop, which never throw (I static_assert that the destructor for the contained element is noexcept) so long as they're not called on an empty queue. I could add a check to these that throws if they're called on empty queues, but then I couldn't define them noexcept.

I was thinking that it makes sense to declare these noexcept, and then say that the behavior is undefined when called on an empty queue (I provide Size and Empty methods for users to check). I could then add the check only on debug builds, so it calls terminate in debug when misused and attempts to destruct or dereference missing elements on release. I was wondering what the better approach would be.


After considering the accepted answer, I decided to follow the standard. Vector's pop_back is not marked noexcept and has the same semantics as my Pop, so I won't mark it noexcept either. And in general, will try to avoid setting narrow contracts as noexcept.

Kian
  • 1,654
  • 1
  • 14
  • 22
  • It probably makes sense to declare the behaviour undefined when the queue is empty, regardless of whether it's `noexcept` or not. Exceptions are not necessarily the best way to assert invariants. Your last paragraph perfectly describes the purpose of `assert`. Possibly related: [N3963](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n3963.pdf). – Oktalist Dec 24 '14 at 20:03
  • Part of item 14 of _Effective Modern C++_ also relates to this exact question, tying it to the notion of wide and narrow contracts. – Oktalist Dec 29 '14 at 17:15

1 Answers1

9

This is not a simple question. In the general case, in C++ you would document your contract stating that the behavior is undefined unless the container is non-empty. The implication is that a call out of context can cause any possible behavior. The use of noexcept in such interfaces then limits what the range of any possible behavior is: it is any possible behavior that does not involve throwing an exception across the boundary.

Why is this important? Where I work we use BSL which is roughly an implementation of an extended C++03 standard library. Part of the library includes utilities for defensive programming and testing. Rather than using assert, the library uses its own flavor of assertion BSLS_ASSERT macros to verify contract violations. The library is built such that user code can control what happens when an assertion is triggered, and that is effectively used in test drivers to verify not only positive behavior (the component does what it should) and negative behavior (it does not do what it shouldn't) but also that it has the appropriate checks for contract violations. For that, an assert handler that throws a particular form of exception is used, and the test driver can then call out of contract and verify that the component is checking the behavior…

Long winded story, the point is that if that if the functions with narrow contracts (can be called out of contract) are marked as noexcept because the implementation should never throw (calling front() on an container), then when called out of contract it cannot throw, and the above mechanism cannot be used to verify that the component will detect contract violations.

This was part of the reason for a late revision of the C++11 standard (just before approval) to remove noexcept from all functions with a narrow contract. You can read more in this proposal

David Rodríguez - dribeas
  • 204,818
  • 23
  • 294
  • 489
  • Hmm, interesting paper. I think I saw a video presentation by those guys describing it, didn't know they had presented it to the committee. I decided to follow the standard. Vector's pop_back is not marked noexcept and has the same semantics as my pop, so I won't mark it noexcept either. – Kian Dec 24 '14 at 21:07
  • 3
    "when called out of contract it cannot throw" I'm not sure that I agree. The postconditions are guaranteed only when the preconditions are met. For sure, it's crazy to think that exception clauses constrain undefined behavior. Your example assumes that contract violations are guaranteed to be detected, which is a very different guarantee than what "undefined behavior" describes. – Ben Voigt Dec 24 '14 at 21:27
  • @BenVoigt: I come from a place that UB is clearly documented in the interfaces, and out of contract calls are detected. Our test drivers ensure that if we ever hit this in production the task will **not** proceed as if nothing happened. What happens when an assert is triggered depends on the configuration in `main`, with the two most common cases being spinning in a loop so that someone can inspect the state of the program and aborting (core file generated for debugging). You are right that UB means anything can happen, we just have a strict policy of detecting UB (when feasible) – David Rodríguez - dribeas Dec 25 '14 at 00:30
  • … debug builds allow for more expensive checks, and in production the more expensive tests might be disabled not to affect overall performance. But during testing, in debug builds the program is build with all assertions on, and each component has been tested to verify the preconditions, giving a high confidence that out-of-contract calls should have been detected before releasing the product. – David Rodríguez - dribeas Dec 25 '14 at 00:32
  • 1
    Annoying they closed this question as a "duplicate" when the accepted answer at the referenced question is (a) wrong in every detail and (b) inferior to this answer. StackOverflow's most irritating failure mode. – Nemo Dec 25 '14 at 19:26
  • Throwing (regardless of `noexcept`) might be another reasonable way to detect UB, which `BSLS_ASSERT` could be configured in `main` to do. – Oktalist Dec 29 '14 at 17:11