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.

Reply via email to