Thread overview
struct parameterless constructor
Aug 04
monkyyy
August 04

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?

August 04

On Monday, 4 August 2025 at 20:02:48 UTC, Brother Bill 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 know of no sane way to do that, I can imagine a something that could be worth a try if you want to really force it.

August 04

On Monday, 4 August 2025 at 20:02:48 UTC, Brother Bill 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?

I'm not 100% sure why parameterless constructors are banned on structs, but you can work around it by making a static method:

struct Foo {
    int a;
    float b;

    static Foo make() {
        // make "default" version of struct
        return Foo(1, 2.0);
    }
}

void main() {
    Foo f = Foo.make();
}
August 04
On Mon, Aug 04, 2025 at 08:02:48PM +0000, Brother Bill via Digitalmars-d-learn wrote:
> I feel like I am going into the hornet's nest with this discussion.

You are. ;-)


> I have created a struct with some members, and want to have a parameterless constructor that sets the member values at run time.

This will be running up against a wall.  It's possible to scale it, but it will require effort.


> I have seen things like @disable this();  and static opCall(), but
> don't quite understand them.

First you need to understand why D is the way it is.  In the original design, complex objects that require non-trivial construction were intended to be class objects.  They would support inheritance and polymorphism and the other standard OO idioms, and they require a constructor.  That's why the compiler will not generate a default class ctor for you: construction of a class object is expected to be non-trivial and require work from the coder to initialize correctly. You were expected to write the default ctor yourself to put the object in the correct initial state.

Structs, OTOH, were intended to be "glorified ints": i.e., they are by-value types intended to be cheap to copy, and to have a default state (the .init state) that's known at compile-time and can be simply blitted (copied bit-for-bit) from a template known at compile-time and would be in a valid initial state. IOW, just like ints.  Therefore, parameterless default ctors are *not* supported: instead, you'd just specify initial field values in the struct declaration:

```
struct S {
	int x = 123;
	float y = 3.14159;
}
```

The compiler would store the initial values (123, 3.14159) in a read-only struct template that it simply memcpy()'s over every time it needs to initialize an instance of S.  It was intended that this was the only initialization needed, so the language did not support default ctors for structs.

//

Fast forward 20 or so years, and things have changed a bit.  People started using structs for many other things, some beyond the original design, and inevitably ran into cases where they really needed parameterless default ctors for structs.  But since the language did not support this, a workaround was discovered: the no-op default ctor that the compiler generates for each struct (i.e., this()), was tagged with @disable, which is a mechanism to indicate that the initial by-value blit of the struct is *not* a valid initial state.  Then opCall was used so that you could construct the struct like this:

	auto s = S();

which resembles default construction for class objects, at least syntactically.  This wasn't in line with the original design of the language, but it worked with current language features and got people what they wanted without further language changes, so it was left at that, and became the de facto standard workaround for the language's lack of default struct ctors.


> How does one do this, and what are the ramifications of doing so? Or should one just leave the parameterless constructor as is?

If you were to ask me, I'd say generally your code will experience less friction with the language if you just went along with the original design of structs being "glorified ints" instead of fighting with the language all the time.  This will generally lead to less unexpected problems that you might run into if you use the @disable/opCall hack, esp. where generic code is concerned.

Generic code often assumes that T.init gives you a valid instance of a type T, but with types that @disable the default ctor and use opCall to construct instances, this is no longer true so some generic code may not work with your type.

Also, generic code may also assume that they can just declare a variable of type T and assign a value to it later, but with a @disable'd default ctor, this will fail to compile.  This may sometimes lead to several pages long compile errors from deep inside some private internal component of a generic API, that are often hard to understand because it's not immediately obvious why it refuses to compile.  Since metaprogramming / generic code is one of D's (very) strong points, this situation is generally undesirable, unless you prefer writing C/C++-style code in D instead of idiomatic D code.

Having said all that, though, some use cases simply demand default struct construction; in such cases, you have no choice but to work with the @disable/opCall hack.


T

-- 
Written on the window of a clothing store: No shirt, no shoes, no service.
August 05
On Monday, August 4, 2025 5:21:53 PM Mountain Daylight Time H. S. Teoh via Digitalmars-d-learn wrote:
> Fast forward 20 or so years, and things have changed a bit.  People started using structs for many other things, some beyond the original design, and inevitably ran into cases where they really needed parameterless default ctors for structs.  But since the language did not support this, a workaround was discovered: the no-op default ctor that the compiler generates for each struct (i.e., this()), was tagged with @disable, which is a mechanism to indicate that the initial by-value blit of the struct is *not* a valid initial state.  Then opCall was used so that you could construct the struct like this:
>
>   auto s = S();
>
> which resembles default construction for class objects, at least syntactically.  This wasn't in line with the original design of the language, but it worked with current language features and got people what they wanted without further language changes, so it was left at that, and became the de facto standard workaround for the language's lack of default struct ctors.

I know that this advice has been given plenty of times the past, but I'd actually strongly advise against ever doing this. It is less error-prone to simply using an explicitly named static function - to the point that it should actually probably be illegal to declare a static opCall which can be called with no arguments. This is because

    S s;

and

    auto s = S();

and

    auto s = S.init;

are all subtly different, and adding a static opCall makes the situation worse.

For a struct that's not nested (or a nested struct which is static) and which does not disable default initialization,

    S s;

and

    auto s = S();

and

    auto s = S.init;

are all identical. In all cases, the struct will be initialized with its init value. This is the same state that a struct has prior to any of its constructors being called if it's initialized via a constructor.

