Object-Oriented Programming with C++

Chapter 8
Robustness, Exceptions, and Defensive Programming

Bachelor Program in Mechatronics Engineering
Object-Oriented Programming with C++   •   Bachelor

Robustness, Exceptions, and Defensive Programming

Introduction

Engineering software operates at the boundary between abstract algorithms and physical reality. A robot may receive an impossible sensor value, a motor controller may become unavailable, a configuration file may be corrupted, or a memory allocation may fail. Object-oriented programming provides mechanisms for organizing such systems, but good organization alone does not make software reliable. Reliability requires deliberate strategies for detecting faults, communicating failures, preserving valid state, and containing damage.

This chapter examines robustness in C++ through exceptions, error codes, assertions, contracts, validation, exception-safety guarantees, and fault containment. The focus is not merely on language syntax. The central engineering question is: What should a program do when an operation cannot fulfill its intended responsibility?

A robust program distinguishes among expected invalid input, environmental failure, programmer error, and unrecoverable system faults. It reports failures at an appropriate abstraction level, avoids silently corrupting state, and provides sufficient information for diagnosis and safe recovery.

Learning Objectives

After studying this chapter, you should be able to:

  • distinguish faults, errors, failures, and invalid input;
  • select appropriately among exceptions, error codes, assertions, and validation;
  • design exception hierarchies and meaningful error messages;
  • explain the basic, strong, and no-throw exception-safety guarantees;
  • write functions that preserve object invariants when failures occur;
  • apply defensive programming and fault-containment techniques;
  • evaluate the reliability of an engineering software design;
  • identify limitations and inappropriate uses of defensive mechanisms.

1. Robustness as an Engineering Property

Robustness is the ability of software to continue behaving correctly—or to fail in a controlled and diagnosable manner—when confronted with invalid inputs, unusual conditions, resource shortages, or component failures. Robustness is broader than the absence of crashes. A program that produces a plausible but unsafe motor command is less robust than one that stops and reports a fault.

Several terms are useful:

  • Fault: The underlying defect or abnormal condition, such as a disconnected sensor.
  • Error: An incorrect internal state resulting from a fault.
  • Failure: Observable behavior that does not satisfy a required service or specification.
  • Recovery: A deliberate response that restores safe operation or moves the system to a controlled degraded state.
  • Invariant: A condition that must remain true for an object or subsystem to be valid.
  • Precondition: A condition required before an operation begins.
  • Postcondition: A condition expected after successful completion.

A useful reliability principle is to detect a problem as close as possible to its origin, but handle it at the level that has enough information to make a sensible decision. A low-level file function can detect that opening failed, but a higher-level controller may decide whether to retry, use a default configuration, or enter a safe mode.

Robustness does not mean accepting every input. In safety-related software, rejecting an invalid command is often the correct behavior. Defensive programming therefore combines detection, communication, containment, and recovery.

2. Classifying Failure Conditions

Before choosing a mechanism, classify the condition.

ConditionTypical exampleSuitable response
Validated user or external input is invalidNegative sampling periodValidation and reported error
Recoverable environmental failureTemporary file or network failureException, error result, retry, or fallback
Programmer violationIndex outside a fixed design rangeAssertion during development; contract failure in production policy
Resource exhaustionAllocation failureException or explicit failure handling
Hardware or subsystem faultEncoder disconnectedFault state, containment, safe shutdown
Unrecoverable process conditionCorrupted global stateTermination or supervisory restart

The same event can require different responses depending on context. A missing optional log file may be recoverable, while a missing safety configuration may make startup impossible. Robustness is therefore a design decision, not simply a choice of syntax.

3. Validation, Assertions, and Contracts

3.1 Input validation

Validation checks data that comes from outside a trusted component. Sources include users, files, networks, sensors, command-line arguments, and other software modules. Validation should be explicit and should occur before data is used in an operation that assumes validity.

#include <stdexcept>
#include <string>

