C++ / a working model

101 / 163   ·   C++11   ·   9 min

Starting from design constraints: using a value type to uphold interval invariants

Keep this sentence

D&E Chapter 1 places expressiveness, runtime efficiency, and tool availability in the same engineering problem. This lesson, based on the already-read English sample, designs a small interval value type: it does not rely on inheritance and does not pursue syntactic showmanship, but establishes verifiable invariants at the construction boundary.

In this lesson
  1. A class is first a conceptual boundary, not an inheritance node
  2. Make normal results and rejected input explicit
  3. Leave performance conclusions to the actual environment
  4. Example
  5. Exercise
READING EVIDENCE / Full text read

The Design and Evolution of C++

The substantial main text of all 18 chapters has actually been read: Chapter 1 is taken from the publisher's English sample printed pages 19–25; Chapters 2–18 are taken from the 2002 English reprint scan, covering PDF pages 38–431 continuously page by page (including subsection pages; blank pages omitted from the scan are not counted as main text). Remote OCR text and code were read paragraph by paragraph; incompletely recognized code, tables, and inheritance/RTTI diagrams were verified against the original images; the final section PDF 387–431 was read directly from the images. 'full' means the 18 chapters of main text are complete, without claiming word-by-word proofreading or having read the bibliography and index at the end of the book. This reprint incorporates subsequent corrections including those from 1995 and does not pretend to be the unrevised 1994 first printing; the Chinese translation is not counted in the coverage.

Edition, actual reading range, and original sources →

A class is first a conceptual boundary, not an inheritance node

The Chapter 1 that was read describes a tension: a type system that can directly express application concepts helps design, but linking, runtime, and portability issues of the tools can equally decide the success or failure of a project. This lesson does not turn historical experience into a language ranking, but converts it into a small problem: how to ensure that an integer interval is always valid?

If the two bounds are passed independently throughout the program, every caller must remember that the left end cannot be greater than the right end. Putting them into a type and having the constructor check the relationship concentrates this constraint at the object entry point. Private members prevent external modification of one end that bypasses the check; no base class, virtual functions, or heap objects are needed here.

Make normal results and rejected input explicit

The example uses a half-open interval [first,last), so an empty interval is a legal state and the right end does not belong to the interval. contains only compares integers and does not compute the difference of the endpoints, so there is no need for extra handling of signed subtraction overflow. Upon discovering a reversed order at construction, an exception is thrown immediately rather than silently swapping the endpoints; swapping would conceal the problem of the caller passing the arguments backwards.

A successfully constructed object always satisfies the invariant. Construction failure means there is no complete object that can be given to the user, which is different from first creating an invalid object and then requiring the user to call a repair function. Assertions are used for behavioral checks in the example; input checks use actual branches and cannot disappear just because assertions are turned off.

Leave performance conclusions to the actual environment

Encapsulation does not require adding dynamic allocation; the state of the object in this example consists of only two integers. But this does not equal a promise that the class size will necessarily equal the sum of two integers on all compilers, nor a promise that functions will always be inlined. The standard specifies observable semantics; the concrete layout and machine instructions are decided by the implementation.

This design can still be used in a C++20 project; the interface is increased only when the need actually increases. For example, when adding a width operation, the integer range must be re-examined, and one cannot assume all arithmetic is safe just because the current comparison is safe. This is derivation from constraints to mechanisms, not an as-is upgrade of the early code in the book. The theme of this lesson is taken from Chapter 1; the actual reading progress of other chapters is independently recorded in the bibliographic coverage notes and cannot be inferred from this lesson that the entire book has been read.

Pitfalls

  • Do not use assert in place of runtime checks on external parameters; after defining NDEBUG, assert may be removed.
  • A half-open interval allows first to equal last; do not misjudge an empty interval as construction failure, and do not directly use int subtraction to obtain the width of arbitrary endpoints.

Run an example

Minimum C++11 · complete program · Download .cpp

#include <cassert>
#include <iostream>
#include <stdexcept>

class Interval {
    int first_;
    int last_;
public:
    Interval(int first, int last) : first_(first), last_(last) {
        if (first > last) throw std::invalid_argument("reversed interval");
    }
    bool contains(int value) const {
        return first_ <= value && value < last_;
    }
};

int main() {
    const Interval work(3, 7);
    assert(work.contains(3));
    assert(!work.contains(7));
    const Interval empty(4, 4);
    assert(!empty.contains(4));
    bool rejected = false;
    try {
        const Interval invalid(7, 3);
        (void)invalid;
    } catch (const std::invalid_argument&) {
        rejected = true;
    }
    assert(rejected);
    std::cout << "interval invariant preserved\n";
}

Compile locally

g++ -std=c++11 -Wall -Wextra -Wpedantic -pthread books-design-and-evolution.cpp -o example && ./example

Expected result

interval invariant preserved

CHECK YOUR UNDERSTANDING

Close the answer. Explain it.

Add an empty() member function, and explain why it should not be implemented with last_-first_==0.

Show a reference answer

Adding bool empty() const { return first_ == last_; } is sufficient. Endpoint equality exactly expresses that a half-open interval is empty and needs no arithmetic. Subtraction would introduce unnecessary integer-range reasoning; for example, the endpoints of a legal interval may be close to INT_MIN and INT_MAX respectively, so the difference may overflow, whereas equality comparison is always safe.

Check the sources

Drafts and official chapters change. The version mark is only the example’s minimum.

Back to the catalog