Paavo Helde writes:
On 03.01.2025 17:50, Sam wrote:
Paavo Helde writes:
>
It might be obvious to you, but not for everybody. What exact problem can be solved by validating thrown exception classes at compile time?
>
Ummm… Making it logically impossible to throw an uncaught exception? The code will refuse to compile because it will be ill-formed. Getting rid of std::uncaught_exception(), completely?
>
I have never used std::uncaught_exception().
It's not typically used directly. Its primary user is your suffering C++ compiler, when it automatically injects a call to it in the code path for uncaught exceptions.
Well, I think I tried to use it once, 20 years ago, but it did not work in any useful way IIRC.
>
Anyway, avoiding uncaught exceptions is easy, one just has to place catch(...) in main() and in all thread functions. Problem solved.
Sure, catch(...) deals with it. But it gives you nothing useful to work with. A valiant attempt to send a bat-signal to std::current_exception will eke out a few useful bits, if one's lucky, but the end result will just be more spaghetti code.
If C++, the language, forced uncaught exception to be ill-formed then this will pretty much negate the need for a wildcard catch, solving that problem. I suppose that a wildcard catch can still be around, if someone needed it. But it'll be viewed as an indication of lack of core C++ knowledge and experience.
Double exceptions appearing during stack unwind are more tricky. Here some compiler support enforcing noexcept would be nice indeed. But this would not need any more detailed exception specifications.
I think that double exceptions would naturally be minimized, in my ideal C++ world. I think that exception-correctness would naturally eliminate them. Exception correctness enforces strict exception handling, making it more prominent.
Think about it. Destructors would be a hard noexcept. C++ then forces you to write an explicit catch for any exceptions that get thrown from a destructor. Mission accomplished.
And how would a developer of a base class know which exceptions I might want to throw from an overridden virtual function in a derived class, which might be developed and compiled fully separately from the base class?
>
The developer doesn't need to figure it out, the compiler will tell him.
>
You misunderstood my point. Yes, the compiler will refuse to compile my code because a new algorithm implementation throws a new kind of SocketException or whatever. So what next?
Fix the code, that's what's next.
The goal is to compile my code, not to get compiler errors.
The only way to achieve the lofty goal is to fix the compiler errors. So: fix them. This is no different than any other ABI break from a new library version.
Now I have to change the signatures of the umpteen functions in the call stack in umpteen classes in umpteen projects to let the new required exception through (or translate it to something else via an even more questionable hack).
This is the logically equivalent to a changed return type, a changed parameter, or a type of any other object that's used commonly in the code.
I recall one time when I replaced a container that was used for important stuff in my code. A whole bunch of functions returned this container. The container used to be a std::list. It was now a std::vector. I don't remember exactly how many functions referenced it. Maybe 30-40, or so.
Now, I'm going to tell you something that will blow you away. Prepare to be shocked. This revelation will rock your world. Make sure you're sitting down. Seal all exits. Cancel the three-ring circus. Secure all elephants in the zoo. Guess what?
I did not have to monotonically go through and update the 30-40 function declarations.
Are you still with us, my friend? You survived such a shocking reveleation? Good! Because, you see, C++ has this highly advanced, space-age tool that only a select few C++ gurus know about.
It's called a "typedef". These days it's also known as a "using" declaration.
All I had to do is change one typedef, and all those functions were now returning a vector. After changing
typedef std::list<Widget> all_my_widgets;
to
typedef std::vector<Widget> all_my_widgets;
Then all the existing code, referencing "all_my_widgets", or "all_my_widgets::iterator", or "all_my_widget_instance.begin()", and so on – guess what? It's still compiled! OMIGOSH!!!!
I think I only needed to fix one or two things, that specifically depended on the actual underlying implementation. The rest of the code, that didn't care for the actual container, remained unchanged, and only had to be recompiled.
So, in your case, your "umpteen functions" just need to be declared correctly. Here's what happened on Alternate Earth, where exceptions were actually implemented like that: the point you raised would've been identified as a pain point early on, and something analogous to "typedef", but for throw specifiers, was added to the language.
I dunno, maybe something like:
typedef throws(ClassA, ClassB) AlgoThrownClasses;
Then you go ahead and declare your algorithm:
void my_algorithm(algorithm_info_t &) throws(AlgoThrownClasses)
I'm now married to the actual syntax or grammar, that's just the syntax that was used on Earth 2. Earth 3 used different syntax, and I don't recall what it is, so I'll just focus on Earth 2's implementation. The code you would write, on Earth 2, would use the alias:
void my_function_that_uses_algorithmm() throws(AlgoThrownClasses)
{
algorithm_info_t info;
my_algorithm(info);
}
Your Earth 2 doppelganger also threw its own exceptions:
void my_function_that_uses_algorithm() throws(AlgoThrownClasses,MyError)
All callers of my_function_that_uses_algorithm(), on Earth 2, used the same alias.
* Pedantic note: your Earth 2 doppelganger will achieve greybeard status by using, instead, "typedef throws(AlgoThrownClasses, MyError) MyThrownClasses" and use that alias, instead.
Now, my friend, guess what happened if the algorithm was updated on Earth 2 to throw a different exception:
typedef throws(ClassA, ClassB, ClassC) AlgoThrownClasses;
You'll be shocked and awed to learn that all those "umpteen functions" will still compile just fine, on Earth 2.
The only additional work was done wherever in your code, on Earth 2, you caught and handled the exceptions from the algorithms. And your suffering C++ compiler, on Earth 2, will tell you exactly where the code needs to be fixed, because it will now have an uncaught exception.
I wish I could move to Earth 2. But, on Earth 2, Donald Trump just won his third term, and their border is already closed, and will be closed for the foreseeable future.
Now, another thing just occured to me: what if the algorithm was changed and it no longer throws one of the exceptions on Earth 2, and the typedef alias was changed accordingly?
Well, in the existing code this results in a catch for an exception that cannot possibly be thrown.
On Earth 2 it's an informational compiler diagnostic.
-Wall -Werror happily deals with it, on Earth 2.
The point is that those umpteen functions could not care less about what exceptions go through them as they will just pass them all through. So what's the point?
The point is that Earth 2 designed C++ properly, and on Earth 2 they learned how to use C++ properly.
At any frame in the call stack, if I want to catch a specific exception, I can do that. Everything else goes up to the top-level catch(...) and gets logged or converted to an error message as appropriate.
>
Why should I arbitrarily restrict what exceptions I can throw in some function?
You can throw whatever you want, on Earth 2, you just have to declare what you throw, and force yourself to catch everything you throw, and your C++ compiler will force you to do the right thing. Resistance is futile. You will be caught.