skip to content

When a base type's initialization code calls a method that a derived type overrides, the language must decide what the object's type already is at that moment. Explain the decision, and how C++, Kotlin, Swift and Rust each resolve it and what each resolution costs.

level: seniorimportance: should knowfreq 48%

answer

  1. object under construction has no stable type
  2. C++: vptr moves with the chain; pure virtual call aborts
  3. JVM/CLR: final class from allocation -> override sees defaults
  4. Kotlin: null in a non-null val
  5. Swift two-phase; Rust: no incomplete value exists

basics

~30 s

An object under construction has no stable type yet. C++ changes it as construction proceeds, so a base constructor sees the base's own version and a pure virtual call aborts. Kotlin, Java and C# treat the object as its final class immediately, so an override runs before its own state exists -- Kotlin can even observe null in a non-null val. Swift forbids the observation at compile time; Rust cannot express it.

solid answer

~60 s

Two escape routes exist, and each language picks one. - **Freeze the type during construction -- C++.** The object's dynamic type *changes* as each constructor completes: inside the base constructor the object is a base, `typeid` says so, and a call to a pure virtual is undefined behaviour that usually aborts with "pure virtual method called". Safe from uninitialized derived state, but the same source line means different things inside and outside a constructor. - **Final type from allocation -- Kotlin, Java, C#.** The override runs before the derived class's property initializers, so it observes defaults. In Kotlin that means a `val` declared with a non-null type reads as `null` -- one of the few holes in its null safety; Scala had the same hole with trait `val`s. - **Forbid the observation -- Swift.** Two-phase initialization: all stored properties of the class must be set before `super.init`, and `self` may not be used until phase two, so the whole family of bugs is a compile error. It pays in `convenience`/`required` delegation rules. - **Make it unrepresentable -- Rust.** No constructors; a value exists only once every field has one.

code

cpp · 9 lines
cpp
struct Base {
    Base() { describe(); }              // prints "base"
    virtual void describe() { puts("base"); }
};
struct Derived : Base {
    int n = 42;
    void describe() override { printf("%d\n", n); }
};
Derived d;   // output: base   (dynamic type is Base during Base::Base)

go deeper

for a junior

Know the rule of thumb: do not call a method that a subclass can override from initialization code, because the subclass's own state may not exist yet.

for a middle

Explain the mechanism -- initialization runs base-first while the override belongs to the derived class -- and name at least two languages that resolve it differently.

for a senior

Diagnose it in real code, including the escaping-this variant, and prescribe the fixes: seal the type, pass the value as a parameter, or move the call to a creation function after construction.

for a principal

Discuss it as a language-design decision with three defensible resolutions -- freeze the type, forbid the observation, or remove the incomplete state -- and pick API rules for your codebase that hold no matter which resolution the host language chose.

## The underlying decision During construction an object is in a state the type system normally pretends does not exist: some of its parts are initialized and some are not. If the language also has dynamic dispatch, a call made from the partially built part can reach code belonging to the not-yet-built part. So the language must answer: **while a base's initialization runs, what type is this object?** Every mainstream answer is a different point on the tradeoff between honesty and safety. ## C++: the dynamic type changes as construction proceeds C++ says the object *is* whatever has finished being constructed so far. As each constructor in the chain begins, the object's vptr is set to that class's vtable; the standard specifies that inside a base constructor, virtual dispatch resolves to the base's own override, and `typeid` reports the base class. This is internally consistent -- you can never reach code that depends on members that do not yet exist -- but it means a virtual call behaves differently inside a constructor than everywhere else, which surprises readers. And when the base declares the function pure virtual with no implementation, there is nothing to call: the behaviour is undefined and the common runtime response is an immediate abort with a message about a pure virtual call. Destruction mirrors this exactly, running the sequence in reverse. ## Java, C#, Kotlin: the object is its final class immediately On the JVM and CLR an object's class is fixed at allocation, before any constructor body runs. Dispatch therefore always finds the most-derived override -- including when the call originates in a base constructor, at which point the derived class's field initializers and constructor body have not run. The override then reads fields that hold their default values. In Java that is `0`, `false` or `null`. In **Kotlin** the same mechanism produces something sharper: a property declared `val name: String` -- a type that promises non-null -- reads as `null` inside an overridden method called from the superclass constructor, which is one of the few places Kotlin's null safety can be defeated without a platform type or an explicit assertion. This is exactly why Kotlin warns on calling an `open` member from a constructor. **Scala** had the analogous hole with `val`s defined in traits, which is part of why early definitions and later trait parameters and lazy vals exist. The same family has a second, related hazard: publishing `this` from a constructor. Registering the object with a listener list, submitting it to an executor, or storing it in a static map inside the constructor hands a reference to a half-built object to someone else, and no dispatch rule protects against it. ## Swift: forbid the observation Swift makes the compiler enforce **two-phase initialization**. In phase one, a designated initializer must give every stored property introduced by its own class a value, and only then may it call `super.init`. Until phase one is complete for the whole chain, `self` may not be used at all -- no instance method calls, no property reads. Phase two, after the superclass chain returns, is where customization happens and `self` becomes available. The result is that the entire bug family described above is rejected at compile time rather than discovered at runtime. The cost is real rules to learn: designated versus convenience initializers, delegation direction, `required` initializers, and stricter constraints in the presence of subclassing. ## Rust: nothing to observe Rust has neither constructors nor implementation inheritance. A struct value comes into existence only when a literal supplies every field, and only then can any method be called on it. There is no interval during which a value exists but is incomplete, so the question does not arise. Multi-step setup goes through an explicit builder -- a separate type holding optional fields that yields the real value only when it can produce a complete one. The cost is that no automatic hook runs on construction and that the builder is boilerplate the language does not generate for you. ## Practical guidance regardless of language Do not call an overridable method from initialization code, and where possible remove the temptation: seal the type, make the method non-overridable, pass the value the method would have supplied in as a parameter, or move the work to a creation function that constructs the object fully and then calls the method. In languages where the compiler will not stop you, a linter usually will -- and the warning is worth treating as an error, because the resulting bug appears only in subclasses, sometimes only in ones written later by someone else.

  • C++ makes the base constructor call the base's own version. Why is that not simply the better design?
    It is safe but dishonest: the same expression dispatches differently depending on whether it appears inside a constructor, so a reader must know the construction context to predict behaviour, and refactoring a helper out of a constructor silently changes what it does. It also cannot help when the base declares the function pure virtual, where the result is undefined behaviour rather than a diagnostic. The JVM answer is at least uniform: dispatch always means the same thing, and the hazard is the uninitialized state rather than the dispatch rule.
  • Beyond overridable calls, what other way does a constructor let a half-built object escape, and why is it worse?
    Publishing this -- registering the object as a listener, submitting it to an executor, or putting it into a shared registry from inside the constructor. It is worse because no dispatch rule limits the damage: another thread or callback can invoke any method on an object whose fields are not yet assigned, and under a memory model it may not even see the values written later. The fix is to construct fully, then register from a creation function outside the constructor.
  • How would you keep this bug out of a codebase where the language permits it?
    Prevent the override from existing: make the class or the method final or non-open unless subclassing is a designed feature. Where subclassing is intended, pass the varying value in as a constructor parameter instead of calling out to get it, so the base never depends on derived state. Finally, turn the compiler or linter warning about calling an overridable member from an initializer into a build error, since the failure surfaces only in subclasses that may not exist yet.

saying these in an interview costs you the question

  • Answering with one language's rule as if it were universal -- C++ and the JVM give opposite results for the same code.
  • Saying the JVM "calls the base version" from a base constructor; dispatch always reaches the most-derived override.
  • Believing final or val fields are safe from this -- they hold defaults until their initializer runs.
  • Treating it as a rare edge case rather than a designed-in hazard of combining dynamic dispatch with staged initialization.
  • Assuming Swift merely warns; its two-phase rules are compile-time errors.

context