Automatic Objects

Introduction

Traditional object-oriented design should change to allow for some of the newer features of the C++ language. Exceptions require all code to be re-evaluated. Many functions and objects no longer function properly if one assumes that exceptions may be thrown at any time. In order for code to remain deterministic, we must develop tools to help us write exception-safe code. One valid method is to wrap anything where an exception may occur in a try...catch statement. However, this method has the drawbacks of detracting from code readability and mainainability, and the levels of try..catch statements to perform even simple tasks in an exception-safe way quickly becomes unbearable. Having a try...catch...finally construct would alleviate some of the problem, but the levels of these constructs could still quickly become unbearable. Automatic objects are designed to help address this problem.

Inspiration: auto_ptr

The inspiration for creating a sub-class of objects comes from a single automatic object included in the Standard C++ Library, named the auto_ptr. The auto_ptr simply deletes whatever object it's pointing to. The auto_ptr can transfer its ownership to another auto_ptr through copy construction or the assignment operator.

The auto_ptr is incredibly useful; however, it would be nice if one had different automatic objects for different needs, so that by creating a local instance of that automatic object, one could then relax, knowing that the operation requested will be carried out when the object leaves scope no matter what.

Analysis of design

The design of the auto_ptr is governed by some simple guidelines:

Definition of automatic objects

The simplest definition of an automatic object is an object that fulfils its purpose in its destructor. However, good programming practice for automatic objects require that they should follow the guidelines above. Namely:


Introducing the auto_action

Terminology

Design of auto_action

The auto_action class is designed to help in the construction of new automatic objects. It provides an inheritable interface for the concept and transfer of Ownership (through the use of a pointer), Forced Execution (performing the actions in the destructor), and Flexibility (through the use of the member function T * release(void);). It is templated on 1) the pointer type, 2) the function object that contains the actions to execute, and 3) the safety measures to be used when executing the action. It also fulfils the Simplicity and Minimal Overhead guidelines (all functions are inlined and non-virtual).

When deriving from auto_action, first create your function object, then your automatic object. Your function object should have a default constructor, unless you provide an instance of it from the constructor of your automatic object. Your automatic object should have a constructor that takes a pointer to T or constant pointer to T, defaulting to 0, and pass that to the constructor of auto_action.

auto_action Interface

template <typename T, typename TFunctionObject, typename Safetype = auto_action_safe>
class auto_action
{
  public:
    typedef T object_type;
    typedef TFunctionObject action_type;
    typedef Safetype safety_type;

    explicit auto_action(T * const NPtr = NULL, const TFunctionObject & NFunctionObject = TFunctionObject());
    auto_action(auto_action<T, TFunctionObject> & Other);
    ~auto_action();

    auto_action<T, TFunctionObject> & operator=(auto_action<T, TFunctionObject> & Other);

    T * release(void);
    T * get(void) const;
    const action_type & action(void) const;
    void execute(void);
    void reset(T * const NPtr = NULL);
    bool owns(void) const;
};

Template Arguments:

A word on Safetype

One of the main uses for auto_action objects is so that actions will always be executed, even in code that may throw exceptions. auto_action_safe is almost always the best choice, because if an exception is thrown during stack unwinding, terminate() is called. Only use auto_action_fast if you are sure that the function object cannot throw exceptions, or that it will not be called during stack unwinding, and you are willing to bet the well-being of your program on it :)

Typedefs:

Constructors:

Destructor:

Copy Constructor:

Member Accessors:

Member Functions:


A simple example: autoNULL

This is an automatic object that binds to a pointer, and its action is to set that pointer to 0. It is used to make sure that a pointer is set to 0 whenever a scope exits.

namespace details {

template <typename T>
struct autoNULL_FunctionObject
{
  void operator()(T * Ptr) { *Ptr = 0; }
};

} // namespace details

template <typename T, typename Safetype = auto_action_safe>
struct autoNULL: public auto_action<T *,
    details::autoNULL_FunctionObject<T *>, Safetype>
{
  typedef auto_action<T *,
      details::autoNULL_FunctionObject<T *>, Safetype> parent_type;
  explicit autoNULL(T ** const NPtr = 0):parent_type(NPtr) { }
};

First we define the function object. We put it into namespace 'details' to avoid namespace pollution. It is a simple object, that is templated on a type T (which will always be a pointer type), and sets its argument to 0.

Then we define the automatic object type itself. We allow the user to determine what type of pointer to set to 0. (In this case, T is not necessarily a pointer type), and inform auto_action (through the template parameters) that we are holding a pointer to a pointer, and our particular function object type.

All automatic objects must explicitly provide all constructors. It is customary to provide a default constructor that creates an unbound automatic object. Our example constructor satisfies the binding and unbinding constructions. Like many automatic objects, we use the default function object.

autoNULL inherits concepts of ownership, the execution of the action in the destructor, etc. all from auto_action.

Example of usage:

extern int * x;
void func(void)
{
  // ... (1)
  autoNULL x_guard(&x);
  // ... (2)
}
If any exceptions or return statements are executed in (1), x will not be set to 0. However, once x_guard is constructed, any exceptions may be thown, or the function may return; in either case, x_guard will ensure that x points to 0 when the execution path leaves this scope.

Simple Automatic Objects

An automatic object is simple if it satisfies the following:

Furthermore, simple automatic objects follow some conventions:

Simple automatic objects are fairly common, and for that reason a macro is provided: BOOST_AUTOACTION_DEFINE_SIMPLE(PrimaryActionName) simplifies defining simple automatic objects. :)

#define BOOST_AUTOACTION_DEFINE_SIMPLE(PrimaryActionName) \
template <typename T, typename Safetype = ::boost::auto_action_safe> \
struct auto_ ## PrimaryActionName : \
    public ::boost::auto_action<T, ::std::mem_fun_t<void, T>, Safetype> \
{ \
  typedef ::boost::auto_action<T, ::std::mem_fun_t<void, T>, Safetype> parent_type; \
  explicit auto_ ## PrimaryActionName(T * const NPtr = 0) \
  :parent_type(NPtr, ::std::mem_fun_t<void, T>(&(T::PrimaryActionName))) { } \
  void PrimaryActionName (void) { execute(); } \
  auto_ ## PrimaryActionName & operator=(auto_ ## PrimaryActionName & Other) \
  { \
    parent_type::operator=(Other); \
    return *this; \
  } \
}

Example of usage:

BOOST_AUTOACTION_DEFINE_SIMPLE(pop);

extern std::stack<int> s;

void func(void)
{
  s.push(13);
  auto_pop<std::stack<int> > s_guard(&s);
  // ... (1)
  s_guard.pop(); // or "s_guard.execute();" (2)
  // ... (3)
}
In this example, the code in (1) is expecting '13' to be on the stack. However, if an exception is thrown in (1), the '13' will be automatically popped off, because code calling 'func' will not be expecting the extra variable on the stack. The next line of code (2) executes the automatic object. Code in (3) may throw exceptions or return whenever it wants; the automatic object has already been executed and therefore is now unbound. In (1), s_guard.owns() is true. In (3), s_guard.owns() is false.

Recommended Usage

Automatic objects are especially useful in the following situations: