Consider these functions which should return a value but which might not have a value to return:
(A) double sqrt(double n ); (B) char get_async_input(); (C) point polygon::get_any_point_effectively_inside();
There are different approaches to the issue of not having a value to return.
A typical approach is to consider the existence of a valid return value as a postcondition, so that if the function cannot compute the value to return, it has either undefined behavior (and can use asssert in a debug build) or uses a runtime check and throws an exception if the postcondition is violated. This is a reasonable choice for example, for function (A), because the lack of a proper return value is directly related to an invalid parameter (out of domain argument), so it is appropriate to require the callee to supply only parameters in a valid domain for execution to continue normally.
However, function (B), because of its asynchronous nature, does not fail just because it can't find a value to return; so it is incorrect to consider such a situation an error and assert or throw an exception. This function must return, and somehow, must tell the callee that it is not returning a meaningful value.
A similar situation occurs with function (C): it is conceptually an error to ask a null-area polygon to return a point inside itself, but in many applications, it is just impractical for performance reasons to treat this as an error (because detecting that the polygon has no area might be too expensive to be required to be tested previously), and either an arbitrary point (typically at infinity) is returned, or some efficient way to tell the callee that there is no such point is used.
There are various mechanisms to let functions communicate that the returned value is not valid. One such mechanism, which is quite common since it has zero or negligible overhead, is to use a special value which is reserved to communicate this. Classical examples of such special values are EOF, string::npos, points at infinity, etc...
When those values exist, i.e. the return type can hold all meaningful values plus the signal value, this mechanism is quite appropriate and well known. Unfortunately, there are cases when such values do not exist. In these cases, the usual alternative is either to use a wider type, such as 'int' in place of 'char'; or a compound type, such as std::pair<point,bool>.
Returning a std::pair<T,bool>, thus attaching a boolean flag to the result which indicates if the result is meaningful, has the advantage that can be turned into a consistent idiom since the first element of the pair can be whatever the function would conceptually return. For example, the last two functions could have the following interface:
std::pair<char,bool> get_async_input(); std::pair<point,bool> polygon::get_any_point_effectively_inside();
These functions use a consistent interface for dealing with possibly inexistent results:
std::pair<point,bool> p = poly.get_any_point_effectively_inside(); if ( p.second ) flood_fill(p.first);
However, not only is this quite a burden syntactically, it is also error prone since the user can easily use the function result (first element of the pair) without ever checking if it has a valid value.
Clearly, we need a better idiom.
In C++, we can declare an object (a variable) of type T, and we can give this variable
an initial value (through an initializer. (c.f. 8.5)).
When a declaration includes a non-empty initializer (an initial value is given), it is said that
the object has been initialized.
If the declaration uses an empty initializer (no initial value is given),
and neither default nor value initialization applies, it is said that the object is
uninitialized. Its actual value exist but has an
indeterminate inital value (c.f. 8.5.9).
optional<T>
intends to formalize the notion of initialization/no-initialization
allowing a program to test whether an object has been initialized and stating that access to
the value of an uninitialized object is undefined behaviour. That is,
when a variable is declared as optional<T> and no initial value is given,
the variable is formally uninitialized. A formally uninitialized optional object has conceptually
no value at all and this situation can be tested at runtime. It is formally undefined behaviour
to try to access the value of an uninitialized optional. An uninitialized optional can be assigned a value, in which case its
initialization state changes to initialized. And given the formal treatment of initialization
states in optional objects, it is even possible to
reset an optional to uninitialized.
In C++ there is no formal notion of uninitialized objects, which
means that objects always have an initial value even if indeterminate.
As discussed on the previous section, this has a drawback because you need additional
information to tell if an object has been effectively initialized.
One of the typical ways in which this has been historically
dealt with is via a special value: EOF,npos,-1, etc... This is equivalent to adding
the special value to the set of possible values of a given type.
On modern languages, this can be modeled with a discriminated
union of T and something else such as a trivial POD or enum.
Discriminated unions are often called variants.
A variant has a current type, which in our case is either T or something else. In C++,
such a variant would be typically implemented as a template class of the form: variant<T,nil_t>
There is precedence for a discriminated union as a model for an optional value: the Haskell Maybe builtin type constructor.
A discriminated union, which can be seen as a container which has an object of either type T or something else, has exactly the semantics required for a wrapper of optional values:
Because of the way a discriminated union is used for this purpose, it only matters whether its current type is T or not. We can put a layer on top of the variant hidding the other type transforming a container of fixed size 1 into a variable size container which either has a T or has nothing. Thus, the variant<T,nil_t> can be seen as if it were a variable-size fixed-capacity stack-based container with the following optional-oriented interface:
// Uninitialized (internally, current type is nil_t) optional<T>::optional(); // Initialized with 'v' (internally, current type is T) optional<T>::optional( T const& v ) ; // Back to uninitialized (current type is set to nil_t) void optional<T>::reset(); // Assigns 'v' whether previously initialized or not (current type is set to T) void optional<T>::reset( T const& v ) ; // Returns 'true' if the optional is initialized, 'false' otherwise. bool optional<T>::initialized() ; // If the optional is initialized (current type is T), returns a reference to its value. // Otherwise (current type is nil_t), the result is undefined. T const& optional<T>::ref() const ; T& optional<T>::ref() ; // If both are initialized, calls swap(T&,T&); // If only one is initialized, calls reset(T const&) and reset(). // If both are uninitalized, do nothing. void swap ( optional<T>& lhs, optional<T>& rhs ) ; // If both are initialized, compare values. // If only one is initialized, they are not equal. // If both are uninitalized, they are equal. bool operator == ( optional<T> const& lhs, optional<T> const& rhs ) ; bool operator != ( optional<T> const& lhs, optional<T> const& rhs ) ;
In C++, unlike many other languages, objects can be referenced indirectly by means of a pointer (or a reference). Pointers have several nice features, two of which are relevant to this development.
One is that a pointer has its own pointer value, which in effect
references the object being pointed to: the pointee. Consequently,
copies of pointers do not involve copies of pointees. This effect results in aliasing:
different pointers can refer to the same object.
The particular semantic that a copy of a pointer does not involve
a copy of the pointee is called shallow-copy, which is oppossed to deep-copy were
a copy of a wrapper involves a copy of the wrapped object (as with optional<>)
Since this is the semantic followed by pointers (and references), shallow-copy
(and therefore aliasing) is implied in pointer semantics.
The other relevant feature of a pointer is that a pointer can have a null pointer value. This is a special value which is used to indicate that the pointer is not referring to any object at all. In other words, null pointer values convey the notion of inexistent objects.
This meaning of the null pointer value allowed pointers to became a defacto standard for handling optional objects because all you have to do to refer to a value which you don't really have is to use a null pointer value of the appropriate type. Pointers have been used for decades—from the days of C APIs to modern C++ libraries—to refer to optional (that is, possibly inexistent) objects; particularly as optional arguments to a function, but also quite often as optional data members.
The possible presence of a null pointer value makes the operations that access the
pointee's value possibly undefined, therefore, expressions which use dereference
and access operators, such as: ( *p = 2 )
and ( p->foo())
,
implicitly convey the notion of optionality, and this information is tied to
the syntax of the expressions. That is, the presence of operators * and -> tell by
themselves—without any additional context—that the expression will be undefined unless
the implied pointee actually exist.
Furthermore, the existence of the pointee can be tested by a comparison against
the null pointer value or via a conversion to bool, which allows expressions of
the form: if ( p != 0 ), or if ( p ) to be used to test for the existence of the pointee.
Such a defacto idiom for referring to optional objects can be formalized in the form of a
concept: the OptionalPointee concept.
This concept captures the syntactic usage of operatos *, -> and conversion to bool to convey
the notion of optionality.
However, pointers are good to refer to optional objects, but not particularly good
to handle the optional objects in all other respects, such as initializing or moving/copying
them. The problem resides in the shallow-copy of pointer semantics: if you need to
effectively move or copy the object, pointers alone are not enough. The problem
is that copies of pointers do not imply copies of pointees. For example, as
was discussed in the motivation, pointers alone cannot be used to return optional
objects from a function because the object must move outside from the function and
into the caller's context.
A solution to the shallow-copy problem that is often used is to resort to dynamic
allocation and use a smart pointer to automatically handle the details of this.
For example, if a function is to optionally return an object X, it can use shared_ptr<X>
as the return value. However, this requires dynamic allocation of X. If X is
a builtin or small POD, this technique is very poor in terms of required resources.
Optional objects are essentially values so it is very convenient to be able to use automatic
storage and deep-copy semantics to manipulate optional values just as we do with ordinary
values. Pointers do not have this semantics, so are unappropriate for the initialization and
transport of optional values, yet are quite convenient for handling the access to the
possible undefined value because of the idiomatic aid present in the OptionalPointee
concept incarnated by pointers.
Therefore, the final solution which is presented in this library is to shape the
previously discussed optional—which is a value-based container—as a model
of the OptionalPointee concept.
The optional<> template class presented with this library is a variation of the
sketch shown before (as a layer on top of a variant). It features deep-copy and
deep relational operators, but also models the OptionalPointee concept.
Instead of the member function 'initialized()' it has a safe conversion to bool,
and instead of the 'ref()' member function, it has operators*() and ->().
However, it is particularly important that optional<> objects are not mistaken by pointers,
they are not. optional<> does not model a pointer.
For instance, optional<> has not shallow-copy so does not alias: two different optionals
never refer to the same value (but my have equivalent values).
The difference between an optional<T> and a pointer must be kept in mind, particularly
because the semantics of relational operators are different: since optional<T>
is a value-wrapper, relational operators are deep: they compare optional values;
but relational operators for pointers are shallow: they do not compare pointee values.
As a result, you might be able to replace optional<T> by T* on some situations but
not always. Specifically, on generic code written for both, you cannot use relational
operators directly, and must use the template function
equal_pointees() instead.
namespace boost { template<class T> class optional { public : optional () ; explicit optional ( T const& v ) ; optional ( optional const& rhs ) ; template<class U> explicit optional ( optional<U> const& rhs ) ; optional& operator = ( optional const& rhs ) ; template<class U> optional& operator = ( optional<U> const& rhs ) ; T const* get() const ; T* get() ; T const* operator ->() const ; T* operator ->() ; T const& operator *() const ; T& operator *() ; void reset(); void reset ( T const& ) ; operator unspecified-bool-type() const ; bool operator!() const ; } ; template<class T> inline bool operator == ( optional<T> const& x, optional<T> const& y ) ; template<class T> inline bool operator != ( optional<T> const& x, optional<T> const& y ) ; template<class T> inline T* get_pointer ( optional<T> const& opt ) ; template<class T> inline void swap( optional<T>& x, optional<T>& y ) ; } // namespace boost
Note: the following section contains various assert() which are used only to show the postconditions as sample code. It is not implied that the type T must support each particular expression but that if the expression is supported, the implied condition holds.
optional<T>::optional();
Effect: Default-Constructs an optional.
Postconditions: *this is uninitialized.
Throws: Nothing.
Notes: T's default constructor is not called.
Example:
optional<T> def ; assert ( !def ) ;
explicit optional<T>::optional( T const& v )
Effect: Directly-Constructs an optional.
Postconditions: *this is initialized and its value is a copy of 'v'.
Throws: Whatever T::T( T const& ) throws.
Notes: T::T( T const& ) is called.
Exception Safety: Exceptions can only be thrown during T::T( T const& ); in that case, this constructor has no effect.
Example:
T v; optional<T> opt(v); assert ( *opt == v ) ;
optional<T>::optional( optional const& rhs );
Effect: Copy-Constructs an optional.
Postconditions: If rhs is initialized, *this is initialized and its value is a copy of the value of rhs; else *this is uninitialized.
Throws: Whatever T::T( T const& ) throws.
Notes: T::T( T const& ) is called if rhs is initialized.
Exception Safety: Exceptions can only be thrown during T::T( T const& ); in that case, this constructor has no effect.
Example:
optional<T> uninit ; assert (!uninit); optional<T> uinit2 ( uninit ) ; assert ( uninit2 == uninit ); optional<T> init( T(2) ); assert ( *init == T(2) ) ; optional<T> init2 ( init ) ; assert ( init2 == init ) ;
explicit template<U> optional<T>::optional( optional<U> const& rhs );
Effect: Copy-Constructs an optional.
Postconditions: If rhs is initialized, *this is initialized and its value is a copy of the value of rhs converted to type T; else *this is uninitialized.
Throws: Whatever T::T( U const& ) throws.
Notes: T::T( U const& ) is called if rhs is initialized, which requires a valid conversion from U to T.
Exception Safety: Exceptions can only be thrown during T::T( U const& ); in that case, this constructor has no effect.
Example:
optional<double> x(123.4); assert ( *x == 123.4 ) ; optional<int> y(x) ; assert( *y == 123 ) ;
optional& optional<T>::operator= ( optional const& rhs ) ;
Effect: Assigns another optional to an optional.
Postconditions: If rhs is initialized, *this is initialized and its value is a copy of the value of rhs; else *this is uninitialized.
Throws: Whatever T::T( T const& ) throws.
Notes: If *this was initialized, it is first reset to uninitialized using T::~T(), then T::T( T const& ) is called if rhs is initialized.
Exception Safety: Basic: Exceptions can only be thrown during T::T( T const& ); in that case, *this is left uninitialized.
Example:
T v; optional<T> opt(v); optional<T> uninit ; opt = uninit ; assert ( !opt ) ; // previous value (copy of 'v') destroyed from within 'opt'.
template<U> optional& optional<T>::operator= ( optional<U> const& rhs ) ;
Effect: Assigns another convertible optional to an optional.
Postconditions: If rhs is initialized, *this is initialized and its value is a copy of the value of rhs converted to type T; else *this is uninitialized.
Throws: Whatever T::T( U const& ) throws.
Notes: If *this was initialized, it is first reset to uninitialized using T::~T(), then T::T( U const& ) is called if rhs is initialized, which requires a valid conversion from U to T.
Exception Safety: Basic: Exceptions can only be thrown during T::T( U const& ); in that case, *this is left uninitialized.
Example:
T v; optional<T> opt0(v); optional<U> opt1; opt1 = opt0 ; assert ( *opt1 == static_cast<U>(v) ) ;
void optional<T>::reset( T const& v ) ;
Effect: Resets the current value.
Postconditions: *this is initialized and its value is a copy of 'v'.
Throws: Whatever T::T( T const& ) throws.
Notes: If *this was initialized, it is first reset to uninitialized using T::~T(), then T::T( T const& ) is called.
Exception Safety: Basic: Exceptions can only be thrown during T::T( T const& ); in that case, *this is left uninitialized.
Example:
optional<T> opt ( some_T ) ; assert( *opt == some_T ); opt.reset ( some_other_T ) ; assert( *opt == some_other_T );
void optional<T>::reset() ;
Effect: Destroys the current value.
Postconditions: *this is uninitialized.
Throws: Nothing.
Notes: T::~T() is called.
Example:
optional<T> opt ( some_T ) ; assert( *opt == some_T ); opt.reset(); assert( !opt );
T const* optional<T>::get() const ; T* optional<T>::get() ; inline T const* get_pointer ( optional<T> const& ) ; inline T* get_pointer ( optional<T>&) ;
Returns: If *this is initialized, a pointer to the contained value; else 0 (null).
Throws: Nothing.
Notes: The contained value is permanently stored within *this, so you should not hold nor delete this pointer
Example:
T v; optional<T> opt(v); optional<T> const copt(v); T* p = opt.get() ; T const* cp = copt.get(); assert ( p == get_pointer(opt) ); assert ( cp == get_pointer(copt) ) ;
T const* optional<T>::operator ->() const ; T* optional<T>::operator ->() ;
Requirements: *this is initialized.
Returns: A pointer to the contained value.
Throws: Nothing.
Notes: The requirement is asserted via BOOST_ASSERT().
Example:
struct X { int mdata ; } ; X x ; optional<X> opt (x); opt->mdata = 2 ;
T const& optional<T>::operator*() const ; T& optional<T>::operator*();
Requirements: *this is initialized
Returns: A reference to the contained value
Throws: Nothing.
Notes: The requirement is asserted via BOOST_ASSERT().
Example:
T v ; optional<T> opt ( v ); T const& u = *opt; assert ( u == v ) ; T w ; *opt = w ; assert ( *opt == w ) ;
optional<T>::operator unspecified-bool-type() const ;
Returns: An unspecified value which if used on a boolean context is equivalent to (get() != 0)
Throws: Nothing.
optional<T> def ; assert ( def == 0 ); optional<T> opt ( v ) ; assert ( opt ); assert ( opt != 0 );
bool optional<T>::operator!() ;
Returns: If *this is uninitialized,
true
; elsefalse.
Throws: Nothing.
Notes: This operator is provided for those compilers which can't use the unspecified-bool-type operator in certain boolean contexts.
Example:
optional<T> opt ; assert ( !opt ); *opt = some_T ; // Notice the "double-bang" idiom here. assert ( !!opt ) ;
bool operator == ( optional<T> const& x, optional<T> const& y );
Returns: If both x and y are initialied,
(*x == *y)
. If only x or y is initialized,false
. If both are uninitialized,true
.Throws: Nothing.
Notes: Pointers have shallow relational operators while optional has deep relational operators. Do not use operator == directly in generic code which expect to be given either an optional<T> or a pointer; use equal_pointees() instead
Example:
T x(12); T y(12); T z(21); optional<T> def0 ; optional<T> def1 ; optional<T> optX(x); optional<T> optY(y); optional<T> optZ(z); // Identity always hold assert ( def0 == def0 ); assert ( optX == optX ); // Both uninitialized compare equal assert ( def0 == def1 ); // Only one initialized compare unequal. assert ( def0 != optX ); // Both initialized compare as (*lhs == *rhs) assert ( optX == optY ) ; assert ( optX != optZ ) ;
bool operator != ( optional<T> const& x, optional<T> const& y );
Returns: !( x == y );
Throws: Nothing.
void swap ( optional<T>& x, optional<T>& y );
Effect: If both x and y are initialized, calls
swap(*x,*y)
using std::swap.
If only one is initialized, say x, calls:y.reset(*x); x.reset();
If none is initialized, does nothing.Postconditions: The states of x and y interchanged.
Throws: If both are initialized, whatever swap(T&,T&) throws. If only one is initialized, whatever T::T ( T const& ) throws.
Notes: If both are initialized, swap(T&,T&) is used unqualified but with std::swap introduced in scope.
If only one is initialized, T::~T() and T::T( T const& ) is called.Exception Safety: If both are initialized, this operation has the exception safety guarantees of swap(T&,T&).
If only one is initialized, it has the same basic guarantee as optional<T>::reset( T const& ).Example:
T x(12); T y(21); optional<T> def0 ; optional<T> def1 ; optional<T> optX(x); optional<T> optY(y); boost::swap(def0,def1); // no-op boost::swap(def0,optX); assert ( *def0 == x ); assert ( !optX ); boost::swap(def0,optX); // Get back to original values boost::swap(optX,optY); assert ( *optX == y ); assert ( *optY == x );
optional<char> get_async_input() { if ( !queue.empty() ) return optional<char>(queue.top()); else return optional<char>(); // uninitialized } void recieve_async_message() { optional<char> rcv ; // The safe boolean conversion from 'rcv' is used here. while ( (rcv = get_async_input()) && !timeout() ) output(*rcv); }
optional<string> name ; if ( database.open() ) { name.reset ( database.lookup(employer_name) ) ; } else { if ( can_ask_user ) name.reset ( user.ask(employer_name) ) ; } if ( name ) print(*name); else print("employer's name not found!");
class figure { public: figure() { // data member 'm_clipping_rect' is uninitialized at this point. } void clip_in_rect ( rect const& rect ) { .... m_clipping_rect.reset ( rect ) ; // initialized here. } void draw ( canvas& cvs ) { if ( m_clipping_rect ) do_clipping(*m_clipping_rect); cvs.drawXXX(..); } // this can return NULL. rect const* get_clipping_rect() { return get_pointer(m_clipping_rect); } private : optional<rect> m_clipping_rect ; };
class ExpensiveCtor { ... } ; class Fred { Fred() : mLargeVector(10000) {} std::vector< optional<ExpensiveCtor> > mLargeVector ; } ;
optional<bool>
should be used with special caution and consideration.
First, it is functionally similar to a tristate boolean (false,maybe,true) —such as
boost::tribool (not yet formally in boost)—except that in a tristate boolean,
the maybe state represents a valid value, unlike the corresponding state
of an uninitialized optional<bool>.
It should be carefully considered if an optional bool instead of a tribool is really needed
Second, optional<> provides an implicit conversion to bool. This conversion
refers to the initialization state and not to the contained value.
Using optional<bool> can lead to subtle errors due to the implicit bool conversion:
void foo ( bool v ) ; void bar() { optional<bool> v = try(); // The following intended to pass the value of 'v' to foo(): foo(v); // But instead, the initialization state is passed // due to a typo: it should have been foo(*v). }
The only implicit conversion is to bool, and it is safe in the sense that typical integral promotions don't apply (i.e. if foo() takes an 'int' instead, it won't compile).
Because of the current implementation (see Implementation Notes),
optional<T>::operator=( optional<T> const& )
and optional<T>::reset( T const& )
can only guarantee the basic exception safety: The lvalue optional is left
uninitialized if an exception is thrown (any previous value is first
destroyed using T::~T())
optional<T>::reset()
provides the no-throw guarantee (assuming a no-throw T::~T())
However, since optional<>
itself doesn't throw any exceptions,
the only source for exceptions here is T's copy constructor, so if you know the exception guarantees
for T::T ( T const& ), you know that optional's assignment and reset has the same guarantees.
// // Case 1: Exception thrown during assignment. // T v0(123); optional<T> opt0(v0); try { T v1(456); optional<T> opt1(v1); opt0 = opt1 ; // If no exception was thrown, assignment succeeded. assert( *opt0 == v1 ) ; } catch(...) { // If any exception was thrown, 'opt0' is reset to uninitialized. assert( !opt0 ) ; } // // Case 2: Exception thrown during reset(v) // T v0(123); optional<T> opt(v0); try { T v1(456); opt.reset ( v1 ) ; // If no exception was thrown, reset succeeded. assert( *opt == v1 ) ; } catch(...) { // If any exception was thrown, 'opt' is reset to uninitialized. assert( !opt ) ; }
void swap( optional<T>&, optional<T>& )
has the same exception guarantee as swap(T&,T&)
when both optionals are initialized.
If only one of the optionals is initialized, it gives the same
basic exception guarantee as optional<T>::reset( T const& )
(since optional<T>::reset()
doesn't throw).
If none of the optionals is initialized, it has no-throw guarantee since it is a no-op.
T must be Copy Constructible
and have a no-throw destructor.
T is not required to be
Default Constructible
optional<T> is currently implemented
using a custom aligned storage facility built from alignment_of
and
type_with_alignment
(both from Type Traits).
It uses a separate boolean flag to indicate the initialization state.
Placement new with T's copy constructor and T's destructor
are explicitly used to initialize,copy and destroy optional values.
As a result, T's default constructor is effectively by-passed, but the exception
guarantees are basic.
It is planned to replace the current implementation with another with
stronger exception safety, such as a future boost::variant
The implementation uses type_traits/alignment_of.hpp
and type_traits/type_with_alignment.hpp
It has been tested on bcc5.5.1, vc6.0 and gcc2.95.2
Pre-formal review:
Peter Dimov suggested the name 'optional', and was the first to point out the need for aligned storage
Douglas Gregor developed 'type_with_alignment', and later Eric Friedman coded 'aligned_storage', which are the core of the optional class implementation.
Andrei Alexandrescu and Brian Parker also worked with aligned storage techniques and their work influenced the current implementation.
Gennadiy Rozental made extensive and important comments which shaped the design.
Vesa Karvonen and Douglas Gregor made quite useful comparisons between optional, variant and any; and made other relevant comments. Douglas Gregor and Peter Dimov commented on comparisons and evaluation in boolean contexts.
Eric Friedman helped understand the issues involved with aligned storage, move/copy operations and exception safety.
Many others have participated with useful comments: Aleksey Gurotov, Kevlin Henney, David Abrahams, and others I can't recall.
Post-formal review:
William Kempf carefully considered the originally proposed interface and suggested the new interface which is currently used. He also started and fueled the discussion about the analogy optional<>/smart pointer and about relational operators.
Peter Dimov, Joel de Guzman, David Abrahams, Tanton Gibbs and Ian Hanson focused on the relational semantics of optional (originally undefined); concluding with the fact that the pointer-like interface doesn't make it a pointer so it shall have deep relational operators.
Augustus Saunders also explored the different relational semantics between optional<> and a pointer and developed the OptionalPointee concept as an aid against potential conflicts on generic code.
Joel de Guzman noticed that optional<> can be seen as an API on top of variant<T,nil_t>.
Dave Gomboc explained the meaning and usage of the Haskell analog to optional<>: the Maybe type constructor (analogy originally pointed out by David Sankel).
Other comments were posted by Vincent Finn, Anthony Williams, Ed Brey, Rob Stewart, and others.
Revised January 20, 2003
© Copyright boost.org 2003. Permission to copy, use, modify, sell and distribute this document is granted provided this copyright notice appears in all copies. This document is provided "as is" without express or implied warranty, and with no claim as to its suitability for any purpose.
Developed by Fernando Cacciola, the latest version of this file can be found at www.boost.org, and the boost discussion list at www.yahoogroups.com/list/boost.