Various errors can occur when adding variables to complex classes and using them as arguments. This chapter shows you a simple way to avoid such errors.
Imagine that you have a class named MyClass
with several constructors. Suppose
you’ve decided to add some new data member named int_data_
to the private section of this
class:
class MyClass { public: MyClass() : int_data_(0) {} explicit MyClass(const Apple& apple) : int_data_(0) {} MyClass(const string& some_text, double weight) : int_data_(0), some_text_(some_text) {} private: int int_data_; std::string some_text_; };
When adding the new data member, you have a lot of work to
do. Every time you add a new data member of a built-in
type, do not forget to initialize it in every constructor like
this: int_data_(0)
. But wait! If you
read the Preface to this book, you probably
remember that we are not supposed to say “Every time you do A, don’t
forget to do B.” Indeed, this is an error-prone approach. If you forget to
initialize this data member, it will most likely fill with garbage that
would depend on the previous history of the computer and the application,
and will create strange and hard-to-reproduce behavior. So what should we
do to prevent such problems?
Before we answer this question, let’s first discuss why it’s only
relevant for built-in types. Let’s take a look at the data member some_text_
, which is of the type std::string
. When you add
a data member some_text_
to the class
MyClass
, you do not necessarily need to
add its initialization to every constructor of MyClass
, because if you don’t do it, the default
constructor of the std::string
will be
called for you automatically by the compiler and will initialize the
some_text_
to a reproducible state (in
this case, an empty string). But the built-in types do not have
constructors—that’s the problem. Therefore, the solution is simple: for
class data members, do not use built-in types, use classes:
and so on. The complete source code of these classes can be found in
Appendix F in the file named scpp_types.hpp. Let’s take a look. The core of
this code is the template class TNumber
:
template <typename T> class TNumber { public: TNumber(const T& x=0) : data_(x) {} operator T () const { return data_; } TNumber& operator = (const T& x) { data_ = x; return *this; } // postfix operator x++ TNumber operator ++ (int) { TNumber<T> copy(*this); ++data_; return copy; } // prefix operator ++x TNumber& operator ++ () { ++data_; return *this; } TNumber& operator += (T x) { data_ += x; return *this; } TNumber& operator -= (T x) { data_ -= x; return *this; } TNumber& operator *= (T x) { data_ *= x; return *this; } TNumber& operator /= (T x) { SCPP_TEST_ASSERT(x!=0, "Attempt to divide by 0"); data_ /= x; return *this; } T operator / (T x) { SCPP_TEST_ASSERT(x!=0, "Attempt to divide by 0"); return data_ / x; } private: T data_; };
First of all, the constructor taking type T
(where T
is
any built-in type, e.g., int
, double
, float
, etc.) is not
declared with the keyword explicit
. This is
intentional. The next function defined in the class is operator T ()
, which allows an implicit
conversion of an instance of this class back into its corresponding
built-in type. This class is intentionally designed to make it easy to
convert the built-in types into it and back. It defines several common
operators that you would expect to use with a built-in numeric
type.
And finally, here are the definitions of actual types we can use:
typedef TNumber<int> Int; typedef TNumber<unsigned> Unsigned; typedef TNumber<int64> Int64; typedef TNumber<unsigned64> Unsigned64; typedef TNumber<float> Float; typedef TNumber<double> Double; typedef TNumber<char> Char;
How do you use these new types, such as Int
and Double
, with names that
look like built-in types but start with uppercase letters? All these types
work exactly the same way as the corresponding built-in types with one
difference: they each have a default constructor, and it initializes them
to zero. As a result, in the example of the class MyClass
you can write:
class MyClass{ public: MyClass() {} explicit MyClass(const Apple& apple) {} MyClass(const string& some_text, double weight) : some_text_(some_text) {} private: Int int_data_; std::string some_text_; };
The variable int_data_
here is
declared as Int
, with an uppercase
first letter, not int
, and as a result you
don’t have to put an initialization of it in all the constructors. It will
be automatically initialized to zero.
Actually, there is one more difference: when you use built-in types, an attempt to divide by zero can lead to different consequences depending on the compiler and OS. In our case, for the sake of consistency, this runtime error will lead to a call to the same error handler function as we’ve used for other errors, so that you can debug on error (see Chapter 15).
But haven’t we forgotten one more built-in type specific to
C++— type bool
(i.e., Boolean)? No,
it is just a special case, because for a Boolean we do not need operators
such as ++
. Instead, we need
specifically Boolean operators, such as &=
and |=
, so this type is
defined separately:
class Bool { public: Bool(bool x=false) : data_(x) {} operator bool () const { return data_; } Bool& operator = (bool x) { data_ = x; return *this; } Bool& operator &= (bool x) { data_ &= x; return *this; } Bool& operator |= (bool x) { data_ |= x; return *this; } private: bool data_; }; inline std::ostream& operator << (std::ostream& os, Bool b) { if(b) os << "True"; else os << "False"; return os; }
Again, as with the other classes wrapping built-in types, the type
Bool
(uppercase) behaves
exactly like bool
(the original
built-in type), with two exceptions:
Why is it initialized to false, not to true? Maybe because the
author is a pessimist, but you can easily follow the pattern and create a
new class like BoolOptimistic
that is
initialized by default to true.
The only thing that we have yet to initialize is a pointer, which naturally should be initialized by default to NULL. We’ll deal with this later in Chapter 9.
So far, the motivation for using classes Int
, Unsigned
, Double
, etc., instead of
the corresponding lowercase built-in types was that you can skip
initialization in multiple constructors. If you use them more widely, say,
for passing arguments to the functions, here is what will to happen.
Suppose you have a function taking an unsigned
(the built-in
one):
void SomeFunctionTaking_unsigned(unsigned u);
then the following will compile:
int i = 0; SomeFunctionTaking_unsigned(i);
Not so with the classes we’ve discussed. If we have a function:
void SomeFunctionTakingUnsigned(Unsigned u);
then the following does not compile:
Int i = 0; SomeFunctionTakingUnsigned(i);
Therefore, in this case, you get additional type safety at compile time for free.
Rules for this chapter to avoid uninitialized variables, especially data members of a class:
Do not use built-in types such as int
, unsigned
, double
, bool
, etc., for class
data members. Instead, use Int
, Unsigned
, Double
, Bool
, etc., because
you will not need to initialize them in constructors.
Use these new classes instead of built-in types for passing parameters to functions, to get additional type safety.
18.116.60.158