Defensive Programming

Steven Zeil

Last modified: Jul 22, 2016
Contents:

Defensive programming is an approach to coding based on minimizing assumptions made by the programmer.

1 Common Assumptions

The Glass is Half-Full

Programmers are optimists by nature. We always believe that our programs are actually going to run “as soon as I fix this one little problem” – despite the fact that, time and time again, we are proven wrong.

Odds are, if we weren’t such blind optimists, we would never actually dare to write a single line of code.


**Programmers Like to Assume … **


What to do About Assumptions?

Two good possibilities

And these are not mutually exclusive.

2 Documenting Assumptions

Preconditions

It’s common for functions to only work under certain circumstances.


Documenting Preconditions

bidcollection.h
#ifndef BIDCOLLECTION_H
#define BIDCOLLECTION_H

#include "bids.h"

class BidCollection {

  int MaxSize;
  int size;
  Bid* elements; // array of bids 

public:

  /**
   * Create a collection capable of holding the indicated number of bids
   */
  BidCollection (int MaxBids = 1000);

  ~BidCollection ();

  

  // Access to attributes
  int getMaxSize() const {return MaxSize;}

  int getSize() const {return size;}



  // Access to individual elements

  const Bid& get(int index) const {return elements[index];}



  // Collection operations


  void addInTimeOrder (const Bid& value);
  //  Adds this bid into a position such that 
  //   all bids are ordered by the time the bid was placed
  //Pre: getSize() < getMaxSize()



  void remove (int index);
  // Remove the bid at the indicated position
  //Pre: 0 <= index < getSize()


  /**
   * Read all bids from the indicated file
   */
  void readBids (std::string fileName);

};


#endif

Internal Assumptions

Internal assumptions can be documented with comments.

E.g., in resolveAuction,

// If highestBidSoFar is non-zero, we have a winner
    if (reservePriceMet && highestBidSoFar > 0.0)
      {
        int bidderNum = bidders.findBidder(winningBidderSoFar);
        // winningBidderSoFar should be in the list of registered bidders
        cout << item.getName()
             << " won by " << winningBidderSoFar
             << " for " << highestBidSoFar << endl;
        Bidder& bidder = bidders.get(bidderNum);
        bidder.setBalance (bidder.getBalance() - highestBidSoFar);
      }

bidderNum should be valid, because all bids should be placed by bidders registered with the auction.

So we document this assumption.

3 Guarding Assumptions

Suppose that

It’s not your fault, but what do you do?


Possible Reactions

  1. Abort the program with an error message

  2. Ignore it and let the program continue

  3. Quietly correct the input and let the program continue.

  4. Issue an error message, correct the input, and let the program continue.

Question: Which of these are legal responses?

**Answer**

Question: Which of these are reasonable responses?

Let’s think about that a bit…


Example

E.g., for

int BidderCollection::add (const Bidder& value)
//  Adds this bidder
//Pre: getSize() < getMaxSize()
{
  addToEnd (elements, size, value);
  return size - 1;
}

What should we do if add is called on a full collection?


Abort the program with an error message

int BidderCollection::add (const Bidder& value)
//  Adds this bidder
//Pre: getSize() < getMaxSize()
{
  if (size < MaxSize) 
    {
     addToEnd (elements, size, value);
     return size - 1;
    }
  else
    {
     cerr << "BidderCollection::add - collection is full" << endl;
     exit(1);
    }
}

Ignore it and let the program continue


Quietly correct the input and let the program continue

int BidderCollection::add (const Bidder& value)
//  Adds this bidder
//Pre: getSize() < getMaxSize()
{
  if (size < MaxSize) 
    {
     addToEnd (elements, size, value);
     return size - 1;
    }
  else
    {
     elements[size-1] = value;
    }
}

Issue an error message and let the program continue

int BidderCollection::add (const Bidder& value)
//  Adds this bidder
//Pre: getSize() < getMaxSize()
{
  if (size < MaxSize) 
    {
     addToEnd (elements, size, value);
     return size - 1;
    }
  else
    {
     cerr << "BidderCollection::add - collection is full" << endl;
     elements[size-1] = value;
    }
}
errorMsg.cpp
int BidderCollection::add (const Bidder& value)
//  Adds this bidder
//Pre: getSize() < getMaxSize()
{
  if (size < MaxSize) 
    {
     addToEnd (elements, size, value);
     return size - 1;
    }
  else
    {
     cerr << "BidderCollection::add - collection is full" << endl;
     elements[size-1] = value;
    }
}

Reaslitically, our two “quiet” options risk serious problems down the road. Quietly ignoring evidence that a failure has occurred is dangerous. It can lead to corrupted output files and databases and embarrassingly bad outputs.

Remember, undetected faulty output can lead to disastrous losses of money property and may even be fatal.

For those reasons, I’m not particularly fond of option #4, either. I know that many programming instructors will say that a good program should never crash, no matter what input it is given. I would argue that aborting/crashing is often the safest thing you could do.

3.1 Guarding Assumptions with Assertions

assert(c); (from the header file <cassert>) tests to see if a boolean condition c is true. If not, it issues an error message and aborts the program.


assert() Example

    if (reservePriceMet && highestBidSoFar > 0.0)
      {
        int bidderNum = bidders.findBidder(winningBidderSoFar);
        // winningBidderSoFar should be in the list of registered bidders
        assert (bidderNum >= 0 && bidderNum < bidders.getSize());
        cout << item.getName()
             << " won by " << winningBidderSoFar
             << " for " << highestBidSoFar << endl;
        Bidder& bidder = bidders.get(bidderNum);
        bidder.setBalance (bidder.getBalance() - highestBidSoFar);
      }

Guarding Preconditions

Perhaps the most common use of assertions is in guarding pre-conditions.

biddercollection.cpp
#include <iostream>
#include "arrayUtils.h"
#include <fstream>
#include <cassert>

#include "biddercollection.h"

using namespace std;


/**
 * Create a collection capable of holding the indicated number of items
 */
BidderCollection::BidderCollection (int MaxBidders)
  : MaxSize(MaxBidders), size(0)
{
  elements = new Bidder [MaxSize];
}

BidderCollection::~BidderCollection ()
{
  delete [] elements;
}



// Collection operations


int BidderCollection::add (const Bidder& value)
//  Adds this bidder
//Pre: getSize() < getMaxSize()
{
  assert (size < MaxSize);
  addToEnd (elements, size, value);
  return size - 1;
}


void BidderCollection::remove (int index)
// Remove the bidder at the indicated position
//Pre: 0 <= index < getSize()
{
  assert (0 <= index && index < MaxSize);
  removeElement (elements, size, index);
}




/**
 * Read all bidders from the indicated file
 */
void BidderCollection::readBidders (std::string fileName)
{
    size = 0;
    ifstream in (fileName.c_str());
    int nBidders;
    in >> nBidders;
    for (int i = 0; i < nBidders && i < MaxSize; ++i)
    {
      string nme;
      double bal;
      in >> nme >> bal;
      Bidder bidder (nme, bal);;
      add (bidder);
    }
}


/**
 * Find the index of the bidder with the given name. If no such bidder exists,
 * return nBidders.
 */
int BidderCollection::findBidder (std::string name) const
{
  int found = size;
  for (int i = 0; i < size && found == size; ++i)
    {
      if (name == elements[i].getName())
	found = i;
    }
  return found;
}


// Print the collection
void BidderCollection::print (std::ostream& out) const
{
  out << size << "/" << MaxSize << "{";
  for (int i = 0; i < size; ++i)
    {
      out << "  ";
      elements[i].print (out);
      out << "\n";
    }
  out << "}";
}