However, if the struct disables default initialization, then

    S s;

and

   auto s = S();

will fail to compile, whereas

   auto s = S.init;

will compile just fine. This is because the first two attempt to use default initialization, whereas the third explicitly gives the variable its init value. Because of this, some folks advise using S() instead of S.init when you need to explicitly default-initialize a value when you can't just declare the variable and let it be default-initialized (e.g. when passing it to a function). And for a struct to overload static opCall screws with that. Of course, it can then be argued that what really needs to happen is that you do something like

    foo(defaultInit!S())

instead of

    foo(S());

where defaultInit is something like

    T defaultInit(T)()
    {
        T retval;
        return retval;
    }

but regardless, there is code in the wild which will use S() instead of S.init to get the default-initialized value in order to not compile when the type cannot be default initialized, which makes static opCall error-prone (at least if it's going to be used with anyone else's code - and especially if it's going to be used with generic code).

This is one of the negative consequences of having introduced the ability to disable default initialization. But it's not the only major problem here. There's also the issue of non-static nested functions.

For instance, if we take the code

    void main()
    {
        import std.stdio;

        string str;

        struct S
        {
            int i;

            void foo()
            {
                str ~= "foo";
            }
        }

        S s;
        s.foo();
        writeln(str);
    }

it will run just fine and print print "foo". The same would be true if you change

    S s;

to

    auto s = S();

However, if you change it to

    auto s = S.init;

it will segfault. This is because S() is treated as a default-initialized value, which is subtly different from the init value. The init value is what the struct is initialized to prior to any of its constructors being run, and if there are no constructors which are run, and the struct is not nested (or is static), then no additional code is run to initialize the variable. However, if it's nested and not static, then it has a context pointer which points to its outer scope. Default initialization - and S() - will initialize the context pointer (including prior to any constructor calls), whereas if you explicitly initialize the variable with the init value, then it's just going to be the init value, and the context pointer in the init value is null - hence the segfault.

So, code dealing with nested structs needs to ensure that those structs are default-initialized rather than simply given their init value. Of course, originally, there shouldn't have been any difference between the two, but the fact that the ability to give structs context pointers like that was added to the language made it so that default initialization and the init value aren't actually the same thing any longer.

And this is particularly annoying with any parts of the language which need to use the init value to initialize things (e.g arrays and out parameters), because they're not going to work properly with types that need more initialization than that unless they're explicitly given a value.

So, both of these language improvements made default-initialization more complex and made it so that you have to be that much more careful with how you initialize types. In the general case, using the init value explicitly should not be done, because it's not actually default initialization any longer. There are cases where it's still appropriate, but it should be used with care.

Similarly, if it weren't for some folks using static opCall for factory
functions, then S() could be used to ensure default initialization. But
because some folks use static opCall for factory functions, using S() is
error-prone. And because some folks use S() for default initialization,
using static opCall for factory functions is also error-prone. Basically,
S() shouldn't ever be used for structs.

So, because of this mess, I would strongly advise against anyone using static opCall without any arguments. It's begging to be shot yourself in the foot.

Similarly, I'd advise against using S() to do default initialization, because some folks use static opCall, and the S() won't do the right thing any longer for code that expects it to be default initialization.

And explicitly using the init value should only be done with extreme care. Unlike the other two, it's still appropriate at times, but it needs to be handled carefully (particularly in generic code).

So, in effect, adding language features has taken a feature which is nice and simple and made it rather error-prone (particularly for generic code).

And to solve the default initialization problem that some folks have tried to solve with S(), we really probably should add an appropriate template helper to Phobos to be used instead.

So, the TLDR is that variables should only ever be default-iniatialized by simply declaring them - and that structs should never be constructed with no arguments whether it's to default-iniatialize them or if it's to use a factory functions. Factory functions should just get their own names.

- Jonathan M Davis




August 05
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




August 05
On Tue, Aug 05, 2025 at 03:27:06AM -0600, Jonathan M Davis via Digitalmars-d-learn wrote:
> On Monday, August 4, 2025 5:21:53 PM Mountain Daylight Time H. S. Teoh via Digitalmars-d-learn wrote:
> > Fast forward 20 or so years, and things have changed a bit.  People started using structs for many other things, some beyond the original design, and inevitably ran into cases where they really needed parameterless default ctors for structs.  But since the language did not support this, a workaround was discovered: the no-op default ctor that the compiler generates for each struct (i.e., this()), was tagged with @disable, which is a mechanism to indicate that the initial by-value blit of the struct is *not* a valid initial state.  Then opCall was used so that you could construct the struct like this:
> >
> >   auto s = S();
> >
> > which resembles default construction for class objects, at least syntactically.  This wasn't in line with the original design of the language, but it worked with current language features and got people what they wanted without further language changes, so it was left at that, and became the de facto standard workaround for the language's lack of default struct ctors.
> 
> I know that this advice has been given plenty of times the past, but I'd actually strongly advise against ever doing this.
[...]

I agree, fighting against the way the language is designed inevitably leads to trouble.  It may work on the surface but you start running into one bad case after another, and it all just goes downhill from there. Better to use the language as it's intended to be used instead.

I've seen Phobos bugs that were ultimately caused by using @disable where it wasn't intended. All kinds of special cases had to be introduced to uglify the code to handle these exceptional cases, often at the expense of the regular code path.  Alias this is another of these things that looked clever at the time, but in retrospect were misfeatures.


T

-- 
If you want to solve a problem, you need to address its root cause, not just its symptoms. Otherwise it's like treating cancer with Tylenol...