Thread overview
Getting the default value of a class member field
Dec 01, 2022
WebFreak001
Dec 01, 2022
kinke
Dec 02, 2022
WebFreak001
Dec 02, 2022
kinke
Dec 02, 2022
kinke
Dec 02, 2022
kinke
Dec 02, 2022
WebFreak001
December 01, 2022

I've got this class definition:

class X
{
    this()
    {
        assert(false);
    }

    int x = 3;
}

due to internal reasons the constructor would fail at compile time, so I put in an assert(false) here, and I can't add or change any methods in X.

How do I get 3 if I have X and field name "x" at compile time?

For structs X.init.x / __traits(getMember, X.init, "x") would work, however for classes it complains about null dereference.

I saw there is __traits(initSymbol), however that one doesn't work at compile time.

December 01, 2022

On Thursday, 1 December 2022 at 08:09:05 UTC, WebFreak001 wrote:

>

I've got this class definition:

class X
{
    this()
    {
        assert(false);
    }

    int x = 3;
}

due to internal reasons the constructor would fail at compile time, so I put in an assert(false) here, and I can't add or change any methods in X.

How do I get 3 if I have X and field name "x" at compile time?

For structs X.init.x / __traits(getMember, X.init, "x") would work, however for classes it complains about null dereference.

I saw there is __traits(initSymbol), however that one doesn't work at compile time.

AFAIK, there is no way. Unlike a struct's init symbol, a class' one doesn't necessarily represent a valid instance state - it's just the raw payload before invoking a ctor (which constructs a valid instance), and the state 'dead' memory is reset to after finalizing an object instance (to prevent dead pointers keeping other GC refs alive).

If the ctor worked at CTFE, one could use:

int get() {
    scope x = new X;
    return x.x;
}
enum bla = get();

to get the x value of a valid instance, which might be different than the static initializer (if modified in the ctor).

I guess the main question is why do you require the static initializers of these fields at compile-time. __traits(initSymbol) was added to aid in manual blitting at runtime.

December 02, 2022

On Thursday, 1 December 2022 at 23:02:31 UTC, kinke wrote:

>

On Thursday, 1 December 2022 at 08:09:05 UTC, WebFreak001 wrote:
[...]

AFAIK, there is no way. Unlike a struct's init symbol, a class' one doesn't necessarily represent a valid instance state - it's just the raw payload before invoking a ctor (which constructs a valid instance), and the state 'dead' memory is reset to after finalizing an object instance (to prevent dead pointers keeping other GC refs alive).

If the ctor worked at CTFE, one could use:

int get() {
    scope x = new X;
    return x.x;
}
enum bla = get();

to get the x value of a valid instance, which might be different than the static initializer (if modified in the ctor).

I guess the main question is why do you require the static initializers of these fields at compile-time. __traits(initSymbol) was added to aid in manual blitting at runtime.

I want to use the static initializers (when used with an UDA) as default values inside my SQL database.

See https://github.com/rorm-orm/dorm/blob/a86c7856e71bbc18cd50a7a6f701c325a4746518/source/dorm/declarative/conversion.d#L959

With my current design it's not really possible to move it out of compile time to runtime because the type description I create there gets serialized and output for use in another program (the migrator). Right now it's simply taking the compile time struct I generate and just dumping it without modification into a JSON serializer.

I might be abusing classes a little bit here, but they provide the easiest way to do a variety of things:

  • is(T : Model) and especially type specialization like void foo(T : Model)(T x) is much easier to write and use
  • it basically allows me to inject methods into my type (using template parameter this This I can even get my real type)
  • it's the most easy and pretty to type for the user. A struct with mixin Model would be quite verbose imo and doesn't allow Model to define custom fields easily, because they would break the implicitly generated constructor
December 02, 2022

On Friday, 2 December 2022 at 00:24:44 UTC, WebFreak001 wrote:

>

I want to use the static initializers (when used with an UDA) as default values inside my SQL database.

See https://github.com/rorm-orm/dorm/blob/a86c7856e71bbc18cd50a7a6f701c325a4746518/source/dorm/declarative/conversion.d#L959

With my current design it's not really possible to move it out of compile time to runtime because the type description I create there gets serialized and output for use in another program (the migrator). Right now it's simply taking the compile time struct I generate and just dumping it without modification into a JSON serializer.

