![]() |
Robert Allan SchwartzRead my professional biography. Contact me. Go to my home page. Read my publications. Read my professional resume. See some student reviews of my teaching. |
Copyright © 1999 by Robert Allan Schwartz
[published in C/C++ Users Journal, June 1999, page 71.]
C++ guarantees that all objects are initialized when they are created. This holds true regardless of the storage class (auto, local static, global static, heap). As long as you implement your constructors properly (i.e. you initialize all the data members), then you will never again have to track down a bug due to an uninitialized object.
Unfortunately, the same can't be said for builtin types.
Variables of builtin types are initialized to 0 by default for the local static and global static storage classes, but auto and heap variables of builtin type are not initialized at all. Thus, it is still possible to have to track down a bug due to an uninitialized variable of builtin type.
Builtin types are not classes, so they do not have constructors. How can we achieve the guarantee that all variables of builtin type are initialized when they are created, regardless of the storage class, even though they do not have constructors?
We can wrap each builtin type in a wrapper class. The wrapper class will have a constructor, and that constructor initializes the builtin-typed data member.
Since we want to do the same thing for all the builtin types, we write a template class.
See figure 1 for an example of how to use the template class.
==========================
Figure 1:
class foo { private: builtin<int> b; }; void xyz(void) { // invokes builtin<float>::builtin(), // so x is initialized to 0. builtin<float> x; // the compiler-generated foo::foo() invokes builtin<int>::builtin(), // so y.b is initialized to 0. foo y; }
==========================
We had to alter the type of the data member. Can we hide that, so all users of b still think they are working with an int?
I believe we can. See figure 2 for an implementation.
==========================
Figure 2:
// builtin.h #ifndef BUILTIN_H #define BUILTIN_H #include <iostream.h> template <class T> class builtin { public: // these functions are required to be members: builtin(T new_value = 0); builtin(const builtin<T> & other); ~builtin(void); builtin<T> & operator =(const builtin<T> & other); builtin<T> & operator +=(const builtin<T> & other); builtin<T> & operator -=(const builtin<T> & other); builtin<T> & operator *=(const builtin<T> & other); builtin<T> & operator /=(const builtin<T> & other); builtin<T> & operator %=(const builtin<T> & other); builtin<T> & operator<<=(const builtin<T> & other); builtin<T> & operator>>=(const builtin<T> & other); builtin<T> & operator &=(const builtin<T> & other); builtin<T> & operator |=(const builtin<T> & other); builtin<T> & operator ^=(const builtin<T> & other); builtin<T> & operator++(void); // prefix ++. returns an lvalue. builtin<T> operator++(int); // postfix ++. returns an rvalue. builtin<T> & operator--(void); // prefix --. returns an lvalue. builtin<T> operator--(int); // postfix --. returns an rvalue. operator T(void) const; const T * operator&(void) const; T * operator&(void) ; private: T value; }; // members: template <class T> inline builtin<T>::builtin(T new_value) { value = new_value; } template <class T> inline builtin<T>::builtin(const builtin<T> & other) { value = other.value; } template <class T> inline builtin<T>::~builtin(void) { } template <class T> inline builtin<T> & builtin<T>::operator=(const builtin<T> & other) { value = other.value; return *this; } template <class T> inline builtin<T> & builtin<T>::operator+=(const builtin<T> & other) { value += other.value; return *this; } template <class T> inline builtin<T> & builtin<T>::operator-=(const builtin<T> & other) { value -= other.value; return *this; } template <class T> inline builtin<T> & builtin<T>::operator*=(const builtin<T> & other) { value *= other.value; return *this; } template <class T> inline builtin<T> & builtin<T>::operator/=(const builtin<T> & other) { value /= other.value; return *this; } template <class T> inline builtin<T> & builtin<T>::operator%=(const builtin<T> & other) { value %= other.value; return *this; } template <class T> inline builtin<T> & builtin<T>::operator<<=(const builtin<T> & other) { value <<= other.value; return *this; } template <class T> inline builtin<T> & builtin<T>::operator>>=(const builtin<T> & other) { value >>= other.value; return *this; } template <class T> inline builtin<T> & builtin<T>::operator&=(const builtin<T> & other) { value &= other.value; return *this; } template <class T> inline builtin<T> & builtin<T>::operator|=(const builtin<T> & other) { value |= other.value; return *this; } template <class T> inline builtin<T> & builtin<T>::operator^=(const builtin<T> & other) { value ^= other.value; return *this; } template <class T> inline builtin<T> & builtin<T>::operator++(void) // prefix ++. returns an lvalue. { ++value; return *this; } template <class T> inline builtin<T> builtin<T>::operator++(int) // postfix ++. returns an rvalue. { // if your compiler doesn't support this syntax: T result(value); // try this one: // T result = value; value++; return result; } template <class T> inline builtin<T> & builtin<T>::operator--(void) // prefix --. returns an lvalue. { --value; return *this; } template <class T> inline builtin<T> builtin<T>::operator--(int) // postfix --. returns an rvalue. { // if your compiler doesn't support this syntax: T result(value); // try this one: // T result = value; value--; return result; } template <class T> inline T builtin<T>::operator T(void) const { return value; } template <class T> inline const T * builtin<T>::operator&(void) const { return &value; } template <class T> inline T * builtin<T>::operator&(void) { return &value; } // non-members: // unary: template <class T> inline builtin<T> operator+(const builtin<T> & bi) { return bi; } template <class T> inline builtin<T> operator-(const builtin<T> & bi) { return -((T) bi); } template <class T> inline builtin<T> operator~(const builtin<T> & bi) { return ~((T) bi); } template <class T> inline builtin<T> operator!(const builtin<T> & bi) { return !((T) bi); } // binary: template <class T> inline istream & operator>>(istream & s, builtin<T> & bi) { T local_t; s >> local_t; bi = local_t; return s; } template <class T> inline ostream & operator<<(ostream & s, const builtin<T> & bi) { s << ((T) bi); return s; } template <class T> inline builtin<T> operator>>(const builtin<T> & bi, int i) // bit shift. { return ((T) bi) >> i; } template <class T> inline builtin<T> operator<<(const builtin<T> & bi, int i) // bit shift. { return ((T) bi) << i; } template <class T> inline bool operator==(const builtin<T> & bi1, const builtin<T> & bi2) { return ((T) bi1) == ((T) bi2); } template <class T> inline bool operator!=(const builtin<T> & bi1, const builtin<T> & bi2) { return ((T) bi1) != ((T) bi2); } template <class T> inline bool operator<(const builtin<T> & bi1, const builtin<T> & bi2) { return ((T) bi1) < ((T) bi2); } template <class T> inline bool operator<=(const builtin<T> & bi1, const builtin<T> & bi2) { return ((T) bi1) <= ((T) bi2); } template <class T> inline bool operator>(const builtin<T> & bi1, const builtin<T> & bi2) { return ((T) bi1) > ((T) bi2); } template <class T> inline bool operator>=(const builtin<T> & bi1, const builtin<T> & bi2) { return ((T) bi1) >= ((T) bi2); } template <class T> inline builtin<T> operator+(const builtin<T> & bi1, const builtin<T> & bi2) { return ((T) bi1) + ((T) bi2); } template <class T> inline builtin<T> operator-(const builtin<T> & bi1, const builtin<T> & bi2) { return ((T) bi1) - ((T) bi2); } template <class T> inline builtin<T> operator*(const builtin<T> & bi1, const builtin<T> & bi2) { return ((T) bi1) * ((T) bi2); } template <class T> inline builtin<T> operator/(const builtin<T> & bi1, const builtin<T> & bi2) { return ((T) bi1) / ((T) bi2); } template <class T> inline builtin<T> operator%(const builtin<T> & bi1, const builtin<T> & bi2) { return ((T) bi1) % ((T) bi2); } template <class T> inline builtin<T> operator&(const builtin<T> & bi1, const builtin<T> & bi2) { return ((T) bi1) & ((T) bi2); } template <class T> inline builtin<T> operator^(const builtin<T> & bi1, const builtin<T> & bi2) { return ((T) bi1) ^ ((T) bi2); } template <class T> inline builtin<T> operator|(const builtin<T> & bi1, const builtin<T> & bi2) { return ((T) bi1) | ((T) bi2); } #endif
==========================
The default constructor uses a default parameter value of 0, which is convertible to all builtin types.
The compiler-generated copy constructor, destructor, and operator=() would have been sufficient, so we did not need to define our own, but they are given here for completeness.
The conversion operator operator T() is invoked implicitly whenever a T is expected but a builtin<T> is provided.
Anyone taking the address of something they thought had type T, wants a T *, not a builtin<T> *, hence the need for the overloaded operator &()'s.
The state-changing operators (i.e. assignment, compound assignment, increment, decrement) are required.
The overloaded operators that take non-const references to builtin<T> (i.e. extractor) are required.
The overloaded operators that take const references to builtin<T> (i.e. inserter, list given below) are not necessary, because of the conversion operator operator T():
builtin<int> b1, b2, b3; b1 = b2 + b3; // becomes: b1 = (operator int(b2)) + (operator int(b3));
This means that we did not need to define the following operators, but they are given here for completeness:
unary: operator+ operator- operator~ operator! binary: operator>> (bit-shift) operator<< (bit-shift) operator<< (inserter) operator== operator!= operator< operator<= operator> operator>= operator+ operator- operator* operator/ operator% operator& operator^ operator|
The ANSI standard doesn't specify where a template class's member function definitions should be, so different compilers tend to look in different places. For portability, all the functions are inline, so we don't need a ".cpp" file. The use of inline functions also means there is no run-time cost for using the wrapper class.
It could be argued that builtin<T> is completely unnecessary, if everyone always initialized their variables of builtin type. It could also be argued that if everyone always wrote correct software, then there would be no need for debuggers.
A benefit provided by builtin<T> is that more classes will be able to use a compiler-generated constructor, so programmers will be able to write less code. See figure 3.
==========================
Figure 3:
class aaa { public: // we are forced to write the constructor, so that we can // initialize bbb. aaa(void) : bbb(0) { } private: int bbb; }; class ccc { // the compiler-generated constructor looks like this: // ccc(void) // : ddd() // which calls builtin<int>::builtin(), // which initializes ddd to 0. // { // } private: builtin<int> ddd; };
==========================
Another benefit provided by builtin<T> is that it is possible to derive from builtin<T>, though it is not possible to derive from builtin types.
Conclusion
We can guarantee that builtin-typed variables and data members are always initialized, even in classes that rely on their compiler-generated constructor, if we wrap the builtin type in a wrapper class. Doing so changes the type of the data member, but we can hide that with suitable overloaded operators. The wrapper class is a template class, so it will suffice for all the builtin types. The use of inline functions means there is no run-time cost for using the wrapper class.
Acknowledgements
A draft of this paper was posted to comp.lang.c++. Readers Siemel Naran and Jim Hyslop kindly provided valuable feedback and suggestions.