Skip to content

Add yet another proposal on initialization #20

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

tangent-vector
Copy link
Contributor

No description provided.

@CLAassistant
Copy link

CLAassistant commented May 28, 2025

CLA assistant check
All committers have signed the CLA.

Copy link

@tdavidovicNV tdavidovicNV left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reactions are written in the order in which I read the document, so some nitty gritty I thought of before I got to the nitty gritty section.. feel totally free to close things as duplicate or answer multiple in a single place or whatever makes sense.

In general, I like it. I really like the named arguments. I worry about the arrays a lot. I did initially miss that this effectively completely bans incremental construction, and I am quite sure that will lead to things just being zero initialized all the time, and then overwritten.

Which I do not see as a big problem (it would be my take on this, tbh), but it very much leads to the C++ way you don't like too much. But it might be a good compromise, because it mostly won't be too expensive to work The C++ Way, and if you decide to go a more optimal route (less writes after writes), the compiler will check it.

I'd rate this 8/10 on the scale of "if you implement it, I will try it", with couple caveats outlined in the comments, mostly the dynamically sized array of expensive types, and structs containing interfaces.

b.tangentV = v;
b.normal = u - v; // ERROR: can't invoke `Basis.normal.set` on value `b` that is not fully initialized

The `set` accessor for `normal` takes its `this` parameter as an `inout Basis`, and an `inout` parameter requires a fully-initialized value as input (and is required to yield a fully-initialized value as output).

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I almost completely forgot about this, that was also a nasty surprise in the update (and I think it might be one of the reasons why inout is no longer checked)


2. A call `__init(a, b, c, ...);` to another constructor of the same type.

3. Zero or mor statements that are allowed to access `this` as a fully-initialized value

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo: mor -> more


The body of a designated constructor must conform to the following sequence of operations:

1. Zero or more statements that may assign to fields of the current type through `this`, but may not otherwise access `this` (notably: cannot access derived fields).

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you give an example of a derived field?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's important for me to note that officially the use of implementation inheritance for structs is an experimental feature, and not something we support user code doing (until we hammer out all the numerous issues). I only mention issues related to inheritance in this proposal to make it clear how we should address them if/when that support becomes non-experimental.

To write up a quick (and contrived) example:

struct BaseVertex
{
    float3 position = default;
    float3 normal;

    // gets the auto-synthesized constructor
    // analogous to:
    //
    // __init(float3 position = ..., float3 normal);
}

struct DerivedVertex : BaseVertex
{
    float3 tangentU;
    float3 tangentV;

    __init(float3 position = default, float3 tangentU, float3 tangentV)
    {
        // step (1): must initialize all fields directly declared in this type
        this.tangentU = tangentU;
        this.tangentV = tangentV;
        //
        // It would be illegal at this point to do:
        //     this.normal = cross(tangentU, tangentV);
        // because it isn't this constructor's job/right
        // to initialize the members of `BaseVertex`

        // step (2): invoke a constructor of the base type:
        //
        // We can compute the values that we'd like to pass in
        // as arguments to the base-type constructor, and thereby
        // control how its fields get initialized...
        //
        let n = cross(tangentU, tangentV);
        super(position, n );

        // step (3): do whatever you want
        //
        // Once step (2) is complete, `this` is a fully initialized value.
        // This is true even once we support things like `class` types
        // with `virtual` methods. All the fields of our base type(s)
        // will have been initialized by the `super()` call in step (2), and all
        // the fields of *derived* types must have been set in step (1)
        // of their constructor, before they called down into this constructor
        // in their step (2).
        //
        // Thus it is okay to call arbitrary methods on `this`, even in the
        // case of `virtual` methods, and you won't have the problem that
        // you face in C++, where the vtbl of an object changes during
        // construction and calls may be dispatched to base-type methods
        // (even if those are non-existant).
    }
}

For the purposes of what this proposal is talking about, tangentU and tangentV are the direct fields of DerivedVertex, while position and normal are inherited fields or "derived fields" like I said in the doc (I probably should have said "inherited fields" because that is more clear...).

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aaah, field derived from the parent, got it.

