 | Posted by Jonathan M Davis in reply to Per Nordlöw | Permalink Reply |
|
Jonathan M Davis 
Posted in reply to Per Nordlöw
| On Monday, October 13, 2025 4:44:58 AM Mountain Daylight Time Per Nordlöw via Digitalmars-d-learn wrote:
> Given a `struct S` is, `S()` always equivalent to `S.init`? I
> wonder which is preferred in an (parameter default value)
> expression that should evaluate to the default value of `S`.
>
> I would like to use `S()` as its shorter to type than `S.init` but I'm uncertain whether it can trigger execution of code despite D disallows nullary constructors.
Originally, there wasn't supposed to be any difference between these
S s;
auto s = S.init;
auto s = S();
but changes to the language over time have made it so that they're all subtly different. Strictly speaking, S.init is the value used to default-initialize an object, but it's not necessarily the default-initialized value.
The main stuff that causes differences are:
1. Structs which disable default initialization.
2. Non-static nested structs.
3. Structs which define static opCall.
If a struct disables default initialization, then
auto s = S.init;
is legal, but
S s;
auto s = S();
aren't. So, in that particular situation, you almost certainly don't want to use S.init.
If a struct is a non-static nested struct, then
S s;
auto s = S();
will both properly initialize the struct. However,
auto s = S.init;
will properly initialize all of the struct _except_ for the context pointer, which will be null. So, if the struct ever actually uses its outer context, it will segfault. So, in that situation, you almost certainly don't want to use S.init.
And if a struct defines a static opCall which can be called with no arguments, then
S s;
auto s = S.init;
will properly initialize the struct, whereas
auto s = S();
will do whatever the static opCall does (and there's no guarantee that the static opCall even returns a value let alone that it returns an S). So, in that situation, you almost certainly don't want to be using S() (and if you do, it's because you know exactly what type you're dealing with and how it will behave).
So, some folks have taken to using S() rather than S.init, because it doesn't bypass the restrictions when disabling default initialization, and it fully initializes non-static nested structs, but in generic code at least, it's still potentially going to do the wrong thing, because a struct could define static opCall.
This means that in the general case (and particularly with generic code), code should use neither S.init nor S(). Rather, it should let a variable default-initialize itself and use that. Using either S.init or S() will result in the code doing the wrong thing at least some of the time. There are still specific cases where you might want to use one or the other, but normal code really shouldn't be using either unless you know exactly how the types you're dealing with will behave - which of course means that generic code shouldn't use either S.init or S() unless it has additional checks to make sure that the type doesn't disable default initialization and doesn't define static opCall.
This of course is kind of annoying in cases where you need to pass a default-initialized value but don't need a variable - e.g. foo(S.init) - but as far as language buit-ins go, to do the correct thing in the general case, you need to declare a variable and use it if you want the code to be correct regardless of the type being used. To work around this, Phobos v3 has a helper trait to solve the problem.
template defaultInit(T)
if (is(typeof({T t;})))
{
enum defaultInit = (){ T retval; return retval; }();
}
So, then you'd do something like foo(defaultInit!S) instead. It doesn't actually work with non-static nested structs, because they can only be constructed in the scope where they're declared, but in that situation, you're dealing with a specific type that you control and will know that static opCall wasn't declared.
However, since defaultInit is in Phobos v3 (which is nowhere near ready),
it's obviously not something that I can tell you to go use instead (outside
of copying it to your own code).
So, if you're writing generic code (particularly if it's in a pubicly available library), you're going to want to either just declare the variable and use it (so that it's default-initialized normally), or you're going to want to create a helper trait like defaultInit and use that. You do not want to be using either S.init or S() outside of some fairly specific circumstances.
However, if you're writing non-generic code, and you know what the type in question is going to do, then both S.init and S() should be fine. In the vast majority of cases, there isn't actually a difference. It's just that there are corner cases where there is a difference, and that's potentially going to screw up generic code.
- Jonathan M Davis
|