What is the case against a struct post-blit default constructor?

Malte Skarupke malteskarupke at web.de
Thu Oct 11 18:50:14 PDT 2012


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.


More information about the Digitalmars-d mailing list