class MotorCommand {
public:
    MotorCommand(double velocity, double acceleration)
        : velocity_(velocity), acceleration_(acceleration) {
        if (velocity < -10.0 || velocity > 10.0) {
            throw std::invalid_argument("velocity outside permitted range");
        }
        if (acceleration < 0.0 || acceleration > 50.0) {
            throw std::invalid_argument("acceleration outside permitted range");
        }
    }

    double velocity() const noexcept { return velocity_; }
    double acceleration() const noexcept { return acceleration_; }

private:
    double velocity_;
    double acceleration_;
};

Validation should address range, format, units, relationships, and plausibility. For example, a temperature reading may be numerically representable but physically implausible. Validation should also clarify units. A function accepting a raw double cannot distinguish metres per second from revolutions per minute. Stronger designs use named types or documented unit conventions.

Validation is not a substitute for access control, authentication, or authorization. In networked systems, data must be validated even when it supposedly comes from a trusted source.

3.2 Assertions

An assertion states a condition that the programmer believes must always hold at a particular point. In C++, the standard assert macro is supplied by <cassert>.

#include <cassert>

void process_buffer(const int* buffer, std::size_t count) {
    assert(buffer != nullptr || count == 0);
    // Processing follows only after the internal assumption is checked.
}

Assertions are appropriate for internal logic errors and violated invariants, not for ordinary external input. They can be disabled by defining NDEBUG, so an assertion must not be the only mechanism protecting safety-critical behavior or validating untrusted data. Assertions should not contain side effects:

// Bad: behavior changes when assertions are disabled.
assert(read_sensor() >= 0);

// Better:
const auto reading = read_sensor();
assert(reading >= 0);

3.3 Contracts

A contract describes preconditions, postconditions, and invariants. C++ projects may implement contracts with assertions, validation functions, static analysis, or third-party libraries. The conceptual value of contracts is independent of a particular language feature.

For a function that commands a motor:

  • precondition: the motor identifier is valid and the controller is connected;
  • postcondition: either the command has been accepted, or the function reports failure without claiming success;
  • invariant: an enabled motor must never receive a command outside its configured limits.

Contracts improve documentation and testing. However, a contract does not automatically make a system safe. The design must specify what happens when a contract is violated and whether the condition is recoverable.

4. Error Codes and Result Objects

An error code is an explicit value returned by a function. It is often appropriate when failure is expected, frequent, or part of normal control flow, especially in low-level interfaces and real-time loops where predictable control over execution is important.

enum class ReadStatus {
    ok,
    timeout,
    disconnected,
    invalid_frame
};

ReadStatus read_encoder(int& position);

void update_position() {
    int position = 0;
    const ReadStatus status = read_encoder(position);

    switch (status) {
    case ReadStatus::ok:
        // Use position.
        break;
    case ReadStatus::timeout:
        // Record a transient fault or retry.
        break;
    case ReadStatus::disconnected:
        // Enter a safe hardware state.
        break;
    case ReadStatus::invalid_frame:
        // Reject the received data.
        break;
    }
}

Strongly typed enumerations prevent accidental conversion to unrelated integers. A result object can carry either a value or a structured error, making ignored failures less likely than with a bare Boolean. In modern C++, a project may use a standard or project-specific result type, such as std::expected where supported by the toolchain.

Error codes have advantages: their control flow is visible, they can avoid exception overhead in selected environments, and they are suitable for APIs crossing language or binary boundaries. Their weaknesses include repetitive propagation code and the risk that callers ignore the result. A Boolean is usually too weak because it does not explain the failure.

5. Exceptions in C++

An exception transfers control from the point where an error is detected to a matching handler. It separates normal operation from recovery logic and automatically unwinds local objects, invoking destructors as stack frames are removed.

#include <fstream>
#include <stdexcept>
#include <string>

std::string load_configuration(const std::string& path) {
    std::ifstream file(path);
    if (!file) {
        throw std::runtime_error("cannot open configuration: " + path);
    }

    std::string text((std::istreambuf_iterator<char>(file)),
                     std::istreambuf_iterator<char>());
    return text;
}

