Jump to page: 1 24  
Page
Thread overview
Structs are Not Plain: A call for empty struct constructors
Sep 19, 2019
FeepingCreature
Sep 19, 2019
Dennis
Sep 19, 2019
Nicholas Wilson
Sep 19, 2019
FeepingCreature
Sep 19, 2019
Max Samukha
Sep 19, 2019
Kagamin
Sep 19, 2019
FeepingCreature
Sep 19, 2019
nkm1
Sep 19, 2019
Ethan
Sep 19, 2019
Jonathan M Davis
Sep 20, 2019
Max Samukha
Sep 20, 2019
Jonathan M Davis
Sep 20, 2019
FeepingCreature
Sep 20, 2019
Kagamin
Sep 21, 2019
Max Samukha
Sep 20, 2019
Max Samukha
Sep 20, 2019
Jonathan M Davis
Sep 20, 2019
FeepingCreature
Sep 20, 2019
Max Samukha
Sep 20, 2019
Andrea Fontana
Sep 21, 2019
Max Samukha
Sep 20, 2019
Dukc
Sep 20, 2019
Dukc
Sep 20, 2019
FeepingCreature
Sep 20, 2019
Dukc
Sep 20, 2019
Kagamin
Sep 20, 2019
FeepingCreature
Sep 23, 2019
Kagamin
Sep 20, 2019
Eugene Wissner
Sep 23, 2019
Olivier FAURE
September 19, 2019
Let me lay out the problem first.

We're doing unittesting with fixtures. A Fixture is a struct that contains default values and mocks of the class structure that is being tested. Unittests will have the form

```
unittest {
  with (Fixture()) {
  }
}
```

so that within the unittest the fields of the fixture can be easily accessed.

Now, from this follow a few requirements. First, Fixture *has* to be a struct. Why "has"? Because one thing the mocker does is check if its expectations were fulfilled, and it does that at scope exit with `~this()`. So the mocker has to be a struct - so Fixture has to be a struct, because only structs have a halfway cleanly defined end of lifecycle.