I find the fact that the derived type must be initialized before the parent type extremely counterintuitive... in just about everything I do, parent has to be valid before child is.


1. Zero or more statements that may assign to fields of the current type through `this`, but may not otherwise access `this` (notably: cannot access derived fields).
At the end of these statements, all fields of the current type that do not have a default-value expression must be fully initialized.
(The rest will then be initialized to their default values)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Many people will expect that default values are there at the start already, not written only at the end.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand you on that, and it initially seems like we could obvious stuff the default values in at the start, based on what we do/don't see people set manually:

struct Foo
{
    int a = expensiveFunction(0);
    int b = expensiveFunction(1);
    int c = expensiveFunction(2);

    __init(int x, int y)
    {
        this.b = x;
        this.c = y;
    }
}

In that case a user might feel like it is "obvious" that the compiler should initialize this.a to expensiveFunction(0) at the start of their constructor, before the rest of the computation runs.

For a C++ programmer, it might seem like intuitively the compiler would initialize all three of a, b, and c at the start of that constructor... which is why I made them be calls to a hypothetical expensive function in my example, to show why that doesn't feel like a good solution.

C++ works around this problem by inventing the separate list of field/base initializers that run before the constructor body. If you think about it for a bit, it might be clear that C++ has the same basic steps (1), (2), (3), with (3) being the actual body statements of the constructor, and (1) + (2) being handled via the initializers. So what we are proposing to do here is to instead write out parts (1) and (2) as ordinary code, rather than have an entirely separate syntax with all kinds of corner cases and special rules.

(We also swap around the order of execution of steps (1) and (2). C++ does (2) first, and then (1), which turns out to be kind of obviously wrong, given how it means that your code in step (3) is still very restricted in what it can safely do with this (for fear of calling through to a pure virtual function, etc.))

Anyway...

If you accept that we don't want to initialize all three of a, b, c to their defaults before the user code starts running, you are left with only the idea that the compiler should infer that it must initialize a, and do so before the user code runs...

...however, it's entirely possible that the user code might conditionally initialize this.a:

    __init(int x, int y)
    {
        this.b = f(x);
        this.c = g(y);
        int z = h(x,y);
        if(z > 0) this.a = z;
    }

In this case, we can't determine statically if the user-authored constructor body will initialize this.a or not, until after we've actually run the user-authored code for step (1). I've written the code to use function calls here to make it clear that we can't just hoist the evaluation of h(x,y) to above the rest of the code, to determine whether or not this.a gets initialized in the user code or not; doing so could alter the semantics of the code if f, g, and h have observable side effects. (Also imagine that expensiveFunction() has side effects too... or at least it could)

Thus we have to contend with the fact that either:

  • We would have to accept that this.a might only be initialized sometimes, and we can only determine at run-time whether the compiler needs to initialize it after the user's code has already run. Thus for consistency it only makes sense to evaluate all such field initial-value expressions after the user code for part (1) has run.

  • Alternatively, we could make it an error for the code in part (1) to conditionally initialize a field of this, and thus insist that the code in part (1) must either initialize a given field along all control-flow paths, or none of them. Then we could move the initialization of the fields that statically don't get initialized during (1) to the top, and everything maybe works like the user expects. This option sounds reasonable on paper, but it would quickly fall into the trap where users expect the compiler to see/know that "obviously" their code that is under an if(0) (or the Slang equivalent of an if constexpr (0)...) shouldn't be considered when deciding what the code in part (1) does or does not initialize... and then everything gets muddy again.

  • A final alternative is to declare that the order in which default field initial values are computed is unspecified, and it is also unspecified whether they run before or after the user code in part (1). That option is pretty nice, and actually opens up some interesting paths (e.g., you could maybe imagine letting the initial value for one field depend on the value of another, and let the compiler sort out the order of evaluation...), but it doesn't avoid the reality that no matter how "unspecified" the order is, there will be user code that accidentally ends up relying on whatever order happens to fall out of the compiler implementation, and those users will be understandably upset by any/all compiler changes that silently alter the meaning of their code. In practice, if we leave this "unspecified" in theory, we would still want to codify what the compiler actually does, and commit to not changing that behavior without careful communication with users.