[...]

Okay, so what's blocking CTFE construction of these models? AFAICT, you have a templated base constructor in Model, which runs an optional @constructValue!(() => Clock.currTime + 4.hours) lambda UDA for all fields of the derived type. Can't you replace all of that with a default ctor in the derived type?

class MyModel : Model {
    int x = 123;        // statically initialized
    SysTime validUntil; // dynamically initialized in ctor

    this() {
        validUntil = Clock.currTime + 4.hours;
    }
}

Such an instance should be CTFE-constructible, and the valid instance would feature the expected value for the validUntil field. If you need to know about such dynamically generated fields (as e.g. here in this time-critical example), an option would be a @dynamicallyInitialized UDA. Then if you additionally need to be able to re-run these current @constructValue lambdas for an already constructed instance, you could probably go with creating a fresh new instance and copying over the fresh new field values.

December 02, 2022

On Friday, 2 December 2022 at 04:14:37 UTC, kinke wrote:

>

[...]
Such an instance should be CTFE-constructible, and the valid instance would feature the expected value for the validUntil field. [...]

Oh well, that time-sensitive example clearly isn't CTFE-able. :D - If that's the primary use case for these @constructValue UDAs, then yeah, this makes things more complicated.

December 02, 2022

You could potentially skip running the @constructValue lambdas in the ctor via if (__ctfe), if skipping this extra init is acceptable for CTFE instances.

December 02, 2022

On Friday, 2 December 2022 at 04:14:37 UTC, kinke wrote:

>

On Friday, 2 December 2022 at 00:24:44 UTC, WebFreak001 wrote:

>

I want to use the static initializers (when used with an UDA) as default values inside my SQL database.

See https://github.com/rorm-orm/dorm/blob/a86c7856e71bbc18cd50a7a6f701c325a4746518/source/dorm/declarative/conversion.d#L959

With my current design it's not really possible to move it out of compile time to runtime because the type description I create there gets serialized and output for use in another program (the migrator). Right now it's simply taking the compile time struct I generate and just dumping it without modification into a JSON serializer.

[...]

Okay, so what's blocking CTFE construction of these models? AFAICT, you have a templated base constructor in Model, which runs an optional @constructValue!(() => Clock.currTime + 4.hours) lambda UDA for all fields of the derived type. Can't you replace all of that with a default ctor in the derived type?

class MyModel : Model {
    int x = 123;        // statically initialized
    SysTime validUntil; // dynamically initialized in ctor

    this() {
        validUntil = Clock.currTime + 4.hours;
    }
}

Such an instance should be CTFE-constructible, and the valid instance would feature the expected value for the validUntil field. If you need to know about such dynamically generated fields (as e.g. here in this time-critical example), an option would be a @dynamicallyInitialized UDA. Then if you additionally need to be able to re-run these current @constructValue lambdas for an already constructed instance, you could probably go with creating a fresh new instance and copying over the fresh new field values.

constructValue is entirely different than this default value. It's not being put into the database, it's just for the library to send it when it's missing. (so other apps accessing the database can't use the same info) - It's also still an open question if it even gives any value because it isn't part of the DB.

To support constructValues I iterate over all DB fields and run their constructors. I implemented listing the fields with a ListFields!T template. However now when I want to generate the DB field information I also use this same template to list all columns to generate attributes, such as what default value to put into SQL. Problem here is that that tries to call the constructor, which wants to iterate over the fields, while the fields are still being iterated. (or something similar to this)

Basically in the end the compiler complained about forward reference / the size of the fields not being known when I put in a field of a template type that would try to use the same ListFields template on the class I put that value in.

Right now I hack around this by adding an int cacheHack template parameter to ListFields, which simply does nothing. However this fixes that the compiler thinks the template isn't usable and everything seems to work with this.

Anyway this is all completely different from the default value thing, because I already found workarounds and changed some internals a bit to support things like cyclic data structures.

I would still like a way to access the initializer from class fields, and it would be especially cool would be to know if they are explicitly set. Right now I have this weird and heavy @defaultValue(...) annotation that's basically the same as = ...;, that I just needed to add to make it possible to use T.init as default value in the DB as well, but not force it. My code uses @defaultFromInit to make it use the initializer, but it would be great if I didn't need this at all. (although because of my cyclic template issues it might break again and be unusable for me)