I've come across this issue in several contexts recently, and some of the opinions expressed on it have surprised me. Here's a first simple example:
void f(std::vector<double> x) {};
The question is: is it acceptable to document or describe f
as offering the no throw guarantee? Similarly, I suspect that since the exception is not generated from f's body, using noexcept
is technically kosher. But should one mark it noexcept
? An example would be, for instance, an optimized version of set
that somehow finds it useful to add the requirement that the templated comparator must not throw. It detects this at compile time using a static assert and causes an error. Yet, someone could write a comparator for vectors that takes by value, and mark it noexcept, and use it with this version of set
. Is it then the container author's fault if this causes poor behavior? Or the person who marked the comparator noexcept?
To take another example that involves another type of exception guarantee, consider:
void g(std::vector<double> x, std::unique_ptr<int> y);
Can this function ever offer the strong guarantee?
std::vector<double> p{1.0, 2.0};
auto q = std::make_unique<int>(0);
bool func_successful = true;
try {
g(p, std::move(q));
}
catch (...) {
func_successful = false;
}
if (!func_successful)
assert(q);
I would argue that if the assertion can ever fail, then g does not offer the strong guarantee, because q
was not empty immediately prior to calling g (remembering that std::move
doesn't actually move anything). And it can fail: order of parameter evaluation is unspecified, so y may be constructed first emptying q, followed by x being constructed and throwing. Is the function still responsible for this even though it does not technically occur in the body, but in the act of calling the function?
Edit: I was going to mention that Herb Sutter talks about this issue here: https://youtu.be/xnqTKD8uD64?t=1h11m56s. He says that marking such a function noexcept is "problematic"; I'm hoping for a more detailed response.
The C++ standard does not believe that it is. Consider its treatment of nothrow when applied to assignment.
is_assignable
is true if, forT
andU
, this expression is valid:std::declval<T>() = std::declval<U>()
.is_nothrow_assignable
is true if that expression is noexcept, in its entirety. The same goes for copy/move assignment (obviously with the same type).The initialization of the
operator=
parameter is part of the expression. And therefore, if that initialization can throw, then the assignment is not nothrow. There's a working group paper discussing this matter in some depth (primarily around move assignment).The question of whether you should mark this function
noexcept
is a separate matter. But a function simply beingnoexcept
should not be confused with the belief that parts of a function call expression will never throw.My personal feeling on the matter is that you shouldn't be marking random functions
noexcept
to begin with. You should use them with special member functions and other very specific places where you have an explicit need to use it. That is, it should be something more than a semantic marker for a user. Usenoexcept
when someone is actually going to do metaprogramming and choose to call or not call that function based on whether it isnoexcept
.The standard (5.2.2 Function call [expr.call] §4) says that parameter initialization and destruction take place in the context of the calling function. So yes, it is technically kosher to declare
f
asnoexcept
.It does make sense though, even if it is counter-intuitive at first.
f
could be called with an rvalue, in which case the move-ctor would be used to initialize the parameter, so even the whole construct of calling the function (including parameter setup and tear-down) would still benoexcept
. Orf
could be called with an empty vector, in which case the whole construct would still effectively benoexcept
- at least with what I would consider "reasonable" library implementations.Also consider functions with const-ref parameters:
x
isnoexcept
allright. The call iny
however is not, because it includes a (possibly throwing) implicit conversion. So if declaringf
asnoexcept
was bad style, wouldn't the same have to be said forx
?So I think C++ programmers should do one of two things: not use and not rely on
noexcept
specifications, or internalize the rules and always be weary of what they're doing at the call site.Regarding the second question (strong guarantee)... I'd say that a function can never make claims about the arguments it's called with. Also it can never make claims about what happens to its would-be parameters if it's never actually called.
If the strong guarantee that you have in mind includes such claims, then I would agree that the function can not give that kind of strong guarantee.
Although I wouldn't necessarily agree that the strong guarantee - for a function with a signature like this - should include such claims. What I would say is: if such claims are necessary for the strong guarantee to make any kind of practical sense, then the function should not use by-value parameters.
OTOH, if such claims are not necessary for the strong guarantee to make practical sense, then I see no reason why I would "forbid" it. E.g.
IMO the "natural" strong guarantee for this function would be that it either appends all elements in
b
toa
, or, in case an exception is thrown, leavesa
unchanged. I would never assume that the guarantee includes any claim about what happens tob
. Even less any claim about what happens to whatever argument was used to initializeb
.