With all of those options weighed, I think the simplest in terms of complexity for both users and the compiler is the first option: any fields that the user doesn't initialize in part (1) get initialized to their default values by the compiler in between parts (1) and (2).


The empty initializer list `{}` will be semantically equivalent to a `default` expression.

While initializer lists will be *allowed*, the Slang tools, documentation, etc. should default to showing examples that use only constructor-based initialization or `default` expressions (which should be explained as a shorthand for a zero-argument constructor call).

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this explains my previous question, but just to get it clear clear:

Foo f = {};
Foo f = default;
Foo f = Foo();
var f = Foo();

are all equivalent and valid in all the same contexts?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those would all be equivalent if we implement this proposal as written.

I believe it is currently not the case in Slang that those are all equivalent (ignoring that we don't support default at all). The handling of initializer lists and constructors is different code, and while an initializer list might be translated into a constructor call, it is not true that there is an easy and obvious equivalence.

Even if it turns out all of those are equivalent today for an empty argument list, it is definitely not the case that all of those options are equivalent for anything where there are one or more arguments (e.g., {a, b, c} vs. Foo(a,b,c)).

I would consider one of the main goals of this proposal to be fixing that problem, so that initializer lists are either eliminated (unlikely) or are always equivalent to a constructor call with the same arguments.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The follow-up here is that I personally think there may be cases where it might make sense to have Foo f = default; be allowed even when Foo f = Foo(); wouldn't be, but I am explicitly not including any of that complexity in the proposal, because my goal at this point was to try and write a sketch for The Simplest Thing That Could Possibly Work, rather than prematurely add in all the bells and whistles we might want in practice.

for(int i = 0; i < 10; i++)
a.add( someFunc(i) );

The implementation of such a type in the core module would be largely straightforward, and the core module could take care of whatever delicate interactions it needs to have with the compiler to reassure it that values are consistently initialized before they are accessed.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The question of converting from the BoundedSizeDynamicArray to a static array remains, but I guess you can just assign it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, we could either make a function in the core module that handles the conversion for you, or a user could just write it for themself if we give them some of the higher-order-function ways of creating an ordinary array:

BoundedSizeDynamicArray<T, N> dynArray = default;
// ... fill in `dynArray`

let array = Array<T, N>( (int i) => dynArray[i] );


int a[10];
for(int i = 0; i < 10; i++)
a[i] = someFunc(i); // ERROR: `a` is being accessed while uninitialized

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We unfortunately have many arrays everywhere. This includes arrays in structs that are in a StructuredBuffer. I assume that anything in a StructuredBuffer is assumed to be initialized?

Same for createDynamicObject and reintepret and as, I assume that if the inputs have been initialized, the outputs are also considered initialized?

We also have many places with out float output[64] that is initialized by a neural network, and the natural thing people will do when faced with inability to initialize step by step will be:

output = default;
// run the actual initializer without imposing restrictions.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we are all in agreement that any changes to initialization behavior will live or die by how they work (or don't work) for arrays.

In the case of a parameter like out float output[64], the most obvious option is what you said:

output = default;
// fill in output however you like

In cases where the parameter is instead out Foo output[64], where Foo is a type you either can't initialize with default, or would rather not, you would instead be able to do:

BoundedSizeDynamicArray<64, Foo> dynOutput = default;
// fill in `dynOutput` however you like
output = convertDynamicallySizedArrayToStaticallySized( dynOutput ); // TODO: naming

If you are in a case where you cannot fill in the elements in sequential order (so that dynOutput is not usable) you would instead fall back on something like:

MaybeUninitialized<Foo> localOutput[64] = default;
// fill in `localOutput` however you like
output = localOutput; // this line implicitly asserts that `localOutput` has been fully initialized

Again, the MaybeUninitialized<Foo> case is more or less semantically equivalent to the = uninitialized sort of proposal, but it puts the limbo status of the value into its type, rather than assuming all of its work can be squared away at (non-)initialization time.

Aside: it's possible that we could allow MaybeUninitialized<Array<Foo,64>> instead of having it wrap the elements, if we treat MaybeUninitialized as a kind of reference-like type so that accessing a field or element of type U on a type T via a MaybeUninitialized<T> would yield a MaybeUninitialized<U>... but there's a lot of subtle errors that I could see users making with that, too.


The only real alternative we have considered so far is to give up, and resign ourselves from slowly sliding into the realm that C++ occupies where almost all types support being implicitly default-constructed, but often to a state that is literally or figuratively equivalent to a null pointer.

"When every variable is 'intialized', none of them are."

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not see a problem with variables being initialized to uninitilized value. It is pretty much the same as you are proposing after all. The objects must be in some valid state by the time you use them in any way imaginable. And whether that is implicit, or explicit is mostly a matter of taste.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suspect that all of us in the conversation are already aware of it, but I want to make sure that anybody reading this is aware that the person most directly responsible for the pervasive acceptance of null pointers refers to it a "billion-dollar mistake".

To be clear, Slang has never been as zealous about these kinds of issues as something like Rust, and it is likely we never will be quite as good about memory safety as Rust. That is by intention/design, rather than just because of a lack of awareness on our part as language designers and compiler writers.

The reality is that the kind of people who are writing code in Slang are the same kind of people who want to be able to write code that might dereference null without their compiler forcing them to fill out forms in triplicate noting that they are being A Very Naughty Programmer by doing so. So... I get it.

For me personally, as a language designer, the thing I care most about is the Path of Least Resistance.

If the kind of Slang code that is easiest to write happens to be correct, safe, and fast, then we are winning on all fronts. If there are cases where Slang makes it easier to write incorrect (or unsafe, or slow) code rather than some well-known correct/safe/fast alternative, we should look carefully at why that is.

The approach to initialization that Slang has been trending toward, and that C/C++ tend to use is known to be less correct/safe than the alternatives, and projects like Rust have shown that those alternatives can have comparable performance (although I'll concede that there are always outlier cases).

The whole point of the proposal I've written is that programmers who are happy enough with the C/C++ state of affairs can opt into it pretty easily (write = default everywhere you can, make all your types conform to IDefaultable, and wrap anything the compiler cares about in Optional<T> or MaybeUninitialized<T>), but simply not doing those things takes less effort, and leads to a programming model where the compiler is assisting the programmer by helping them see where they might have forgotten to initialize a variable, or neglected to think about the value that they wanted to put in some struct field.


First, it is important to note that in cases where the element type of the array can be default-constructed, an array can be initialized with `default`:

Thing t[10] = default; // works if `Thing()` is valid

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know these things are a slippery slope, but would something like unsafe or uninitialized (overloading previous proposal) work here? Some "I know what I am doing, this is intentional, please stop checking" keyword. The nature of our work requires us to sometimes do really dirty hacks where we just know better than the compiler. We know that a combination of circumstances will never happen at the same time, so things are safe even if analysis can't find it. And lets face it, we will need it at some point. So might try to embrace it from the start and make it part of the initial proposal? Maybe an attributes [unchecked_initialization] that is verbose enough and takes a line to be annoying to just blast everywhere, but if you're in a pinch and just need the thing to work for a deadline, you don't have to consult language designers (or turn off checking wholesale)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would be exactly what MaybeUnintialized<T> would do, so I think that would satisfy the requirements.
Simply writing:

MaybeUninitialized<Thing> t[10] = default; // always works, even if `Thing()` wouldn't

seems to resolve the basic problem. The key difference, again, between MaybeUninitialized<...> and either a magic = uninitialized expression or an [unchecked_initialization] attribute is that it makes the situation manifest in the type system, so that the rest of the compiler's machinery can trivially propagate the information through to where the compiler actually needs to exploit it.

...

So maybe we're on the same page, and it's just that the MaybeUninitialized<T> thing needs to be in the initial proposal.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Encoding uninitialized-ness in the type system worries me a bit. How would the below situation behave?

// Expects the array to be initialized
void f(inout Thing arr[4])
{
    ...
}

void main()
{
    MaybeUninitialized<Thing> arr[4] = default;
    for (int i = 0; i < 4; ++i)
        arr[i] = Thing(i);
    f(arr);
}

Should f take MaybeUninitialized<Thing> instead? If so, can it then be called with a regular Thing[4] as well? I guess the BoundedSizeDynamicArray may help here, but it seems like that will likely "waste" a register (unless there's a good optimizer) which I'm not very keen on since high register pressure is already a fairly common performance issue for me.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code should compile just fine. arr can coerce to Thing[4] without the compiler complaining anything.

@ccummingsNV
Copy link

My notes (from email):

Thanks for jumping straight on this. I think this is heading in the right direction.

I think the key to making this work smoothly is ensuring there’s a way to ‘opt out’ of automatic initialization when needed, ideally on a per-variable basis.

For example, something that achieves the same goal as this (albeit not necessarily written the same way):

Wave w;
w.amplitude = 100.0;
w.phase = 0.0;

Is still essential in my view. Unless we can absolutely guarantee that the compiler will always eliminate any redundant initialization, we need to retain the ability to bypass it. We definitely don’t want to end up in a situation where HLSL can produce faster code than Slang simply because we’ve tied the user’s hands on this.

With this:

int a[10];
for(int i = 0; i < 10; i++)
    a[i] = someFunc(i); // ERROR: `a` is being accessed while uninitialized

I’d really prefer not to have to rewrite that using unfamiliar or verbose syntax — and I suspect most users would feel the same. One of Slang’s big strengths is being easy to pick up and use; that kind of change risks undermining that.

A potential concern (though might be my lack of understanding). If we prevent function calls inside constructors, it becomes difficult to share initialization logic across multiple constructors. It would be unfortunate if we had to choose between duplicating code or initializing everything by default and then overwriting it — both approaches come with drawbacks in terms of clarity and performance.

All that said, I’m broadly on board with the idea of pushing toward more explicit and disciplined initialization. I’m also fine with making users be explicit when they want to work with uninitialized data — that feels like a reasonable trade-off for clarity and safety.

But I do think we need to be careful not to introduce complexity or performance limitations in the process. If we can't strike a balance that avoids those pitfalls, then — bluntly — I’d prefer the unsafe flexibility of C++/HLSL, because it guarantees that I can write the code I want and make it fast, if I'm willing to put in the effort.

Hopefully we though we can have our cake and eat it too 😊

@tangent-vector
Copy link
Contributor Author

...
For example, something that achieves the same goal as this (albeit not necessarily written the same way):

Wave w;
w.amplitude = 100.0;
w.phase = 0.0;

Is still essential in my view. Unless we can absolutely guarantee that the compiler will always eliminate any redundant initialization, we need to retain the ability to bypass it. We definitely don’t want to end up in a situation where HLSL can produce faster code than Slang simply because we’ve tied the user’s hands on this.

I believe that for 99% of use cases that want to do this sort of thing, they programmer would just set up their types to have a trivial = default and then write:

Wave w = default;
w.amplitude = 100.0;
w.phase = 0.0;

That doesn't give you any kind of 100% rock-solid guarantee that the compiler will eliminate the redundancy, but in any case where the compiler can determine that the defaulted value for, e.g., w.phase will be overwritten, it will make the logic that computed that value dead, and it would be eliminated unless there is a reason that the compiler cannot determine it is side-effect-free (which would seemingly only occur if we are doing full separate compilation and the function that computes the initial value is in another module and wasn't marked as being side-effect-free).

As I've said up-thread, the intention with this proposal is to start with The Simplest Thing That Could Possibly Work, so that all of us in the conversation can see what things might look like with all the special cases eliminated.
One of the most obvious special cases we might decide to add back in is the ability to write code just like your original:

Wave w;
w.amplitude = 100.0;
w.phase = 0.0;

in cases where Wave is determined to be a sufficiently "C-like" type. The semantics in that case would seemingly have to be that w would be fully uninitialized at the point of declaration, and would only become initialized one all of its fields have been explicitly written to.

Note that as soon as Wave has a user-defined constructor or any of the fields of Wave defines a default-value expression, then the Wave type doesn't really qualify as "C-like" and we are right back in the world where it doesn't make sense to do w.amplitude = ... until after somebody has run a constructor to initialize w. And once you need to construct w, the difference between using = default to do it vs. having the compiler auto-magically call a constructor behind your back (for some variables but not others...) is pretty much just a syntactic one, and your concern about whether the compiler can eliminate the double-initialization applies equally to this proposal and to C++ style pervasive automagical default constructors to "initialize" things.

I hope its clear that enough that in the case of a user-defined constructor on Wave the compiler really has no alternative but to insist that a call to at least one constructor happens before anybody uses w.

In the case where Wave doesn't have any user-defined constructors but does set default initial values for some of its fields, it may be harder to see why there's a problem... but trying to actually work out the details makes you quickly realize that its a tar pit.

The only reasonable way out is to either give every field in Wave an = default and then do Wave w = default and trust the compiler to eliminate the redundancy (since all your default values were simple and had no side effects), or to have a constructor for Wave that leaves it in a partially-initialized state.
The easiest way to express the latter condition is to wrap the specific fields of Wave that you want to support being in an uninitialized state with MaybeUninitialized<T>.

Aside: if we can push through more stringent checking for initialization in the compiler front-end, then it would be a lot easier to change the logic in the back-end that currently writes zero values to initialize variables that it cannot prove are initialized (to avoid having compilers like dxc yell at us...) and have it instead write NaNs or 0xDEADBEEF or similar intentionally-conspicuous values.

@tangent-vector
Copy link
Contributor Author

With this:

int a[10];
for(int i = 0; i < 10; i++)
    a[i] = someFunc(i); // ERROR: `a` is being accessed while uninitialized

I’d really prefer not to have to rewrite that using unfamiliar or verbose syntax — and I suspect most users would feel the same. One of Slang’s big strengths is being easy to pick up and use; that kind of change risks undermining that.

Strong agree. Despite what I've written in the proposal, in my gut I think there's no way out of this particular issue.

I suspect that even in the 2026 language version, Slang will need to support a certain amount of field-by-field or element-by-element initialization logic, and the question is really around what we can support without breaking things too badly.

@tangent-vector
Copy link
Contributor Author

A potential concern (though might be my lack of understanding). If we prevent function calls inside constructors, it becomes difficult to share initialization logic across multiple constructors. It would be unfortunate if we had to choose between duplicating code or initializing everything by default and then overwriting it — both approaches come with drawbacks in terms of clarity and performance.

To make sure we are on the same page:

  • During part (1) of the proposed flow for constructors, you cannot make calls to member functions of this, but calls to other ordinary functions are allowed. This is comparable to the way that many C++ compilers will issue a strong warning if they see you doing member function calls on this during your base/member initialization list (because the order of initialization might not match what you think...).

  • During part (3) of the proposed flow, you can make arbitrary member function calls on this, and the semantics are well defined and follow the Principle of Least Surprise, unlike the situation in C++ where calls to member functions anywhere during the body of a constructor can have unpredictable results. With this proposal Slang simply restricts member function calls to the situations where they can actually be given well-defined and unsurprising semantics.

To reiterate something I said up-thread: the steps (1), (2), and (3) that I describe for Slang constructors mirror how C++ does things, just with different syntax. C++ does step (2) via the base initializers, then it does (1) via member initializers, and then it does (3) by running the body of the user-defined constructor.

The main things that differ from C++ in this proposal are:

  • You actually get a guarantee that your entire object/value is in a usable state for (3), so things like member function calls in the constructor will actually mean something for Slang classes, compared to where they are a known dangerous thing with C++ constuctors.

  • In order to enable those guarantees, we swap the order of (2) and (1), which typically won't matter except in types that were too clever for their own good anyway. No matter what, a derived type gets the opportunity to run code both before and after the constructor the base type, which is strictly more powerful than being restricted to only one or the other.

  • We don't allow arbitrary access to the contents of this (including member function calls) during (1) which, again, is something that C++ compilers strongly discourage even when the language allows it, and also isn't something the compiler can ever be confident isn't an error. If you really need to call arbitrary code, wait until (3), and just stick whatever default values you need to into the fields of your type first.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants