logo

Constructor vs Init() vs Factory

Last Updated: 2022-02-05

TL;DR

There are a few ways to create a new object in a object-oriented programming languages.

  • Constuctor: public constructors return fully constructed objects.
  • Init: public trivial constructors return partially constructed objects + separate public Init() method to finish initialization.
  • Factory method: private constructors + public static factory (e.g. Create()) methods that return fully constructed objects.

https://abseil.io/tips/42 suggests "Prefer Factory Functions to Initializer Methods".

Constructors

Cons:

  • they cannot fail except by crashing the entire program or throwing an exception. Not suitable for classes with complex initialization logic that may fail.
  • they cannot safely call virtual methods of the object being constructed: if it calls virtual functions, these calls will not get dispatched to the subclass implementations.
  • cannot take the address of a constructor, so whatever work is done in the constructor cannot easily be handed off to, for example, another thread.

public Init() method

Keep the constructors simple, and place complex or possibly-failing initialization in an Init() method

Pros:

  • derived classes can perform possibly-failing initialization of their base classes.

Cons:

  • the object is in a half-initialized state before Init() has been called, which can lead to confusion about which methods can be called when.
  • it can be difficult to reverse the decision to use this design, because it imposes fewer restrictions on client code.

Conclusion:

Prefer factory functions unless there is some good reason to avoid heap allocation, or if the interface naturally lends itself to two-phase initialization.

Guidelines for using Init() methods:

  • If Init() is only needed so derived classes can initialize their base class, make it protected rather than public, and express the public API in terms of factory functions.
  • If possible, have the constructor leave the object in a well-defined 'empty' or 'zero' state, where all methods work correctly even before initialization.
  • If a 'zero' state is not possible, forbid all methods (or as many as possible) from being called before Init(), enforce that rule, and provide a way for clients to determine if the object is initialized.
  • Pass any initialization data as parameters to Init(), not as constructor parameters; avoid situations where an object may store pending-but-uninitialized data, as this is fertile ground for bugs. This has the useful side effect that Init() may be called multiple times to reset the object to different values.
  • By the same token, avoid "setters", i.e. methods that set or modify the state of the object prior to initialization; in addition to the problem of pending-but-uninitialized data, these create the risk that a fully-initialized object may be invalidated by a setter called after initialization. If an API involving setter calls is more natural than passing the data all at once, consider using the Options class pattern or Builder pattern, which let you put these setter methods in a separate class.
  • Failed initialization should leave the object unmodified; don't have separate states for "uninitialized" and "initialization failed". -->

Factory method

How:

  • static Create() function that returns the object (can return null on failure)
  • private constructor so it cannot be invoked directly Not copyable.
  • Never return partially-initialized objects, or objects in an "error" state; factory functions should return a non-null result only on success.
  • Make all constructors private or protected, so that all initialization goes through the factory functions.
  • Try to do the initialization work prior to calling the constructor; it's easier to reason about objects that are fully-initialized as soon as their constructor returns.

Pros:

  • Factory functions can return null to signal failure.
  • Factory functions can choose from different implementations, and may return different concrete implementation classes depending on the parameters
  • Factory functions can have meaningful names, unlike constructors.

Cons:

  • they can only construct objects on the heap, not suitable for lightweight, frequently-allocated, or value-like classes.
  • cannot be invoked from a derived class constructor
// foo.h
class Foo {
 public:
  // Factory method: creates and returns a Foo.
  // May return null on failure.
  static std::unique_ptr<Foo> Create();

  // Foo is not copyable.
  Foo(const Foo&) = delete;
  Foo& operator=(const Foo&) = delete;

 private:
  Foo(); // Clients can't invoke the constructor directly.
};

Foo foo = Foo::Create();

// foo.cc
std::unique_ptr<Foo> Foo::Create() {
  // Foo's constructor is private, we have to use new. https://abseil.io/tips/134
  return absl::WrapUnique(new Foo());
}