Re: Are throwing default constructors bad style, and if so, why?
Dave Harris wrote:
SeeWebsiteForEmail@erdani.org (Andrei Alexandrescu) wrote (abridged):
Consider a File type that provides the usual operations.
Certainly there are situations where the approach is painless and then
it's the natural thing to do. Vector is one example, and File is another.
File in particular because you expect to have to tell it the name of the
file you want to use, so it makes sense to have an explicit "open" step
before using the other operations. I agree that where it is easy it is
often good idea to postpone acquiring resources until they are actually
needed.
The tricky situations are where the object doesn't need any arguments to
be usable, because then it feels unnatural to have an explicit "open"
step, and without that we get a can of worms with resurrection,
unexpected throws, efficiency hits etc. If we want a general rule, then
we have to think about the difficult cases.
I agree. But then if both painful/painless situations are useful, then
it may make sense to classify them in distinct bins. Right now they're
in the same bin.
When File's destructor gets called, the underlying file handle
is closed, thus freeing the resource. [...]
Can the file be reopened post destruction? [...] Yes [...]
When you say "destructor", do you mean "dispose"?
Yah.
In standard C++, the destructor changes the type of the object, from
derived class to base class and then to raw memory. The vtable pointer
gets reset.
struct BaseFile {
virtual bool open( const char *name );
};
struct File {
virtual bool open( const char *name );
};
It sounds like after "File's destructor gets called", you still expect a
call to open() to invoke File::open() rather than BaseFile::open(). If so,
then I'd rather not use the terminology of destruction, destructors etc.
The word "dispose" better reflects the meaning.
Good point. The compiler could do some footwork in calling that
nonthrowing default constructor for File right after destruction.
I don't think the keyword "delete" should be changed to mean dispose in a
GC environment. That's too scary a change for me.
I understand. I do think that would be a sensible decision for D, it
being a new language built with GC from the get-go.
This is a very fertile subject. As an aside, I'm a bit surprised
that most people spend energy solely on rehashing the known
problems with GC + destructors. Yes, it is understood there are
problems. The more interesting and challenging task is to define
a system that does work.
We should be no worse off if instead of the object being destroyed, it is
left in a zombie state with well-defined behaviour at the language level.
This is an opportunity, and I agree it warrants some thought as to how
best to exploit it.
However, although resurrection can make sense, in most cases if you
dispose an object it will mean you are finished with it, and using it
after would be a bug. So I see GC/disposal as providing opportunities to
better detect bugs, and whether to throw an exception or assert depends
on how one thinks bugs should be dealt with: whether one believes in
defensive programming or design by contract.
I think we all need some mindset adjustment when trying to reconcile GC
with deterministic resource management. I'd be the first to admit that
my mindset is not entirely adjusted. For example zombie states and
resurrection are both regarded with contempt, but I see them more as
"unowned" and "owned" states. It is easy to provide a flag and functions
like "own", "disown", and "is_owned" for an object that needs them. In
that model, the user of a resource could ask whether that resource has
an owner, and releasing it explicitly if it has no owner.
(That's also my conclusion about finalisers, although for different
reasons. I don't believe there is much useful that can be done with a
finaliser except to detect the mistake of failing to dispose the object.)
Yah finalisers are pretty botched.
So to me the default constructed state is the opposite of the disposed
state. One is the start of the object's lifetime, and the other is
effectively the end. And to tie these together, and say that the default
constructed state should be the same as the disposed state, is a bit
unnatural. Much of what you write seems to be tantamount to saying
defensive programming wins.
I don't particularly like defensive programming, but I also wanted to
make a simple proposition that does not add a specific state.
But then again: in discussing the disadvantages of
an approach, don't forget the advantages.
It sounds like the main advantage you seek is for default construction
not to throw. But in C++, construction is exactly when I do expect the
object to throw, because of RAII. (And because they are often created on
the heap, and operator new() can throw.) I am more surprised if
subsequent, apparently innocuous methods throw (because they are secretly
acquiring resources that should have been got in the constructor).
I think things can be worked out even if the default constructor does
throw. (It would require destructors to be able to throw, which is
possible and IMHO needed by C++.) But allow me this conjecture:
1. If a default constructor may throw, it can be presumed that it failed
to acquire proper state.
2. Because of that it can be presume that whatever was needed was in
scarce supply.
3. Then that means that supply should be refunded pronto, otherwise
other objects will have even less of it.
4. Then the post-destructed state should not default-construct the
object because that works straight against the notion of pronto refunding.
So we go back to the idea that post-destroyed state is distinct from all
other states yet valid, which IMHO brings more problems than it solves.
Andrei
--
[ See http://www.gotw.ca/resources/clcm.htm for info about ]
[ comp.lang.c++.moderated. First time posters: Do this! ]