Pregunta

I have read lots of arguments about (not) using throw(X) in function signatures, and I think the way it's specified in ISO C++ (and implemented in current compilers) it's rather useless. But why compiler cannot simply enforce exception-correctness at compile time?

If I write function/method definition that contains throw(A,B,C) in it's signature, the compiler should not have many problems in figuring out if implementation of given function is exception-correct. That means that function body has

  • no throw's other than throw A; throw B; throw C;;
  • no function/method call that have throw-signatures less restrictive than throw (A,B,C);

, at least outside try{}catch() catching other thrown types. If compiler raises error if these requirements are not met, then all functions should be "safe" and no runtime functions like unexpected() would be required. All this would be assured at compile-time.

void fooA() throw (A){
}

void fooAB() throw (A,B){
}

void fooABC() throw (A,B,C){
}


void bar() throw (A){

    throw A();   // ok
    throw B();   // Compiler error

    fooA();      // ok
    fooAB();     // compiler error
    fooABC();    // compiler error

    try{
       throw A();   // ok
       throw B();   // ok
       throw C();   // Compiler error

       fooA();   // ok
       fooAB();  // ok
       fooABC(); // compiler error
    } catch (B){}
}

This would require that all non-C++ realm code was either throw() specified by default (extern "C" should assume it by default), or if there is some exception interoperability then appropriate headers (for C++ at least) should be throw-specified as well. Failure to do so could be compared to using headers with different function/method return type in different compilation units. While it's not producing warnings or errors, it's obviously wrong - and as thrown exceptions are part of signature, they should also match.

If we enforce such constrains it would have three effects:

  • It would remove all those implicit try{}catch blocks, otherwise required for runtime checking, thus improving exception-handling performance.
  • The argument that "Exceptions are making our library too big, so we switch them off"; would disappear, as most of additional code lies in those unnecessary implicit throw/catch instructions at every function call. If code was properly throw-specified, most of it wouldn't be added by compiler.
  • It would make most of the programing world furious, as nobody ever seemed to like exceptions. Now, as these are actually usable, we need to learn how to use them.

If we used some compatibility compiler flags for older code it wouldn't break anything, but as new exception-code would be quicker, there would be a good motivation not to write new code using it.

To summarize my question: Why such enforcement is not required by ISO C++? Are there any strong reasons for it not to be? I always thought that exceptions are just another function's return value, but one that is governed automatically, so you can avoid writing functions like

std::pair<int, bool> str2int(std::string s);
int str2int(std::string s, bool* ok);

plus additional auto-destruction of variables and propagation through multiple functions up the stack so you don't need code like

int doThis(){

    int err=0;

    [...]

    if ((err = doThat())){
        return err;
    }

    [...]

}

;and if you can require function to return only the correct type, why can't you require it to throw it?

Why can't exception specifiers be better? Why weren't they made like I described from the start?

PS I know there might be some issues with exceptions and templates - depending on answers to this question perhaps i will ask another one about it - for now let us just forget templates.

EDIT (in response to @NicolBolas):

What kind of optimization could the compiler do with exception class X that it couldn't do with Y?

Compare:

void fooA() throw (A){
}

void fooAB() throw (A,B){
}

void fooABC() throw (A,B,C){
}


void bar() throw (){

    try{
       fooA();
         // if (exception == A) goto A_catch
       fooAB();
         // if (exception == A) goto A_catch
         // if (exception == B) goto B_catch
       fooABC();
         // if (exception == A) goto A_catch
         // if (exception == B) goto B_catch
         // if (exception == C) goto C_catch
    }
    catch (A){  // :A_catch
      [...]
    }
    catch (B){  // :B_catch
      [...]
    }
    catch (C){  // :C_catch
      [...]
    }
}

and:

void fooA(){
}

void fooAB(){
}

void fooABC(){
}


void bar(){

    try{
       fooA();
         // if (exception == A) goto A_catch;
         // if (exception == B) goto B_catch;
         // if (exception == C) goto C_catch;
         // if (exception == other) return exception;
       fooAB();
         // if (exception == A) goto A_catch;
         // if (exception == B) goto B_catch;
         // if (exception == C) goto C_catch;
         // if (exception == other) return exception;
       fooABC();
         // if (exception == A) goto A_catch;
         // if (exception == B) goto B_catch;
         // if (exception == C) goto C_catch;
         // if (exception == other) return exception;
    }
    catch (A){  // :A_catch
      [...]
    }
    catch (B){  // :B_catch
      [...]
    }
    catch (C){  // :C_catch
      [...]
    }
}

Here I included some pseudo-code that compiler would have generated no assembly level. As you can see, knowing what exceptions you can get can reduce amount of code. If we had some additional variables to destroy here, additional code would have been even longer.

¿Fue útil?

Solución

Compiler-verified exceptions as part of a function's signature have two (theoretical) advantages: compiler optimizations and compile-time error checking.

What is the difference, in terms of the compiler, between a function that throws exception class X and class Y? Ultimately... nothing at all. What kind of optimization could the compiler do with exception class X that it couldn't do with Y? Unless std::exception were special (and X were derived from it, while Y was not), what would it matter to the compiler?

Ultimately, the only thing a compiler would care about in terms of optimization is whether a function will throw any exceptions or not. That's why the standards committee for C++11 ditched throw(...) in favor of noexcept, which states that the function will throw nothing.

As for compile-time error checking, Java clearly shows how well this works. You're writing a function, foo. Your design has that it throws X and Y. Other pieces of code use foo, and they throw whatever foo throws. But the exception specification doesn't say "whatever foo throws". It must list X and Y specifically.

Now you go back and change foo so that it no longer throws X, but now it throws Z. Suddenly, the entire project stops compiling. You must now go to every function that throws whatever foo threw just to change its exception specification to match foo.

Eventually, a programmer just throws up their hand and says that it throws any exception. When you abandon a feature like that, it's a de facto admission that the feature is doing more harm than good.

It's not that they cannot be useful. It's just that actual use of them shows that they're generally not useful. So there's no point.

Plus, remember that C++'s specification states that no specification means that anything will be thrown, not nothing (as in Java). The simplest way to use the language is exactly that way: no checking. So there are going to be plenty of people who don't want the bother of using it.

What good is a feature that many don't want to bother with, and even those who do will generally get a lot of grief out of it?

Otros consejos

That means that function body has

no throw's other than throw A; throw B; throw C;;
no function/method call that have throw-signatures less restrictive than throw (A,B,C);

Don't forget that code can be compiled on different machines at different times and only linked together at runtime, via dynamic libraries. The compiler may have a local version of the signature of a called function but it may not match with the version actually used at runtime. (I presume the linker could be modified to forbid linking if exceptions don't match perfectly, but that could introduce more annoyances than it solves.)

Throw specifications could be useful in some cases, especially on embedded systems, where the alternative would be to forbid exceptions altogether. Among other things, if the only functions that are allowed to throw are those which are explicitly specified as such, then it is possible to eliminate exception-handling overhead when calling functions which cannot throw. Note that even in systems which use "metadata" for exception handling, the fact that an exception handler must be able to make sense of stack frames precludes optimizations which might otherwise be possible.

Another situation where throw specifications could be useful would be if the compiler allowed catch statements to specify that they should only catch exceptions which did not bubble up through any layers where they were not "expected". One major weakness with the exception handling concepts in C++ and languages which borrow from it is that there's no nice way to distinguish an exception which occurs in a called routine, for a condition documented thereby, from an exception which occurs in a nested subroutine, for reasons the directly-called routine did not expect. It would be helpful if a catch statement could act upon the former without catching the latter.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top