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: 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.