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




Reply via email to