The Lakos Rule
The Lakos Rule is one of those STL design principles that is often brought up vaguely during discussions, and then inevitably someone doesn’t understand what it is, or knows what it is but disagrees that the STL generally conforms to it, or whatever. Here’s a quick description of what it is and the precedent for it.
From N3279 “Conservative use of noexcept
in the Library” (March 2011):
A wide contract for a function or operation does not specify any undefined behavior. Such a contract has no preconditions […]
A narrow contract is a contract which is not wide. Functions or operations having a narrow contract result in undefined behavior when called in a manner that violates the documented contract.
[…]
Each library function having a wide contract, that we agree cannot throw, should be marked as unconditionally
noexcept
.
The above guideline is half of the Lakos Rule, as generally understood. The other, more subtle, half of the Lakos Rule — and the half not explicitly stated in N3279 — is the converse:
- A library function having a narrow contract, which has undefined behavior when passed certain parameter values,
should not be marked as
noexcept
.
A commenter on Reddit points out
that the subtler half of the Lakos Rule is stated explicitly in
N3248 “noexcept
Prevents Library Validation”:
Remove
noexcept
specifications from each library function having a narrow contract, typically (but not always) indicated by the presence of a Requirements: clause.
Here’s a concrete example. Suppose our library contains the following array
-like class:
class ArrayOfTen {
int data[10];
public:
int unsafe(int i) const { return data[i]; }
int safe(int i) const noexcept { return (0 <= i && i < 10) ? data[i] : -1; }
};
Of course that’s just an implementation; in WG21-world, we deal with specifications.
Looking at this
implementation, we can surmise that ArrayOfTen::safe(int)
naturally ought to be specified with a wide contract.
No matter what int i
you put in, our specification will tell you exactly what behavior to expect in return.
In terms of Jon Postel’s Robustness Principle, ArrayOfTen::safe(int)
is liberal in what it accepts, and conservative in what it emits.
And, looking at this
implementation, we can surmise that ArrayOfTen::unsafe(int)
naturally ought to be specified with a narrow contract.
If you put in an int i
satisfying 0 <= i && i < 10
, our specification will tell you
exactly what behavior to expect in return; whereas, if you violate that precondition, our specification
gives the library implementor the freedom to do anything they like — even to segfault the program.
In terms of the Robustness Principle, ArrayOfTen::unsafe(int)
is conservative in what it accepts.
The Lakos Rule as stated in N3279 tells us categorically that ArrayOfTen::safe(int)
should be marked noexcept
.
It is a function with a wide contract, and we all agree that it cannot throw; therefore, it is noexcept
. Easy.
The subtler half of the Lakos Rule tells us that ArrayOfTen::unsafe(int)
should not be marked noexcept
.
It is a function with a narrow contract. Thus, even though it does not seem to throw, we should not mark it
noexcept
.
Why not?
Because in the precondition-violating case its behavior is undefined. The library implementor would be within their rights to write something like this:
class ArrayOfTen {
int data[10];
public:
int unsafe(int i) const { return (0 <= i && i < 10) ? data[i] : -1; }
};
Or even something like this:
class ArrayOfTen {
int data[10];
public:
int unsafe(int i) const {
#ifdef PARANOID
if (!(0 <= i && i < 10)) throw std::out_of_range("precondition violation");
#endif
return data[i];
}
};
This is particularly nice because we can write unit tests against the PARANOID
behavior: we can make sure
that our code that uses the unsafe
function never accidentally violates the precondition. And then, to test
our tests, we can write some unit tests that do violate the precondition, and verify that when compiled with
-DPARANOID
, they throw the expected exception.
If we mark unsafe
as noexcept
, then the throw
in its body above will result in a call to std::terminate
,
and our unit tests won’t work.
So the chain of logic here is: Narrow contract — undefined behavior in some cases — maybe even throw
in some
cases — might throw — therefore can’t be noexcept
.
We see this precedent being applied in the STL today. Here are some functions that you might
naively expect to be noexcept
(because they do not throw), but which are actually non-noexcept
(because their contracts are narrow):
std::vector::operator[]
std::array::operator[]
std::optional::operator*
andoperator->
But there is one huge exception to the Lakos Rule: smart pointers. The dereference operator on a
pointer type definitely has a narrow contract. The precondition is that the pointer must be non-null. But
WG21 decided that having noexcept(*ptr) == false
would just be too weird to inflict on
future generations of programmers in the name of consistency.
And therefore the following handful of member functions are noexcept
even though the Lakos Rule implies that they should not be!
std::shared_ptr::operator*
andoperator->
std::unique_ptr::operator*
andoperator->
Postscript: Not the Lakos Rule but still relevant
N3279 provided another guideline as well:
If a library
swap
function, move-constructor, or move-assignment operator is conditionally-wide (i.e. can be proven not to throw by applying thenoexcept
operator) then it should be marked as conditionallynoexcept
. No other function should use a conditionalnoexcept
specification.
Personally I am not a fan of this rule, but WG21 certainly does follow it.
Here are some templates that you might naively expect to be noexcept
with certain template parameters
(because they have wide contracts and do not throw), but which are actually non-noexcept
(because the STL is allergic to conditional noexcept
):
std::priority_queue<int>::empty()
std::optional<int>(int&&)
std::less<int>::operator()(const int&, const int&)
std::less<void>::operator()(const int&, const int&)
std::exchange(int&, int)
std::copy
andstd::transform
Update: Lakos Rule further codified into policy in P0884R0
Nicolai Josuttis wrote a policy paper
P0884 “Extending the noexcept
policy”
(February 2018) which LEWG adopted by 5–14–1–0–0 consensus
at the Jacksonville WG21 meeting. P0884R0 states LEWG’s current-as-of-2018 policy like this:
a) No library destructor should throw. They shall use the implicitly supplied (nonthrowing) exception specification.
b) Each library function having a wide contract (i.e., does not specify undefined behavior due to a precondition) that the LWG agree cannot throw, should be marked as unconditionally noexcept.
c) If a library swap function, move-constructor, or move-assignment operator is conditionally wide (i.e. can be proven to not throw by applying the noexcept operator) then it should be marked as conditionally noexcept.
d) If a library type has wrapping semantics to transparently provide the same behavior as the underlying type, then default constructor, copy constructor, and copy-assigment operator should be marked as conditionally noexcept [so that] the underlying exception specification still holds [for the wrapper].
e) No other function should use a conditional noexcept specification.
f) Library functions designed for compatibility with C code (such as the atomics facility) may be marked unconditionally noexcept.
Again, this is a general policy used by the standard library.
The standard library deliberately deviates from this general policy
in some specific cases, such as shared_ptr::operator*
.
And if you’re writing a non-standard (third-party) library, you very well might want to throw out this policy altogether.
But if you’re writing a proposal for WG21’s consideration, you should generally follow this policy;
and if you’re using the standard library, you should not be surprised when it follows this policy
more often than not.