list<type>::insert
A while ago I 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 internal list node could throw
std::bad_alloc
for example). However, you can’t use this
insert
in embedded C++ for two reasons.
-
Embedded C++ does not support templates. I regard this as a minor problem. In this case templates are being used to get the compiler to write code. You can fake this use of templates quite easily if you put your mind to it.
-
Embedded C++ does not support exceptions. This is definitely not a minor problem. What to do?
The role of failure
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.
#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 |
Listing 1: loud_bool.hpp |
To use an analogy, consider taking a short walk to the local shop for a pint of milk. Do you:
a) check whether you haven’t died after every step, or
b) just walk to the shop and hope you don’t die.
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. If you’re still alive you’ll be much more concerned about getting to the nearest hospital.
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 a state to do anything so the only sensible and practical approach is to make arrangements before you set off.
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 constantly 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.
#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; } |
Listing 2: loud_bool.cpp |
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.) See Listings 1-3.
The
loud_bool
class generalizes to the template class shown in Listings 4-6 for those of you working in full C++ (I’ve not put it in a namespace to save horizontal space).
#include "loud_bool.hpp" loud_bool example1() { //... return true; } int main() { bool result = example1(); //no warning example1(); //generates warning return 0; } |
Listing 3: Example use of loud_bool |
#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 |
Listing 4: warn_if_ignored.hpp |
The final step (left as exercise for the reader) is to parameterize the behaviour if the result is ignored.
Jon Jagger
jon@jaggersoft.com
References [1] To Engineer is Human. The Role of Failure in Successful Design. Henry Petroski. Vintage. 0-679-73416-3
#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; } |
Listing 5: warn_if_ignored-template.hpp |
#include "warn_if_ignored.hpp" warn_if_ignored<bool> example2() { // ... return false; } int main() { bool result = example2(); //no warning example2(); //generates warning return 0; } |
Listing 6: Example use of warn_if_ignored template |