On 2/20/12 4:04 PM, H. S. Teoh wrote:
On Mon, Feb 20, 2012 at 03:12:08PM -0600, Andrei Alexandrescu wrote:
I still don't like the idea of using Variant[string], though.
I don't like it, either. I mean not "like" like. It's an approach
suggested by necessity.
(1) It doesn't allow compile-time type checking. This is a big minus, in
my book.
In mine, too. Literally. We're on the same boat.
(2) It's overly flexible. Anyone along the call stack can insert
(hopefully NOT delete!!) additional data into the Exception object, as
the stack is unwound.
But that's a plus. It means the approach scales up to any number of
control flows, of which there are combinatorially many. Defining one
type for each... well you wouldn't "like" that, either.
By the time it gets to the final catch() block,
you cannot guarantee a particular field you depend on will be defined.
Indeed. If you depend on anything you'd want to catch the specific type.
Say if your call graph looks something like this:
main()
+--func1()
+--func2()
| +--helperFunc()
| +--func3()
| +--helperFunc()
+--func4()
+--helperFunc()
Suppose helperFunc() throws HelperException, which func1's catch block
specifically wants to handle. Suppose func2() adds an attribute called
"lineNumber" to its catch block, which then rethrows the exception, and
func3() adds an attribute called "colNumber".
Now how should you write func1()'s catch block? You will get all
HelperException's thrown, but you've no idea from which part of the call
graph it originates.
If it comes from func3(), then you have both
"lineNumber" and "colNumber". If it comes before you reach func3(), then
only "lineNumber" is defined. If it comes from func4(), then neither is
present.
Exactly. So you suggest adding one type for each possible control flow?
Are you sure this scales beyond a toy example?
So your catch block degenerates into a morass of if-then-else
conditions.
No, precisely on the contrary. You catch blockS degenerate into a morass
of catch (This) { ... } catch (That) { ... } catch (TheOther) { ... }.
That is fine if the code in different "..." does very different things,
but it's a terrible approach if all do the same thing, such as
formatting. That shouldn't make anyone feel better than using a morass
of if/else.
The code with Variant[string] does not need combinatorial testing if it
wants to do a uniform action (such as formatting). It handles formatting
uniformly, and if it wants to look for one particular field it inserts a
test.
And then what do you do if you're depending on a particular
field to be set, but it's not? Rethrow the exception? Then you have the
stack trace reset problem.
Don't forget that Variant[string] does not preclude distinct exception
types. It's not one or the other.
Whereas if HelperException always has the the same fields, the catch
block is very straightforward: just catch HelperException, and you are
guaranteed you have all the info you need.
HelperException can definitely be there. It can only help if there's
additional information associated with it.
Then if func3() wants to add more info, create a new exception derived
from HelperException, and add the field there. Then in func1(), add a
new catch block that catches the new exception, and makes use of the new
field.
They call that non-scalable code bloat.
This does introduce a lot of little exception classes, which you could
argue is class bloat, but I don't see how the Variant[string] method is
necessarily superior. It comes with its own set of (IMHO quite nasty)
problems.
Variant[string] is not superior because it doesn't compete against
anything. It's a simple addition to the primitives available to the base
Exception class.
Andrei