So, we would like the compiler to generate noexcept-specifications as much as possible.
Thorsten and Bjarne's papers argue for implicit deduction of noexcept-specifications, which is certainly attractive; if the compiler can just do the right thing without any programmer intervention, so much the better. Unfortunately, there are some significant problems with this approach.
int f(); // noexcept(false) int f() { ... } // no deduction is doneor
int f() { ... } // deduction is done int f(); // use previously deduced specification? ill-formed?Or as Daveed wrote, "What if you instantiate a template that references a function f in two translation units: In one f is defined and in the other not. You end up with a silent ODR violation, and quasi-random behavior."
This is the issue Thorsten described as "almost every statement in function templates leak into the noexcept declaration", except now it would happen implicitly, without the user ever writing anything to request it.
template <typename T> struct A { int ar[T()-1]; }; template <typename T> void f (T t, void *) { A<T> a; } // approximately, noexcept(noexcept(A<T>())) template <typename T> void f (T t, int) { } // noexcept(true) void g() { f(1,2); }Here, overload resolution considers f(T,void*). It deduces int for T and starts to produce a function declaration for f<int>(int,void*). When it tries to instantiate the implicit noexcept-specification, it needs to instantiate A<int> to evaluate noexcept(A<int>()), but A<int> is ill-formed, and that error is not in the immediate context of template argument deduction substitution, so the program is ill-formed.
Without implicit noexcept deduction, overload resolution chooses the other function, so f<int>(int,void*) is not instantiated, so A<int> is not instantiated, and the program is well-formed.
This paper proposes a compromise: still require the user to explicitly ask for a noexcept-specification, but let them ask the compiler to determine exactly what that noexcept-specification should be.
int f() noexcept(auto) { return 42; } // noexcept(true) template<typename T> void g(T t) noexcept(auto) { T t2 = t + t; } // approximately, noexcept(noexcept(T(t+t)))The deduction proposed for noexcept(auto) is the same as that proposed by Thorsten and Bjarne: "[a] function is (implicitly) noexcept unless it contains a throw or a call of a non-noexcept function."
My previous thinking had been that it would only be allowed on a definition, not on a forward declaration. If you want to use noexcept(auto) on a function that has a forward declaration, you would need to manually write a matching noexcept-specification for that forward declaration, which limits the usefulness of noexcept(auto) for such functions—but remember from above that under the implicit deduction proposal no deduction would be done for a function defined after an initial forward declaraton, so simply omitting noexcept(auto) in such a case would produce the same result.
However, at the meeting today Pablo suggested an enhancement: allow noexcept(auto) on forward declarations, but make any mention of that function ill-formed until we have seen its definition. That allows patterns like
template <class T> struct A { void f() noexcept(auto); }; template <class T> void A<T>::f() noexcept(auto) { ... }So deduction can be done for functions defined outside the class body, just as long as the definition comes before any uses. I can't think of any additional issues that might arise from this addition.
struct A { void f(int i) noexcept(auto) { if (i > 1) g(i-1); } void g(int i) noexcept(auto) { if (i > 1) f(i-1); } };This testcase would be ill-formed because when we parse f, we have not yet deduced the noexcept-specification for g, so the call to g is ill-formed. Note that this is the same rule as described above for definitions outside the class body.
template<bool> struct M; template<> struct M<true> { int large[100]; }; template<> struct M<false> { char small; }; struct B { template<bool> void maybe_throw(); template<> void maybe_throw<true>() noexcept(auto) { throw 0; } // deduced noexcept(false) template<> void maybe_throw<false>() noexcept(auto) { } // deduced noexcept void f() noexcept(auto) { maybe_throw<(sizeof(B) > 10)>(); }; M<noexcept(f())> data; };Here, similarly, the call to f() in the declaration of B::data is ill-formed because we have not yet deduced the noexcept-specification for f.
Recursion is an interesting case:
int f(int i) noexcept (auto) { if (i == 0) return i; else return f(i-1)+i; }Deducing the noexcept-specification isn't a problem here (whether f throws does not affect whether f throws), but if we don't allow use of functions with pending noexcept deduction, that would seem to apply to the recursive call as well.
template<class T> void f(T t) noexcept (noexcept (T(t+t))); template<class T> void f(T t) noexcept (auto) { T t = t + t; }Then we need to know exactly what the deduced noexcept-specification looks like so that the user can write an equivalent one on the forward declaration. The above transformation probably isn't quite right, but describing a canonical form for users to imitate is a daunting task; it seems simpler just to say that noexcept(auto) on a template is not compatible with any other noexcept-specification and let implementers represent it however is most convenient.
15.4p3:noexcept-specification: noexcept ( constant-expression ) noexcept ( auto ) noexcept
Two exception-specifications are compatible if:Remove 14.8.2p5 (not a substantive change, just removing redundancy):If any declaration of a function has an exception-specification that is not a noexcept-specification allowing all exceptions, all declarations, including the definition and any explicit specialization, of that function shall have a compatible exception-specification. If any declaration of a pointer to function, reference to function, or pointer to member function has an exception-specification, all occurrences of that declaration shall have a compatible exception-specification In an explicit instantiation an exception-specification may be specified, but is not required. If an exception-specification is specified in an explicit instantiation directive, it shall be compatible with the exception-specifications of other declarations of that function. A diagnostic is required only if the exception-specifications are not compatible within a single translation unit.
- both are non-throwing (see below), regardless of their form,
- both have the form noexcept(constant-expression) and the constant-expressions are equivalent,
- both have the form noexcept(auto),
- one exception-specification is a noexcept-specification allowing all exceptions and the other is of the form throw(type-id-list), or
- both are dynamic-exception-specifications that have the same set of adjusted types.
If a declaration of a function has an exception-specification of the form noexcept(auto), then the exception specification for the function is deduced from the definition of the function. If no full-expression (1.9) in the function can throw an exception (in the sense of the noexcept operator, 5.3.7), then the exception specification is equivalent to noexcept(true); otherwise, it is equivalent to noexcept(false). [ Note: This analysis only considers expressions (including expressions implied by the use of various language constructs), so functions that use a catch(...) handler to prevent exceptions from escaping should not use noexcept(auto). —end note ] Until the definition of the function is complete, referring to the function (even in an unevaluated context) is ill-formed.
[ Note: The exception specification for an implicitly-instantiated function is determined when the function is instantiated (14.7.1), which can be triggered by a reference in an unevaluated context. A noexcept(auto) specification can require an implementation to completely instantiate the specialization immediately when it is referenced, rather than defer the instantiation until a later point in compilation. —end note ]
When all template arguments have been deduced or obtained from default template arguments, all uses of template parameters in the template parameter list of the template and the function type are replaced with the corresponding deduced or default argument values. If the substitution results in an invalid type, as described above, type deduction fails.