void start_controller() {
    try {
        const auto configuration = load_configuration("controller.cfg");
        // Parse and apply configuration.
    } catch (const std::exception& error) {
        // Log diagnostic context and enter a defined safe state.
    }
}

Catch exceptions by reference, normally as const std::exception&, to avoid copying and slicing. Catch more specific types before general types. Do not catch an exception merely to print a message and continue if the object or subsystem is no longer valid.

5.1 Exception types and translation

Standard exception classes include std::invalid_argument, std::out_of_range, std::runtime_error, and std::logic_error. A project can define domain-specific types:

#include <stdexcept>

class HardwareError : public std::runtime_error {
public:
    using std::runtime_error::runtime_error;
};

class SensorTimeout : public HardwareError {
public:
    using HardwareError::HardwareError;
};

An exception should communicate what failed and provide relevant context. Low-level exceptions may be translated at subsystem boundaries:

try {
    raw_device.write(command);
} catch (const std::exception& error) {
    throw HardwareError(std::string("motor write failed: ") + error.what());
}

When rethrowing the current exception, use throw;, not throw error;, because the latter can alter the exception type and lose information. Preserve the original cause where the project’s error framework supports nested exceptions.

5.2 Destructors and noexcept

Destructors should normally not allow exceptions to escape. During stack unwinding, a second escaping exception can cause program termination. Resource-management classes should release resources in destructors and report operational failures through explicit operations or prior checks. C++ destructors are implicitly noexcept in many circumstances; declaring this intention explicitly can make design expectations clear.

A function marked noexcept promises not to let exceptions escape. If an exception does escape, the program calls std::terminate. Use noexcept when the guarantee is true and valuable, such as for simple move operations or cleanup functions. It is not a mechanism for suppressing unexpected failures.

6. Exception-Safety Guarantees

Exception safety describes what remains true if an operation fails. Three guarantees are widely used.

  1. No-throw guarantee: The operation never throws.
  2. Strong guarantee: If the operation throws, observable program state is unchanged; the operation has transactional behavior.
  3. Basic guarantee: If the operation throws, invariants remain valid and no resources leak, although values may have changed.

A fourth informal category is the no-fail or nothrow operation, often used for cleanup and operations essential to preserving invariants.

Consider a configuration update. A strong-guarantee design parses and validates a new configuration into a temporary object, then commits it only after success:

class Controller {
public:
    void update_config(const std::string& text) {
        Config candidate = Config::parse_and_validate(text);
        config_ = std::move(candidate); // Commit after successful preparation.
    }

private:
    Config config_;
};

If parsing fails, the existing configuration remains unchanged. This is the copy-and-swap or prepare-then-commit pattern. It is especially valuable when partial updates could produce unsafe combinations of settings.

The basic guarantee is often acceptable for containers and stateful services when the object remains valid but its exact previous value cannot be preserved. The no-throw guarantee is essential for destructors, cleanup, and some real-time or fail-safe operations.

6.1 RAII and resource safety

Resource Acquisition Is Initialization (RAII) binds a resource’s lifetime to an object. The destructor releases the resource automatically, including during exception unwinding. Standard examples include std::vector, std::string, std::unique_ptr, std::lock_guard, and file-stream objects.

#include <memory>

void use_device() {
    auto device = std::make_unique<Device>();
    device->open();
    device->operate();
    // Device is released automatically on normal return or exception.
}

RAII reduces cleanup paths and prevents leaks. It does not guarantee that an external device has reached a safe state; the destructor must still implement appropriate shutdown semantics.

7. Defensive Programming Practices

Defensive programming anticipates misuse and abnormal conditions without making the code opaque or unnecessarily restrictive. Effective practices include:

  • make invalid states difficult to represent;
  • validate at trust boundaries;
  • use const, scoped enumerations, and narrow interfaces;
  • initialize members and avoid uninitialized data;
  • prefer RAII over manual resource management;
  • keep ownership explicit with smart pointers;
  • check array and container access where failure is possible;
  • avoid exposing mutable internal state;
  • log events with timestamps, identifiers, and severity;
  • make units and ranges explicit;
  • use static analysis, sanitizers, tests, and code review;
  • define behavior for timeout, retry, and shutdown paths.

Defensive code must remain understandable. Excessive checks that duplicate trusted invariants can obscure the actual design and create inconsistent policies. Validation should be centralized where practical, and error handling should preserve useful context.

8. Fault Containment and Recovery

Fault containment limits the effects of a failure. A mechatronic controller may separate sensing, planning, actuation, logging, and communication into components. If logging fails, actuation should not silently receive corrupted commands. If a sensor becomes invalid, the system should mark its data stale and apply a defined fallback rather than reusing it indefinitely.

Containment mechanisms include:

  • process or thread boundaries;
  • watchdog timers;
  • timeouts and bounded retries;
  • circuit breakers for repeatedly failing services;
  • safe-state transitions;
  • redundant sensors or plausibility checks;
  • message validation and sequence numbers;
  • resource quotas;
  • supervisory restart of failed components.

Retries require care. A retry is appropriate for transient faults, not for invalid commands or deterministic configuration errors. Retrying a non-idempotent actuator command can duplicate physical action. A command protocol should therefore define whether an operation is idempotent and how acknowledgements are correlated.

Logging is part of containment and diagnosis, but logs may expose sensitive information or consume excessive storage. Security-sensitive systems should avoid logging credentials, raw personal data, or unvalidated strings without appropriate controls.

9. Applied Case: Robotic Conveyor Controller

Background

Consider an illustrative conveyor controller that receives a target speed from a supervisory computer and reads an encoder. The controller must stop the conveyor if the command is outside configured limits, if encoder data times out repeatedly, or if the motor driver rejects a command.

Problem

A first implementation uses a Boolean return value from the encoder and directly assigns incoming speed values to the motor command. In testing, a malformed network message produces a very large speed, while a temporary encoder timeout is interpreted as a zero speed. The result is ambiguous: zero may mean a commanded stop or a missing measurement.

Analysis

The design has three weaknesses. First, external input is not validated. Second, the Boolean result does not distinguish timeout, disconnection, and invalid data. Third, the controller has no explicit fault state, so it cannot distinguish normal operation from degraded operation.

Approach and solution

The revised design applies several layers:

  1. Decode the network message and validate its format, range, units, and sequence number.
  2. Represent encoder outcomes with a scoped enumeration or structured result.
  3. Maintain an explicit controller state such as Running, Degraded, or SafeStopped.
  4. Use a bounded retry for transient communication failures.
  5. Transition to SafeStopped after the configured fault policy is exceeded.
  6. Ensure that a configuration update uses prepare-then-commit, preserving the previous valid configuration if parsing fails.
  7. Log the fault category, timestamp, device identifier, and transition without logging confidential data.

A simplified state transition policy might be:

EventState transitionAction
Valid command and valid encoderRunning → RunningApply bounded command
One transient timeoutRunning → DegradedHold or use defined fallback
Repeated timeout threshold reachedDegraded → SafeStoppedDisable drive and notify supervisor
Invalid commandAny active state → SafeStoppedReject command and stop safely
Recovery after diagnostic checksSafeStopped → DegradedRequire controlled restart

Outcome

The revised system no longer confuses missing data with a legitimate zero. It rejects invalid commands before actuation, preserves valid configuration during failed updates, and gives operators a meaningful fault classification. The design also makes testing more systematic: each transition and failure category can be exercised independently.

Learning points

The principal lesson is that robustness comes from coordinated policy. No single exception, assertion, or error code solves the problem. Input validation detects bad data; typed results communicate hardware conditions; state machines contain faults; and safe-state logic determines the physical response.

