Jon Jagger
jon@jaggersoft.com

A C++ Type That Doesn't Like Being Ignored

(Overload 2003 Article of the Year)

list<type>insert

A while ago finished a contract to write a small subset of STL targeted at embedded C++. This proved an interesting challenge. For example, in full STL, you insert a value into a list using, you'll never guess, list::insert.

    template<typename type>
    list<type>::iterator 
    list<type>::insert(iterator position, const type & value)
    { ... 
    }

This returns an iterator to the inserted copy of the value or it throws an exception (allocating the interal list node could throw std::bad_alloc for example). However, you can't use this insert in embedded C++ for two reasons.

Copying Without Exceptions

My first choice was to emulate exceptions. However, for various understandable reasons, my client decided not to adopt an exception-like mechanism. I'll explain what I did shortly, but first I'd like to make a short diversion and explain why emulating exceptions is my strong preference.

It's all to do with failure [1]. The role of failure is fundamental to writing good software. When you use STL list<type>::insert you don't have to worry about its return value if it fails because if it fails it doesn't return. This allows you to write sequences of expressions and statements without having to constantly check if anything went wrong. This is really important. To use an analogy, consider taking a short walk to the local shop for a pint of milk. Do you:

Answer; you just walk to the shop of course. No checks if you've died yet. If something serious happens (eg you're hit by a bus) then you won't get to the shop but frankly you won't care much about the milk by then anyway. The point is that constantly checking if you've died is silly not because it slows your journey so massively but because it's exactly the wrong thing to be concerned about in the first place. The right thing to be concerned about is what happens if you are hit by a bus. The reality is simple. If you are hit by a bus you won't be in much of state to do anything so the only sensible and practical approach is to make arrangements before you set off (eg carry some form of id).

My conclusion is this: The software model of use that exceptions bring with them is so valuable that if you're working in an environment (or language subset) that doesn't support exceptions you should consider ways in which you can emulate them. The alternative is constanty checking if you've died.

Back to the Story

But as I said, my client decided not to adopt an exception-like mechanism. So what did I do? I considered making list::insert return an iterator equal to end() if the insertion failed but decided this was not a good idea. When things are different they should be clearly different. One option is to return a bool and a list::iterator inside a pair-like structure. However, as it turned out, my client's particular list requirement didn't require the returned iterator, so the version of list<type>::insert I wrote looked like this:

    bool
    list::insert(iterator position, const type & value);

Lacking exceptions, this list::insert returns a bool to signify success or failure. The problem now of course is that the bool return is just too easy to ignore. In contrast, exceptions, by design, are impossible to ignore. In full STL if you're not interested in the iterator return value, ignoring it is precisely what you do. I thought about this for a while before realizing there was a solution. The problem is that if you ignore a bool nothing happens. So the answer is not to use a bool! Use something that looks like a bool, smells like a bool, feels like a bool, and shouts loudly when ignored. (In practice, this version for embedded C++ has to be modified slightly because embedded C++ does not support the mutable keyword either.)

C++ Code

loud_bool.hpp
#ifndef LOUD_BOOL_INCLUDED
#define LOUD_BOOL_INCLUDED

class loud_bool
{
public: // construction/destruction
    
    loud_bool(bool decorated);
    loud_bool(const loud_bool &);
    ~loud_bool();

public: // conversion

    operator bool () const;

private: // inappropriate

    loud_bool & operator=(const loud_bool &);

private: // state

    bool result;
    mutable bool ignored;
};

#endif
loud_bool.cpp
#include "loud_bool.hpp"
#include <ios>
#include <iostream>

loud_bool::loud_bool(bool decorated)
    : result(decorated)
    , ignored(true)
{
    // all done
}

loud_bool::loud_bool(const loud_bool & other)
    : result(other.result)
    , ignored(other.ignored)
{
    other.ignored = false;
}
    
loud_bool::~loud_bool()
{
    if (ignored)
    {
        std::cerr.setf(std::ios_base::boolalpha);
        std::cerr << "WARNING: return " 
                  << result 
                  << "; is being ignored" 
                  << std::endl;
    }
}

loud_bool::operator bool () const 
{
    ignored = false;
    return result;
}
example.cpp
#include "loud_bool.hpp"

loud_bool example1()
{
    //...
    return true;
}

int main()
{
    bool result = example1(); // no warning
    example1();               // generates warning

    return 0;
}

The loud_bool class generalizes to the following template class for those of you working in full C++ (I've not put it in a namespace to save horizontal space).

warn_if_ignored.hpp
#ifndef WARN_IF_IGNORED_INCLUDED
#define WARN_IF_IGNORED_INCLUDED

template<typename result_type>
class warn_if_ignored
{
public: // construction/destruction
    
    warn_if_ignored(const result_type & decorated);
    warn_if_ignored(const warn_if_ignored &);
    ~warn_if_ignored();

public: // conversion

    operator result_type () const;

private: // inappropriate

    warn_if_ignored & operator=(const warn_if_ignored &);

private: // state

    result_type result;
    mutable bool ignored;
};

#include "warn_if_ignored-template.hpp"

#endif
warn_if_ignored-template.hpp
#if !defined WARN_IF_IGNORED_INCLUDED || defined WARN_IF_IGNORED_INCLUDED_TEMPLATE
#error      "warn_if_ignored-template.hpp" #included directly
#endif

#define      WARN_IF_IGNORED_INCLUDED_TEMPLATE   

#include <ios>
#include <iostream>

template<typename result_type>
warn_if_ignored<result_type>::warn_if_ignored(
    const result_type & decorated
)
    : result(decorated)
    , ignored(true)
{
    // all done
}

template<typename result_type>
warn_if_ignored<result_type>::warn_if_ignored(
    const warn_if_ignored & other
)
    : result(other.result)
    , ignored(other.ignored)
{
    other.ignored = false;
}
    
template<typename result_type>
warn_if_ignored<result_type>::~warn_if_ignored()
{
    if (ignored)
    {
        std::cerr << "WARNING: return " 
                  << result 
                  << "; is being ignored" 
                  << std::endl;
    }
}

template<typename result_type>
warn_if_ignored<result_type>::operator result_type () const 
{
    ignored = false;
    return result;
}
example2.cpp
#include "warn_if_ignored.hpp"

warn_if_ignored<bool> example2()
{
    //...
    return false;
}

int main()
{
    bool result = example2(); // no warning
    example2();               // generates a warning

    return 0;
}

[1] To Engineer is Human. The Role of Failure in Successful Design. Henry Petroski. Vintage. 0-679-73416-3

{ JSL }
Jagger Software Ltd
Company # 4070126
VAT # 762 5213 42