On Monday, August 4, 2025 2:02:48 PM Mountain Daylight Time Brother Bill via 
Digitalmars-d-learn wrote:
> I feel like I am going into the hornet's nest with this
> discussion.
>
> I have created a struct with some members, and want to have a
> parameterless constructor that sets the member values at run time.
>
> I have seen things like @disable this();  and static opCall(),
> but don't quite understand them.
>
> How does one do this, and what are the ramifications of doing so?
> Or should one just leave the parameterless constructor as is?

Realistically, you really shouldn't ever do this in D.

D as a whole is designed with the idea that all types can be
default-initialized. It simplifies various aspects of the language and helps
make it memory-safe. If a struct is used somewhere where it's
default-initialized rather than explicitly constructed, it starts with that
struct's init value - that is what each of the member variables are directly
initialized with (and all of that is calculated at compile time). This
avoids issues with garbage values like you get in C/C++. And if a value is
explicitly constructed, then the struct is initialized to its init value
before the constructor is called, ensuring that the constructor isn't
dealing with garbage either.

On the whole, this has simplified the language, but it does come with the
downside that default construction doesn't exist in D. So, you can't just
declare a variable and expect it to run any code. You need to explicitly
call a function (be it a constructor or some function that returns that
type) to be able to run code when initializing a variable, e.g.

    auto s = S(42);

or

    auto s = foo();

So, at its core, what D does with default initialization is pretty simple.
You get the init value. And D takes advantage of this in a number of places
(e.g. declaring a dynamic array of a given size will initialize all of its
elements to the element type's init value).

Unfortunately, the addition of the ability to disable default initialization
and the ability to have nested structs has complicated things a bit, and not
in a good way. And adding static opCall into the mix just makes things
worse.

To quickly explain @disable this();, what it does is make it so that that
type cannot be used in any context where it's default-initialized. That
means that

    S s;

won't compile, and neither will

    auto arr = new S[](10);

The struct can still be explicitly initialized with its init value, e.g.

    auto s = S.init;

but the idea is that at that point, it really should just be initialized
using a constructor - presumably because the programmer wanted to require
that some piece of code be run when the struct is initialized.

To quickly explain opCall, it's the overloaded operator which lets you call
a variable like it's a function, e.g.

    S s;
    s(42);

And like any function, it can have zero or more arguments, depending on the
parameters that the function has.

static opCall is then the overloaded operator which lets you call a type
like it's a function, e.g.

    auto s = S(42);

This is mostly a terrible idea, because you can just use a constructor for
that, and while you _can_ make static opCall return a different type
entirely, that's just going to be really confusing, because everyone is
going to expect S(42) to be calling a constructor to construct an S.
However, some folks like to use static opCall to try to act like a struct
has a default constructor. Normally,

    auto s = S();

would default-initialize a type, similar to

    auto s = S.init;

but if S has a static opCall which takes no arguments, then it would call
that static opCall. But it's still not a default constructor, because
something like

    S s;

is not going to call that static opCall. It's just going to
default-initialize the variable. So, trying to use it as a default
constructor is going to be error-prone.

Some folks have then tried to combine that with disabling
default-initialization (i.e. @disable this();) so that it's then not legal
to do

    S s;

so that your static opCall can't be bypassed with default initialization,
but you still have to explicitly call S(), and of course, it means that you
can't use S in any situation which requires default initialization (which
can get annoying, because various parts of the language were designed with
the assumption that all types can be default-initialized, and thus they
either don't work at all with such types that don't allow it, or they only
partially work with such types).

The situation gets even worse though, because some generic code which wants
to use the default-initalized value for a type will use S(), e.g.

    foo(S());

instead of

    foo(S.init);

This is for two reasons:

    1. It will fail to compile if S can't be default-initialized, whereas
           auto s = S.init;
       would compile just fine even if S has @disable this();. So, if you
       don't want to bypass @disable this();, you need to not use the
       type's init value.

    2. Nested structs have a context pointer (to access their outer scope)
       which only gets a proper value if the struct is default-initialized.
       And while historically, default initialization meant that a type was
       initialized with its init value, for nested structs, that's not true.
       They get initialized with their init value, and then their context
       pointer is initialized based on where they're defined. So, any code
       which uses the init value rather than using default initialization
       will end up with a nested struct whose context pointer is null, and
       if it ever tries to use its context pointer, then you'll get a
       segfault. Using S() rather than S.init avoids the problem, because
       S() does default initialization, whereas S.init is just the init
       value.

So, this means that there is D code out there which explicitly uses S() for
default initialization, and if you declare a static opCall for S which takes
no arguments, then it's going to have problems if it's ever used with such
code.

So, unfortunately the fact that nested structs and the ability to disable
default initialization were added to the language has complicated things
somewhat.

But at the end of the day, D is designed around the idea that all types can
be default-initialized, and trying to fight that means that you're going to
be fighting warts in the language. Disabling default initialization will
make a variety of language features not work with your type, and using
static opCall is a bug waiting to happen. It may work just fine if you're in
control of all of the code that you're using, but if you call into library
code anywhere with your type, you could easily run into trouble.

And really, if you want a factory function, you can just give it a different
name, e.g.

    struct S
    {
        ...
        static S make()
        {
            ...
        }
        ...
    }

Using static opCall is really just syntactic sugar that risks causing you
problems. It doesn't actually buy you anything. Folks just like using it,
because it looks like a default constructor even though it isn't.

And if you actually _need_ to disable default initialization for a struct in
order for it to work properly, then you can do it, but be prepared for
various language features (like dynamic arrays) to not work very will with
your struct.

So, at the end of the day, I'd strongly advise against trying to have
structs with anything like a parameterless constructor. There may be cases
where it still makes sense, but you'll be fighting the language pretty much
any time that you try it. As general rule, it really only makes sense in
very restricted circumstances, and even then, it's better to use a factory
function with an actual name.

- Jonathan M Davis




Reply via email to