(`scope(exit) validate;` does *not* work - that's the sort of line you just know will get forgotten. We want validation to be automatic.)

So how to construct a Fixture? It can't be the `init` state, because there's classes in it. It can't be a constructor, because D structs cannot have empty constructors (because "why would you want that if they're just plain old data"). So we have to use `static Fixture opCall()`, aka "ssh, it's a constructor but we'll pretend it isn't." But that means we can't use a regular autogenerated constructor, because if we give the struct *any* constructor it disables `static opCall`.

(Why? I can only conclude that we have sinned by giving the struct a fake empty constructor, and the language has decided to punish us.)

What else follows from this? We can't use immutable - because immutable fields can only be set in a proper constructor. We could do horrible workarounds like `Fixture(null)`, but that's just making a bad situation worse.

So that's all terrible.

I think the deeper problem stems right from the original assumption that structs are "plain old data" and thus shouldn't be interested in construction. With their use in reference counting, with(), destructors, assignments... does anyone still really believe that? Structs have taken on a dual role of "plain old data, and also anything that needs customized behavior bound to lifecycle."

The decision to not allow `this()` stems from a time where that wasn't really on the table, and I think it should be overturned. Let `Struct s;` be Struct.init, by all means, but `Struct()` should call `this()`, if there is one.

September 19, 2019
On Thursday, 19 September 2019 at 09:02:39 UTC, FeepingCreature wrote:
> We could do horrible workarounds like `Fixture(null)`, but that's just making a bad situation worse.

The usual workaround is to have a function `fixture` that returns your default constructed `Fixture` struct. I don't know why default constructors are not allowed on structs though.
September 19, 2019
On Thursday, 19 September 2019 at 09:02:39 UTC, FeepingCreature wrote:
> Let me lay out the problem first.
>
> We're doing unittesting with fixtures. A Fixture is a struct that contains default values and mocks of the class structure that is being tested. Unittests will have the form
>
> ```
> unittest {
>   with (Fixture()) {
>   }
> }
> ```

A workaround, wrap it with alias this in another struct:

```
import std.stdio;
struct Foo
{
    int a;
    this(int) { a = -1; }
    ~this() { writeln("~this(), a = ",a); }
}

struct Bar
{
    Foo f = Foo(1);
    alias f this;
}
void main()
{
    with(Bar())
    {
        writeln(a);
        a = 42;
    }
}
```
September 19, 2019
On Thursday, 19 September 2019 at 09:30:00 UTC, Nicholas Wilson wrote:
> A workaround, wrap it with alias this in another struct:
>
> ```
> import std.stdio;
> struct Foo
> {
>     int a;
>     this(int) { a = -1; }
>     ~this() { writeln("~this(), a = ",a); }
> }
>
> struct Bar
> {
>     Foo f = Foo(1);
>     alias f this;
> }
> void main()
> {
>     with(Bar())
>     {
>         writeln(a);
>         a = 42;
>     }
> }
> ```

Okay first of all that's brilliant, but please note that any solution has to pass our internal review and I would never even try to put this code in a pull request. :) The point is to end up with a *cleaner* solution.
September 19, 2019
On Thursday, 19 September 2019 at 09:02:39 UTC, FeepingCreature wrote:
> (`scope(exit) validate;` does *not* work - that's the sort of line you just know will get forgotten. We want validation to be automatic.)

The test code is usually tested for alternative scenario to verify that it actually tests what it's supposed to test. Forgotten validation won't pass that.
September 19, 2019
On Thursday, 19 September 2019 at 09:30:00 UTC, Nicholas Wilson wrote:
> A workaround, wrap it with alias this in another struct:
>
> ```
> import std.stdio;
> struct Foo
> {
>     int a;
>     this(int) { a = -1; }
>     ~this() { writeln("~this(), a = ",a); }
> }
>
> struct Bar
> {
>     Foo f = Foo(1);
>     alias f this;
> }
> void main()
> {
>     with(Bar())
>     {
>         writeln(a);
>         a = 42;
>     }
> }
> ```

Won't work - Foo(1) is evaluated at compile time.
September 19, 2019
On Thursday, 19 September 2019 at 10:06:12 UTC, Kagamin wrote:
> On Thursday, 19 September 2019 at 09:02:39 UTC, FeepingCreature wrote:
>> (`scope(exit) validate;` does *not* work - that's the sort of line you just know will get forgotten. We want validation to be automatic.)
>
> The test code is usually tested for alternative scenario to verify that it actually tests what it's supposed to test. Forgotten validation won't pass that.

`validate` is not actually part of the test in this case, it's part of dmocks and nothing bad happens if you forget to call it.
September 19, 2019
On Thursday, 19 September 2019 at 09:02:39 UTC, FeepingCreature wrote:
> What else follows from this? We can't use immutable - because immutable fields can only be set in a proper constructor. We could do horrible workarounds like `Fixture(null)`, but that's just making a bad situation worse.

I have no idea why default constructors are disallowed, but your factory doesn't need to be opCall(). E.g., this seems to me a reasonable solution:

struct Fixture
{
    int a, b;
    static Fixture Default() { return Fixture(1, 2); }
}

void main()
{
    import std.stdio;

    with (Fixture.Default) {
        writeln(a);
        writeln(b);
    }
}

(I actually like it more than default ctors, which is, of course, not a reason to disallow default ctors).
September 19, 2019
On Thursday, 19 September 2019 at 09:02:39 UTC, FeepingCreature wrote:
> I think the deeper problem stems right from the original assumption that structs are "plain old data" and thus shouldn't be interested in construction.

I've made this point many times over the years with regards to C++ binding. One of my DConf talks explicitly points out that making a Mutex/CriticalSection object cannot be initialised with a postblit. I think I even pointed out the static opCall hack as well in that talk... but I disagree with it because the meaning is different between C++ and D (C++ -> zero initialisation; D -> call a function that isn't a constructor of any kind).

Whenever someone says "RAII doesn't work in D" it's because struct default constructors are disallowed.
September 19, 2019
On Thursday, September 19, 2019 3:02:39 AM MDT FeepingCreature via Digitalmars-d wrote:
> I think the deeper problem stems right from the original assumption that structs are "plain old data" and thus shouldn't be interested in construction. With their use in reference counting, with(), destructors, assignments... does anyone still really believe that? Structs have taken on a dual role of "plain old data, and also anything that needs customized behavior bound to lifecycle."
>
> The decision to not allow `this()` stems from a time where that
> wasn't really on the table, and I think it should be overturned.
> Let `Struct s;` be Struct.init, by all means, but `Struct()`
> should call `this()`, if there is one.

This has nothing to do with structs being POD. It has to do with how D was designed with the idea that every type has an init value which is known at compile time, and various operations rely on being able to simply blit that init value. Having default constructors would mean that none of that code could simply blit the init value but would instead have to construct the structs using the default constructor. It also could no longer rely on having a default value which works at compile time. If you want to see how much in D relies on the init value working, then @disable default initialization on a struct and see how much it doesn't work with. Having @disable this(); was hacked into the language later, and there's plenty of stuff that doesn't work when it's used (e.g. dynamic arrays won't work at all with types that can't be default-initialized, because they rely on being able to blit the init value).

Classes avoid all of this, because they have an extra layer of indirection, and T.init is the init value for T's reference, meaning that it can just be null, whereas because structs can live directly on the stack, default initialization requires initializing the struct itself.

Default construction of structs was a casualty of D's goal of avoiding objects being initalized as garbage like you get in C/C++. It certainly can be annoying at times, but in most cases, you can just use a factory function instead, and if you want to avoid such a struct ever accidentally being used via its init value instead of the factory function, then disable default initialization for that type. And for a use case like you're talking about here where you're using RAII without needing to actually put the object inside of a container (or anything else that would require the init value), it should work just fine. You'll just get something like

with(Fixture.make())
{
}

or

with(Fixture.cons())
{
}

or whatever instead of

with(Fixture())
{
}

Adding default construction to the language would be a pretty big change, and I expect that you would need a DIP going over all of the pros and cons, taking into account exactly how the language relies on init in order to get it changed. Given how baked into the language init is, my guess is that such a DIP doesn't stand much of a chance, but I don't know.

- Jonathan M Davis



« First   ‹ Prev
1 2 3 4