10. Testing and Verification

Robust error handling must be tested deliberately. Normal-path tests are insufficient. Include tests for malformed input, boundary values, missing files, allocation failure where practical, timeouts, duplicate messages, partial configuration, and exceptions from dependencies.

Useful techniques include:

  • unit tests for validators and state transitions;
  • property-based tests for ranges and invariants;
  • fault injection for I/O and hardware failures;
  • sanitizers for memory and undefined behavior defects;
  • static analysis for ownership and unreachable paths;
  • integration tests with simulated devices;
  • watchdog and recovery tests;
  • code coverage focused on error paths, not only line count.

A test should verify both the reported error and the resulting state. For example, after a failed configuration update, verify that the previous configuration is still active. For a failed motor command, verify that the controller does not report success or continue using an unsafe command.

11. Limitations and Professional Judgment

Exceptions can obscure control flow when used indiscriminately, and some real-time systems restrict dynamic allocation or exception handling in time-critical paths. Error codes can be ignored, and assertions may disappear in production builds. Validation can reject unusual but valid conditions if its assumptions are too narrow. Fault containment can increase complexity, memory use, and testing effort.

Therefore, teams should define a project-wide failure policy. It should specify exception ownership, permitted exception types, logging rules, real-time restrictions, retry behavior, safe states, and shutdown responsibilities. Coding standards should be supported by automated tools and architectural review rather than treated as substitutes for engineering judgment.

In safety- or security-related applications, applicable standards and regulatory requirements must be considered. Examples may include functional-safety processes, secure coding standards, traceability, hazard analysis, and independent verification. The exact standard depends on the industry and jurisdiction; developers should not claim compliance merely because they use exceptions or assertions.

Chapter Summary

Robust C++ software detects abnormal conditions, communicates them clearly, preserves object invariants, and contains failures. Input validation protects trust boundaries, while assertions and contracts express internal assumptions. Error codes and result objects provide explicit control flow and are often suitable for expected or real-time events. Exceptions separate normal flow from recovery logic and work effectively with RAII, but they require disciplined types, ownership, and catch policies.

The no-throw, strong, and basic exception-safety guarantees describe progressively weaker obligations for state preservation. Prepare-then-commit designs can provide strong guarantees for configuration and transactional updates. Defensive programming is most effective when combined with explicit state machines, timeouts, bounded retries, watchdogs, safe states, and fault-aware testing. In engineering systems, the correct response to failure is not always recovery; controlled refusal or shutdown may be the safest behavior.

Key Terms

Fault: An underlying defect or abnormal condition.

Error: An incorrect internal state caused by a fault.

Failure: Observable behavior that violates a required service or specification.

Robustness: The ability to behave correctly or fail controllably under abnormal conditions.

Validation: Checking external or untrusted data before use.

Assertion: A check expressing a programmer assumption or invariant.

Contract: A specification of preconditions, postconditions, and invariants.

Exception: An object and control-flow mechanism representing an abnormal condition.

Error code: An explicit return value describing success or failure.

RAII: A C++ resource-management technique binding resource lifetime to object lifetime.

Invariant: A condition that must remain true for an object or subsystem.

Precondition: A requirement that must hold before an operation begins.

Postcondition: A condition expected after successful completion.

No-throw guarantee: A promise that an operation will not allow an exception to escape.

Strong guarantee: A failure leaves observable state unchanged.

Basic guarantee: A failure preserves invariants and resource safety, but values may change.

Fault containment: Limiting the effects of a failure to a defined boundary.

Safe state: A controlled condition that minimizes hazard after a fault.

Idempotence: The property that repeating an operation has the same intended effect as performing it once.

Review Questions

Short Answer Questions

  1. Distinguish a fault, an error, and a failure.
  2. Why should assertions not be used as the only validation mechanism for untrusted input?
  3. What is the difference between the basic and strong exception-safety guarantees?
  4. Why should exceptions generally be caught by const reference?
  5. Give two examples of fault-containment mechanisms in a mechatronic system.

