Jason Merrill
2010-11-11
Revision 10
N3207=10-0197
noexcept(auto)
Introduction
As discussed extensively in reflector traffic and
Thorsten and
Bjarne's
papers, use of noexcept-specifications as specified in the FCD is
extremely cumbersome, forcing users to write their functions over again in
a mangled form in the noexcept-specification. This is a gratuitous test
for whether the user knows what transformation to apply and which pieces of
the function matter; as Thorsten writes, "a user-maintained noexcept
increases the likelihood that the specification is not correct. In turn,
this implies (a) an increased chance that client code terminates
unexpectedly, or (b) that optimization opportunities are lost. (Note that
providing correct warnings is also undecidable."
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.
Problem 1
Problem 1: If a function has multiple declarations, the order in which the
declarations are seen can affect how deduction is done, and potentially
lead to ODR violations. Consider
int f(); // noexcept(false)
int f() { ... } // no deduction is done
or
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."
Problem 2
Problem 2: Implicit noexcept deduction for function templates requires
tentative instantiation of much of the function body which previously was
not required.
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.
Other problems?
Since we have no implementation experience, it seems fair to assume that
implicitly adding noexcept-specifications to almost all functions is likely
to have other unexpected consequences that we haven't thought of yet.
A compromise
So, having to write everything out explicitly is horribly cumbersome, and
having it all implicitly deduced has significant problems, what's left?
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."
noexcept(auto) on declarations
Clearly, we can only deduce a noexcept-specification from a definition.
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.
Ordering dependencies
Bjarne's slide
presentation included a couple of interesting examples from Jens, which
I will adapt here; I believe that this proposal resolves these questions in
a straightforward way.
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.
Deduction specification
The tricky part of the specification is describing exactly how the
deduction is done for a template. If we want to be able to write, say,
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.
noexcept sfinae
Discussion of this proposal led me to think that currently the
exception-specification of a function is not exposed to SFINAE, and that
this could lead to hard errors from substitution of noexcept-specifications
in templates not chosen by overload resolution. Further core discussion
led to the conclusion that this substitution can be deferred until overload
resolution is complete, so it would only produce a hard error when we
actually try to use the function.
Proposed Wording
In 15.4p1:
noexcept-specification:
noexcept ( constant-expression )
noexcept ( auto )
noexcept
15.4p3:
Two exception-specifications are compatible if:
- 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 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.
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 ]
Remove 14.8.2p5 (not a substantive change, just removing redundancy):
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.