First of all thank you for the detailed responses.
I wrote a response yesterday but somehow the website seems to
have swallowed it.
On Thursday, 11 October 2012 at 12:43:31 UTC, Andrei Alexandrescu
wrote:
We could (after all, C++ does it). There are a few
disadvantages to doing so, however.
1. Defining static data is more difficult. Currently, all
static data is statically-initialized. With default
constructors, we'd need to define the pre-construction state of
such objects anyway, and then change the compiler to call
constructors prior to main(). I find the current design simpler
and easier to use.
This is a good reason. I like the idea of "no code gets run
before main" Running code before main only lead to problems in
C++. However those problems were always merely inconvenient,
never big issues. I think it's not good to allow people to run
code before main, but I think it's a bigger problem to have no
default constructors.
2. Creating a temporary object cannot be anymore assumed to be
a O(1), no-resources-allocated deal. Instead, generic code must
conservatively assume that objects are always arbitrarily
expensive to create. That makes some generic functions more
difficult to implement.
I think that is OK. The generic algorithm should assume that your
object is cheap to create. In C++ all algorithms assume this and
there are few issues. Sure, every now and then you pass an
expensive-to-create object to an algorithm which creates
instances, but that bug is very easy to debug.
3. Two-phase object destruction (releasing state and then
deallocating memory), which is useful, is made more difficult
by default constructors. Essentially the .init
"pre-default-constructor" state intervenes in all such cases
and makes it more difficult for language users to define and
understand object states.
I'm not sure that I understand this. My two ways of interpreting
this are:
A) You mean that the compiler currently assumes that it doesn't
have to call the destructor for objects that are at init. But
after introducing a default constructor it would always have to
call the destructor. I think that's OK. That's an optimization
that's unlikely to give you much gain for types that need a
destructor.
B) You mean that if we introduce a default constructor, there
would still be situations where an object is at init and it's
destructor gets called. For example if people throw exceptions.
And users might be confused by this when their destructor gets
run and their object is at init, instead of the state that they
expect. I think this is OK. It is the same situation that we
currently have with static opCall(). Yes, with the static
opCall() hack people kinda expect that their object isn't always
initialized, so their destructors probably react better to the
state being at init, but I think if the documentation states
clearly "there are situations where the destructor will be called
on an object whose default constructor was not called" then
people can handle that situation just fine.
4. Same as above applies to an object post a move operation.
What state is the object left after move? C++'s approach to
this, forced by the existence of default constructors and other
historical artifacts, has a conservative approach that I
consider inferior to D's: the state of moved-from object is
decided by the library, there's often unnecessary copying, and
is essentially unspecified except that "it's valid" so the
moved-from object can continue to be used. This is in effect a
back-door introduction of a "no-resources-allocated" state for
objects, which is what default constructors so hard tried to
avoid in the first place.
For moving you'd just have to define a state that the source
object is in after moving. Since it's a destructive move I would
expect the object to be at init after moving, as if the
destructor had been called. If that is well defined, then I think
users will be fine with it. This is actually the situation that
we currently have, and users seem to be fine with it. This can be
achieved with a tiny change to the current implementation of
std.algorithm.move: Make it memcpy from init instead of a
statically allocated value.
I'd also like it if we could write all structs so that init is a
valid state of the struct, as Walter suggests. However this is
going to make certain things impossible in the language. Simple
things like having shared data between multiple instances of a
struct. Or counting how often objects of a certain type was
allocated. Or iterating over all instances of a type.
In fact there are parts of the standard library that don't work
because they'd need a default constructor. One of the linked
posts mentions this example from std.typecons:
import std.typecons;
{
RefCounted!(int, RefCountedAutoInitialize.yes) a;
assert(a == 0); // works
RefCounted!(int, RefCountedAutoInitialize.no) b = a;
assert(b == a); // works
a = 5;
assert(b == a); // works
}
{
RefCounted!(int, RefCountedAutoInitialize.yes) a;
//assert(a == 0);
RefCounted!(int, RefCountedAutoInitialize.no) b = a;
assert(b == a); // works
a = 5;
assert(b == a); // doesn't work
}
In this case it just means that that struct needs to be
rewritten, because the whole "RefCountedAutoInitialize" thing is
impossible in D. std.typecons.RefCounted should never be auto
initialized. People have to always initialize it manually. Of
course that also means that any type which uses this has to be
initialized manually. And any type which uses that.
So you can't really have arrays of RefCounted things. Or arrays
of structs which use RefCounter. Basically you will come across
situations where RefCounted doesn't do what you expect and then
you'll have to hack around it. All because structs can't really
have shared data between multiple instances.
But really the bigger problem is not that this makes a small
amount of features impossible. The bigger problem is that this
means that you have to use classes for a large amount of things
for which structs are better suited. Just because I have things
that need to happen at creation time doesn't mean that I want it
to be a class.
Also, as has been brought up several times: It means you have to
fight against the language if you want to use invariants. You
have to always say "if it is in an invalid state, then everything
is OK. Otherwise check if the state makes sense."
You are all making good points, but I think the current state is
not going to work long term in the real world. Heck, it already
introduces problems in the standard library and hacks like static
opCall are accepted wisdom. Sure, introducing a default
constructor would create problems, but those are either minor or
solvable.