What is the purpose of a constructor, and why should an object be valid after construction?
Interview preparation resource from Gate Smashers.
A constructor establishes an object’s initial valid state. It initializes required fields, validates inputs, and enforces the class invariants—the conditions that must hold for the object to behave correctly. After successful construction, the object should be ready to use so callers and instance methods do not need to handle an uninitialized or internally inconsistent state. If that state cannot be established, construction should report failure using the language-appropriate mechanism, such as an exception or a factory method that returns an error result.
#include <cstdio>
#include <stdexcept>
#include <string>
class FileWrapper {
public:
explicit FileWrapper(const std::string& path) {
file_ = std::fopen(path.c_str(), "r");
if (!file_) {
throw std::runtime_error("cannot open file");
}
// Invariant: file_ != nullptr.
}
~FileWrapper() {
if (file_) {
std::fclose(file_);
}
}
FileWrapper(const FileWrapper&) = delete;
FileWrapper& operator=(const FileWrapper&) = delete;
FILE* get() const { return file_; }
private:
FILE* file_ = nullptr;
};
// Construction either succeeds with a valid FileWrapper or throws.public class Person {
private final String name;
private final int age;
public Person(String name, int age) {
if (name == null || name.isEmpty()) {
throw new IllegalArgumentException("name required");
}
if (age < 0) {
throw new IllegalArgumentException("age must be non-negative");
}
this.name = name;
this.age = age;
}
}class Vector:
def __init__(self, coords):
if not coords:
raise ValueError("coords must not be empty")
self.coords = tuple(coords)
# Vector([1, 2, 3]) creates a valid object.
# Vector([]) raises ValueError.Purpose of a constructor
A constructor runs when an object is created. Its main responsibility is to establish the minimum state required by the class contract: initialize essential data, validate constructor arguments, and set up required dependencies or resources.
This is where class invariants should be established. For example, if a class requires a non-empty name or a non-negative age, the constructor can reject invalid input rather than allowing an object that violates those rules.
- Initialize state: Assign required fields and dependencies.
- Validate input: Reject values that would violate the class contract.
- Establish invariants: Ensure the conditions required for correct object behavior hold after successful construction.
Why the object must be valid after construction
A successfully constructed object should be immediately usable according to its public contract. This lets callers use it without first checking whether an initialization step was missed, and it lets instance methods rely on the object’s invariants.
Allowing a publicly accessible object to remain partially initialized increases complexity and creates more opportunities for incorrect use. Keeping invalid states from escaping construction improves encapsulation and makes the class easier to reason about and test.
- Predictable behavior: Clients can assume that successful construction produces a usable object.
- Simpler methods: Methods can rely on established invariants instead of repeatedly checking initialization state.
- Stronger encapsulation: The class controls and protects its own validity rules.
Failure during construction
If a constructor cannot establish a valid state, it should not silently leave a usable reference to an invalid object. In languages with exceptions, a constructor can throw or raise an exception. Where explicit error handling is preferred, a factory method can perform the work and return either a valid object or an error result.
Resources acquired during construction require careful cleanup if a later step fails. The exact mechanism is language-dependent: C++ commonly relies on RAII and automatic destruction of fully constructed members, while other languages use structured cleanup or resource-management constructs.
Two-phase initialization—creating an object and requiring a later init call—should generally be avoided when it exposes an object before it is ready. It may be necessary in some APIs, but the not-ready state must then be made explicit and controlled.
- Exception-based failure: Throw or raise when the required valid state cannot be created.
- Factory-based failure: Use a factory that returns an object only on success and otherwise returns an explicit error result.
- Resource safety: Ensure resources acquired before failure are released by the language-appropriate cleanup mechanism.
