Hacker News new | ask | show | jobs
by _ZeD_ 719 days ago
dude, java constructor are easy... that C++ stuff is really black magic

and from what I understand rust constructors are basically the same as java, no?

4 comments

Inside a constructor you can access a partially initialised "this" value, and even call methods on it, which leads to rules like: "Do not call overridable methods in constructors"[0], as they can lead to surprising, non-local, bugs.

Rust has functions associated with types which are conventionally used like constructors, but critically the new objects must have all their fields provided all at once, so it is impossible to observe a partially initialised object.

[0] https://learn.microsoft.com/en-us/dotnet/fundamentals/code-a...

Virgil solved this a little differently. The initialization expressions for fields (outside of constructors) as well as implicit assignment of constructor parameters to fields happens before super constructor calls. Such initialization expressions cannot reference "this"--"this" is only available in _constructor bodies_. Initializing fields before calling super and then the chaining of super calls guarantees the whole chain of super constructor calls will finish before entering the body of a constructor, and all fields will be initialized. Thus by construction, virtual methods invoked on "this" won't see uninitialized fields.

https://github.com/titzer/virgil/blob/master/doc/tutorial/Cl...

You can most likely use session types to soundly observe a partially initialized MaybeUninit<MyObject> in Rust. The proper use of session types could ensure that the object is only assumed to be initialized after every field of it has been written to, and that no uninitialized fields are ever accessed in an unsound way. The issue though is that this is not automated in any way, it requires you to write custom code for each case of partial initialization you might be dealing with.
Rust does not have constructors at all[0], it uses factory functions (conventionally named `new_somethignsomething`) but those are not special to the language.

[0] except in the more generalised haskell-ish sense that structs or enum variants can be constructed and some forms (“tuple structs” and “tuple variants”) will expose an actual function

I've often longed for first class constructors in Go and Rust. It was more of a problem for me with Go because you can omit a struct field when building a value, something you can't do in Rust unless it has an explicit Default impl and even then you have to explicitly add ..Default::defualt() when you're building the value.

I never thought that constructors were that burdensome and therefore do not understand the omission in other languages like Go and Rust that followed. Quite the opposite really -- knowing that a type always went through a predefined init was comforting to me when writing Java.

I think people don’t like constructors because of the potential side effects of something happening in constructors, especially if the constructor is big or doesn’t finish properly.
Rust doesn't have constructors. By convention, a static method called new returns a struct - no magic.
I think if you think constructors in Java are easy, you are much, much smarter than I am or have missed some really, really subtle footguns.

Eg:

- Java constructors can return the object before they complete construction, finishing at a later time; this is visible in concurrent code as partially constructed objects

- Java constructors can throw exceptions and return the partially constructed object at the same time, giving you references to broken invalid objects

- Just.. all the things about how calling super constructors and instance methods interleaved with field initialization works and the bazillion ordering rules around that

- Finalizers in general and finalizers on partially constructed objects specifically

I don't in any way claim it's on the same level as C++, but any time I see a Java constructor doing any method calls anymore - whether to instance methods or to super constructors - I know there are dragons

> bazillion ordering rules

There are 3 which pertain to object initialization in Java.

1. super is initialized in it's entirety by an implicit or explicit call to `super()`

2. All instance initializers of the present class are invoked in textual order.

3. Constructor code following the `super()` call is executed.

The only awkward thing here is the position of #2 in between #1 and #3, whereas the text of a constructor body suggests that #1 and #3 are consecutive. It gets easier to remember when you recognize that, actually, there's a defect in the design of the Java syntax here. A constructor looks like a normal function whose first action must be a `super()` call. It's not. The `super()` call is it's own thing and shouldn't rightly live in the body of the constructor at all.

Edit: Tweaks for clarity.

Those are the normal issues inherent to constructors as a concept (except for the finalizer one).

Any language that has constructors has some complex rules to solve those things. And it's always good to check what they are when learning the language. Java has one of the simplest set of those rules that I know about.

> - Java constructors can return the object before they complete construction, finishing at a later time; this is visible in concurrent code as partially constructed objects > > - Java constructors can throw exceptions and return the partially constructed object at the same time, giving you references to broken invalid objects

Java constructors do not actually return the object. In Java code, it would appear to the caller as though the contructor returns the new instance, but that is not really the case. Instead, the new object is allocated and then the constructor is called on the object in (almost) the same manner as an instance method.

Additionally, Java constructors can only leak a partially initialized object if they store a `this` reference somewhere on the heap (for example, by spawning a thread with a reference to `this`). The assertion that this gives you a reference to a "broken invalid object" is only potentially correct from the perspective of invariants assumed by user-written code. It is perfectly valid and well-defined to the JVM.

> - Just.. all the things about how calling super constructors and instance methods interleaved with field initialization works and the bazillion ordering rules around that

This is a gross mischaracterization of the complexity. There is only a single rule that really matters, and that is "no references to `this` before a super constructor is called". Until very recently, there was also "no statements before a super constructor is called".

> - Finalizers in general and finalizers on partially constructed objects specifically

Finalizers are deprecated.

I think you’re exaggerating the complexity here. There are corner cases yes, but the compiler will warn you about them.

    > Java constructors can throw exceptions and return the partially constructed object at the same time
Can you show some sample code to demonstrate this issue?