Long Answer Questions

  1. Compare exceptions, error codes, assertions, and validation. Explain when each is appropriate in an engineering application.
  2. Explain how RAII supports exception safety and illustrate the explanation with a C++ resource-management example.
  3. Describe a prepare-then-commit design for updating a controller configuration and identify the guarantee it can provide.
  4. Discuss how retry policies can improve robustness and how inappropriate retries can create physical or logical hazards.
  5. Develop a failure-handling policy for a sensor-driven actuator subsystem, including detection, communication, containment, recovery, and logging.

Analytical / Application Questions

  1. A controller receives a speed command over a network. The command is numerically valid but uses unknown units, and the encoder sometimes times out. Design a validation and fault-handling strategy that prevents unsafe actuation.
  2. A function modifies five related configuration fields and throws while modifying the fourth field. Analyze which object states are possible and redesign the function to provide the strong guarantee.
  3. A motor command operation is retried automatically after every communication failure. Analyze the risks if the command is not idempotent, and propose a safer acknowledgement and retry protocol.

Multiple Choice Questions with Answers

Question 1

Which condition is most appropriately handled by input validation?

A. A malformed command received from a network

B. An impossible internal invariant caused by a programming defect

C. A destructor releasing a memory resource

D. A deliberate process termination

Correct Answer: A. A malformed command received from a network

Question 2

What is the principal limitation of a standard C++ assertion?

A. It cannot test Boolean conditions

B. It may be disabled in production builds

C. It always throws an exception

D. It can only be used with floating-point values

Correct Answer: B. It may be disabled in production builds

Question 3

Which exception-safety guarantee states that observable state is unchanged if an operation fails?

A. No-throw guarantee

B. Basic guarantee

C. Strong guarantee

D. Partial guarantee

Correct Answer: C. Strong guarantee

Question 4

Why is RAII useful during exception handling?

A. It prevents all exceptions from being thrown

B. It releases owned resources during scope exit and unwinding

C. It disables destructors during failures

D. It converts exceptions into error codes automatically

Correct Answer: B. It releases owned resources during scope exit and unwinding

Question 5

How should a general exception normally be caught when preserving polymorphic type information?

A. By value as std::exception

B. By non-const integer reference

C. By const std::exception&

D. By converting it to a Boolean

Correct Answer: C. By const std::exception&

Question 6

Which response is most appropriate for a repeated encoder timeout in a safety-oriented controller?

A. Treat the missing reading as a valid zero without recording it

B. Continue indefinitely using the last value

C. Enter a defined degraded or safe state according to policy

D. Ignore the timeout because hardware faults are external

Correct Answer: C. Enter a defined degraded or safe state according to policy

Question 7

What does the basic exception-safety guarantee require?

A. The operation must never throw

B. The exact original state must be restored

C. Invariants remain valid and resources are not leaked

D. The process must terminate on failure

Correct Answer: C. Invariants remain valid and resources are not leaked

Question 8

Which design most directly supports the strong guarantee for configuration updates?

A. Modify the live configuration field by field before validation

B. Parse and validate a temporary candidate, then commit it

C. Ignore parsing errors and use default values silently

D. Disable all exception handling during parsing

Correct Answer: B. Parse and validate a temporary candidate, then commit it

Question 9

Why can automatically retrying a motor command be hazardous?

A. Retries always reduce network bandwidth

B. A non-idempotent command may be executed more than once

C. Retries prevent acknowledgement messages

D. A retry can never occur after a timeout

Correct Answer: B. A non-idempotent command may be executed more than once

Question 10

Which statement best describes fault containment?

A. Removing every possible fault from the system

B. Limiting a failure's effects to a defined boundary

C. Converting all errors into assertions

D. Allowing failed components to continue without supervision

Correct Answer: B. Limiting a failure's effects to a defined boundary

Object-Oriented Programming with C++
Chapter 8 of 14