Trip Switch Booleans in C++

© Bill Rubin, published in ACCU's CVu, , pp. 12—13.

I've long held the view that advances in programming have come as much from adding constraints as from adding features. Computers were first programmed in machine language with no constraints. Instructions and data could be freely intermixed, either being used as the other. The advance of high-level languages (FORTRAN, COBOL) constrains instructions and data to be separate. The advance of structured programming (C, PL/I) constrains the flow of control among statements. The advance of object-oriented programming constrains the scope of functions and data. This short note proposes two modest C++ classes to constrain the way a boolean variable can be changed. These classes encapsulate what I call the trip switch protocol. When this protocol is applicable, the classes reduce software entropy, compared with using bare bool variables.

A trip switch is a two state device like a common household circuit breaker. A circuit breaker can be manually “initialized” to the “on” state, and can automatically be “tripped” into the “off” state, but it cannot automatically change back into the “on” state. Once off, it stays off. The protocol of a trip switch can be represented by a bool variable with “on” and “off” mapped into true and false values.

Unencapsulated Trip Switch

Consider how I first used the trip switch protocol without encapsulating it: I was doing file I/O with a class File. I defined a bool data member called isValid_, and used it as a trip switch to manage file validity as shown below. (The File class is pseudo-code, not really what I wrote, but simply intended to illustrate implementation of the protocol.)

class File {
  public:
    File(/*  some arguments */) : isValid_(true), ... {...}
    File(/* other arguments */) : isValid_(true), ... {...}

    void access() {
      if(pathNotFound())    isValid_ = false;
      else if(openFailed()) isValid_ = false;
      else if(readFailed()) isValid_ = false;
      }

    bool isValid() const {return isValid_;}
    // Other member functions ...

  private:
    bool isValid_;  // See Note 1 below.
    // Other data members ...
};

/* Note 1:  This bool is initialized to true, and
may be set to false, but is never set back to true. */

Data member isValid_ is initialized to true in each constructor. During each stage of accessing the file, isValid_ is set to false if something goes wrong. Once false, it makes no sense to set it to true.

For industrial-strength code, it is good programming practice to include the code comment Note 1 to document the intent of the design surrounding the isValid_ usage. Without this code comment, future readers of the code can never be sure of the intent. They can search the code base and discover that isValid_is never explicitly assigned the value true, but that fact alone does not imply that the designer intended that it never should be. Thus, Note 1 gives important guidance, not otherwise available, to maintainers and other code readers.

Problems with Unencapsulated Trip Switch

There are at least three problems with the above approach of using a bare bool data member to implement the isValid_ trip switch protocol. Firstly, the decision to employ the trip switch protocol is not made in one place, but is distributed throughout the File class. In a realistic setting, the statements involving isValid_ would not be grouped together, but would be buried among all sorts of other concerns in both header and source files. The design principle of separation of concerns is not well served.

Secondly, it is easy for a maintenance programmer to overlook Note 1, especially when the File class is much more complex than shown here. If this occurs, the maintenance programmer may degrade the code base by introducing an assignment statement

isValid_ = true;

somewhere in the File class implementation. This statement degrades the code base for one of the following two reasons:

  1. The maintenance programmer inadvertently violated the design intent, possibly introducing a bug.
  2. The maintenance programmer correctly assumed (or was indifferent to) the design intent, and deliberately changed it. But because the programmer overlooked Note 1, it remains in the code, and is now wrong. A wrong comment is far worse than no comment. It will surely confound future maintainers.

Thus, the insertion of a code comment, while laudable, is by no means foolproof.

Thirdly, the bare bool approach doesn’t scale well. I recently wrote a very small application which required half a dozen trip switch protocol instances in three enclosing classes. (Some instances used the inverse protocol, with “on” and “off” mapped into false and true.) Adding the constructor initializer for each constructor of the enclosing class, and for each bool data member, and adding the same code comment to each data member all violate the DRY principle (Don’t Repeat Yourself).

Encapsulated Trip Switch

All the above problems are avoided by encapsulating the trip switch protocol in the following simple class:

class BoolSettableToFalse {

public:
    BoolSettableToFalse(const bool v = true) : value_(v) {}
    operator bool() const {return value_;}
    void setToFalse() {value_ = false;}

private:
    BoolSettableToFalse& operator=(const BoolSettableToFalse&);

    bool value_;
};

An object of type BoolSettableToFalse can be used almost exactly as a bool object, since it implicitly converts to and from bool. The main exception is that it cannot be assigned to. Only the setToFalse() member function can be used to change the value. There’s no way to set the value to true, and that’s the whole point of the class. There is no performance penalty — the run time and space are the same as for a bare bool variable.

The copy assignment operator is private to prevent protocol violations; one could otherwise change a false BoolSettableToFalse to a true one by assignment. The conversion constructor from bool is needed so that bool variables can be implicitly converted to BoolSettableToFalse. This constructor allows initializing to false, which is useful for BoolSettableToFalse instances which start out life already tripped.

The big advantage of the encapsulation approach is that it enforces the trip switch protocol at compile time. Variables of type BoolSettableToFalse clarify the design intent, and eliminate the need for descriptive comments and for initializers in enclosing class constructors. The decision to employ the trip switch boolean protocol is made in one place — the variable declaration — rather than being distributed throughout all references to the variable. The only disadvantage I am aware of is that the setToFalse() member function is less intuitive than a simple assignment.

Applying BoolSettableToFalse to the File class example yields

class File {
  public:
    void access() {
      if(pathNotFound())    isValid_.setToFalse();
      else if(openFailed()) isValid_.setToFalse();
      else if(readFailed()) isValid_.setToFalse();
      }

    bool isValid() const {return isValid_;}

  private:
    BoolSettableToFalse isValid_;
};

The new File class differs from the original implementation in the following ways:

  1. The type of isValid_ has been changed from bool to BoolSettableToFalse.
  2. The File constructors are not shown, because they no longer participate explicitly in isValid_ initialization. (They may still be needed, but not to implement the trip switch protocol.)
  3. The Note 1 code comment has been removed, because it’s no longer needed. The design intent is encapsulated in BoolSettableToFalse.
  4. The isValid_ assignment statements have been replaced by calls to setToFalse().

In this implementation, the decision to use the trip switch protocol is made in one place, by specifying the type of the isValid_ data member. Less client code and no client comments are needed to support the protocol. The only code that must change is the type of isValid_ and its assignment statements. A maintenance programmer cannot inadvertently violate the integrity of the code base because assignments to isValid_ will not compile, and because there are no longer any code comments to become incorrect.

The dual of BoolSettableToFalse is BoolSettableToTrue, which is useful for the inverse protocol:

class BoolSettableToTrue {

public:
    BoolSettableToTrue(const bool v = false) : value_(v) {}
    operator bool() const {return value_;}
    void setToTrue() {value_ = true;}

private:
    BoolSettableToTrue& operator=(const BoolSettableToTrue&);

    bool value_;
};

Of course, both these classes are useful not only for data members, but also for automatic variables and so on.

Summary

The BoolSettableToFalse and BoolSettableToTrue classes can be used in place of bools to implement the trip switch protocol. The advantages are typical of those for encapsulation generally: Behavior is enforced at compile time; code comments are not needed; the protocol is implemented only once; multiple instances scale up efficiently. Some may argue that the trip switch booleans are too trivial to bother with. My view is that simple classes like these are part of a style of programming in which a large number of “trivial” refinements accumulate to provide a much more robust code base.