Constructing and initializing objects in a generic way is difficult in
C++. The problem is that there are several different rules that apply
for initialization. Depending on the type, the value of a newly constructed
object can be zero-initialized (logically 0), default-constructed (using
the default constructor), or indeterminate. When writing generic code,
this problem must be addressed. value_initialized
provides
a solution with consistent syntax for value initialization of scalar,
union and class types.
The C++ standard [1] contains the definitions
of zero-initialization
and default-initialization
.
Informally, zero-initialization means that the object is given the initial
value 0 (converted to the type) and default-initialization means that
POD [2] types are zero-initialized, while class
types are initialized with their corresponding default constructors. A
declaration can contain an initializer, which specifies the
object's initial value. The initializer can be just '()', which states that
the object shall be default-initialized (but see below). However, if a declaration
has no initializer and it is of a non-const
, non-static
POD type, the initial value is indeterminate:(see §8.5 for the
accurate definitions).
int x ; // no initializer. x value is indeterminate.
std::string s ; // no initializer, s is default-constructed.
int y = int() ;
// y is initialized using copy-initialization
// but the temporary uses an empty set of parentheses as the initializer,
// so it is default-constructed.
// A default constructed POD type is zero-initialized,
// therefore, y == 0.
void foo ( std::string ) ;
foo ( std::string() ) ;
// the temporary string is default constructed
// as indicated by the initializer ()
The first Technical Corrigendum for the C++ Standard (TC1), whose draft was released to the public in November 2001, introduced Core Issue 178 (among many other issues, of course).
That issue introduced the new concept of value-initialization
(it also fixed the wording for zero-initialization). Informally, value-initialization
is similar to default-initialization with the exception that in some cases
non-static data members and base class sub-objects are also value-initialized.
The difference is that an object that is value-initialized won't have
(or at least is less likely to have) indeterminate values for data members
and base class sub-objects; unlike the case of an object default constructed.
(see Core Issue 178 for a normative description).
In order to specify value-initialization of an object we need to use the empty-set initializer: ().
(but recall that the current C++ Standard states that '()' invokes default-initialization, not value-initialization)
As before, a declaration with no intializer specifies default-initialization, and a declaration with a non-empty initializer specifies copy (=xxx) or direct (xxx) initialization.
template<class T> void eat(T);
int x ; // indeterminate initial value.
std::string s; // default-initialized.
eat ( int() ) ; // value-initialized
eat ( std::string() ) ; // value-initialied
Value initialization is specified using (). However, the empty set of parentheses is not permitted by the syntax of initializers because it is parsed as the declaration of a function taking no arguments:
int x() ; // declares function int(*)()
int y ( int() ) ; // decalares function int(*)( int(*)() )
Thus, the empty () must be put in some other initialization context.
One alternative is to use copy-initialization syntax:
int x = int() ;
This works perfectly fine for POD types. But for non-POD class types, copy-initialization searches for a suitable constructor, which could be, for instance, the copy-constructor (it also searches for a suitable conversion sequence but this doesn't apply in this context). For an arbitrary unknown type, using this syntax may not have the value-initialization effect intended because we don't know if a copy from a default constructed object is exactly the same as a default constructed object, and the compiler is allowed (in some cases), but never required to, optimize the copy away.
One possible generic solution is to use value-initialization of a non static data member:
template<class T>
struct W
{
// value-initialization of 'data' here.
W() : data() {}
T data ;
} ;
W<int> w ;
// w.data is value-initialized for any type.
This is the solution supplied by the value_initialized<> template
class.
template class value_initialized<T>
namespace boost {
template<class T>
class value_initialized
{
public :
value_initialized() : x() {}
operator T&() const { return x ; }
T& data() const { return x ; }
private :
unspecified x ;
} ;
template<class T>
T const& get ( value_initialized<T> const& x )
{
return x.data() ;
}
template<class T>
T& get ( value_initialized<T>& x )
{
return x.data() ;
}
} // namespace boost
An object of this template class is a T
-wrapper convertible
to 'T&'
whose wrapped object (data member of type T
)
is value-initialized upon default-initialization
of this wrapper class:
int zero = 0 ;
value_initialized<int> x ;
assert ( x == zero ) ;
std::string def ;
value_initialized< std::string > y ;
assert ( y == def ) ;
The purpose of this wrapper is to provide a consistent syntax for value initialization of scalar, union and class types (POD and non-POD) since the correct syntax for value initialization varies (see value-initialization syntax)
The wrapped object can be accessed either through the conversion operator
T&
, the member function data()
, or the
non-member function get()
:
void watch(int);
value_initialized<int> x;
watch(x) ; // operator T& used.
watch(x.data());
watch( get(x) ) // function get() used
Both const
and non-const
objects can be wrapped.
Mutable objects can be modified directly from within the wrapper but constant
objects cannot:
value_initialized<int> x ;
static_cast<int&>(x) = 1 ; // OK
get(x) = 1 ; // OK
value_initialized<int const> y ;
static_cast<int&>(y) = 1 ; // ERROR: cannot cast to int&
static_cast<int const&>(y) = 1 ; // ERROR: cannot modify a const value
get(y) = 1 ; // ERROR: cannot modify a const value
Both the conversion operator and the data()
member function
are const
in order to allow access to the wrapped object
from a constant wrapper:
void foo(int);
value_initialized<int> const x ;
foo(x);
But notice that this conversion operator is to T&
although
it is itself const
. As a consequence, if T
is
a non-const
type, you can modify the wrapped object even from
within a constant wrapper:
value_initialized<int> const x_c ;
int& xr = x_c ; // OK, conversion to int& available even though x_c is itself const.
xr = 2 ;
The reason for this obscure behavior is that some commonly used compilers just don't accept the following valid code:
struct X
{
operator int&() ;
operator int const&() const ;
};
X x ;
(x == 1 ) ; // ERROR HERE!
These compilers complain about ambiguity between the conversion operators.
This complaint is incorrect, but the only workaround that I know of is
to provide only one of them, which leads to the obscure behavior just explained.
The obscure behavior of being able to modify a non-const
wrapped object from within a constant wrapper can be avoided if access to
the wrapped object is always performed with the get()
idiom:
value_initialized<int> x ;
get(x) = 1 ; // OK
value_initialized<int const> cx ;
get(x) = 1 ; // ERROR: Cannot modify a const object
value_initialized<int> const x_c ;
get(x_c) = 1 ; // ERROR: Cannot modify a const object
value_initialized<int const> const cx_c ;
get(cx_c) = 1 ; // ERROR: Cannot modify a const object
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.
Revised 19 September 2002
© Copyright Fernando Cacciola, 2002.
Distributed under the Boost Software License, Version 1.0. See www.boost.org/LICENSE_1_0.txt