...one of the most highly
regarded and expertly designed C++ library projects in the
world.
— Herb Sutter and Andrei
Alexandrescu, C++
Coding Standards
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. The template value_initialized
provides
a solution with consistent syntax for value initialization of scalar,
union and class types.
Moreover, value_initialized
offers a workaround to various
compiler issues regarding value-initialization.
There are various ways to initialize a variable, in C++. The following declarations all may have a local variable initialized to its default value:
T1 var1; T2 var2 = 0; T3 var3 = {}; T4 var4 = T4();Unfortunately, whether or not any of those declarations correctly initialize the variable very much depends on its type. The first declaration is valid for any DefaultConstructible type (by definition). However, it does not always do an initialization! It correctly initializes the variable when it's an instance of a class, and the author of the class has provided a proper default constructor. On the other hand, the value of
var1
is indeterminate when
its type is an arithmetic type, like int
, float
, or char
.
An arithmetic variable
is of course initialized properly by the second declaration, T2
var2 = 0
. But this initialization form usually won't work for a
class type (unless the class was especially written to support being
initialized that way). The third form, T3 var3 = {}
initializes an aggregate, typically a "C-style" struct
or a "C-style" array.
However, the syntax is not allowed for a class that has an explicitly declared
constructor. (But watch out for an upcoming C++ language change,
by Bjarne Stroustrup et al [1]!)
The fourth form is the most generic form of them, as it
can be used to initialize arithmetic types, class types, aggregates, pointers, and
other types. The declaration, T4 var4 = T4()
, should be read
as follows: First a temporary object is created, by T4()
.
This object is value-initialized. Next the temporary
object is copied to the named variable, var4
. Afterwards, the temporary
is destroyed. While the copying and the destruction are likely to
be optimized away, C++ still requires the type T4
to be
CopyConstructible.
(So T4
needs to be both DefaultConstructible and CopyConstructible.)
A class may not be CopyConstructible, for example because it may have a
private and undefined copy constructor,
or because it may be derived from boost::noncopyable.
Scott Meyers [2] explains why a class would be defined like that.
There is another, less obvious disadvantage to the fourth form, T4 var4 = T4()
:
It suffers from various compiler issues, causing
a variable to be left uninitialized in some compiler specific cases.
The template value_initialized
offers a generic way to initialize
an object, like T4 var4 = T4()
, but without requiring its type
to be CopyConstructible. And it offers a workaround to those compiler issues
regarding value-initialization as well! It allows getting an initialized
variable of any type; it only requires the type to be DefaultConstructible.
A properly value-initialized object of type T
is
constructed by the following declaration:
value_initialized<T> var;
The C++ standard [3] 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 [4] types are zero-initialized, while non-POD 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 value-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, [dcl.init], 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: ().
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-initialized
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(*)()
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 as it was supplied by earlier versions of the
value_initialized<T>
template
class. Unfortunately this approach suffered from various compiler issues.
We have encountered issues regarding value-initialization on compilers by Microsoft, Sun, Borland, and GNU. Here is a list of bug reports on those issues:
Microsoft Feedback ID 100744 - Value-initialization in new-expression
Reported by Pavel Kuznetsov (MetaCommunications Engineering), 2005-07-28 GCC Bug 30111 - Value-initialization of POD base class doesn't initialize members Reported by Jonathan Wakely, 2006-12-07 GCC Bug 33916 - Default constructor fails to initialize array members Reported by Michael Elizabeth Chastain, 2007-10-26 Borland Report 51854 - Value-initialization: POD struct should be zero-initialized Reported by Niels Dekker (LKEB, Leiden University Medical Center), 2007-09-11 |
New versions of value_initialized
(Boost release version 1.35 or higher)
offer a workaround to these issues: value_initialized
will now clear
its internal data, prior to constructing the object that it contains.
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
var
of any DefaultConstructible type
T
to be value-initialized by doing T var = {}
.
The papers are listed at Bjarne's web page,
My C++ Standards committee papers value_initialized was reimplemented by Fernando Cacciola and Niels Dekker for Boost release version 1.35 (2008), offering a workaround to various compiler issues.
Developed by Fernando Cacciola, the latest version of this file can be found at www.boost.org.
Revised 15 January 2008
© Copyright Fernando Cacciola, 2002, 2008.
Distributed under the Boost Software License, Version 1.0. See www.boost.org/LICENSE_1_0.txt