There are many data types in C++; some are related to each other. Conversion between related types is implicitly supported by C++. However, between unrelated types, we need to forcefully cast them or need to write an explicit conversion function. This article focuses on some of the key aspects of automatic type conversion in C++ with examples.
Overview
Conversion between types is logical only when the types are related. In such a case, we can use an object or value of one type as an operand in place of an actual operand type that is expected. The compiler does the conversion implicitly, without any direct programmer intervention. For example, consider the following expression.
int i = 10; double d = 1.14159; int sum = d + i;
Observe that operands of the expressions are two different types: double and integer. Before adding the two values of two different types, the compiler uses a conversion function to transform the operands to a common type. Even a simple looking expression has many tacit functions associated with it.
Observe that the expression has both integer and floating-point operands; the compiler prior to addition transforms the lower type (int) to its associated higher type (floating-point) before performing the function signified by the operator (addition). Therefore, after addition the result actually becomes 11.14159. Now, because initialization happens next (with = operator), the type we are initializing to prevail over any type we want to assign and the floating-point result are re-converted back to the dominant type (which is integer in this case). Therefore, the final content of the sum is 11, discarding the fractional part as the sum variable which is declared as an integer.
The conversion between fundamental types thus is defined to do the transformation from lower type to higher type to preserve precision in the context of a mathematical expression. The conversion from double to int shall always truncate the fractional part, delimiting the precision.
Conversion
Be it explicit or implicit, conversion is always sought when the compiler sees an expression or function call using unmatched types. C++ has built-in capabilities to convert one type to another.
The compiler knows how to perform certain conversions without the programmer’s intervention. As we have seen, this is particularly true when conversion takes place among fundamental types. However, in cases where we want to perform the conversion from higher to lower fundamental types, such as double to int, we use explicit cast operators to force the conversion. But, for user-defined types such as class, there is no way the compiler can know in advance how to do the conversion, be it among user-defined types or between user-defined and fundamental types. Therefore, the programmer must explicitly specify the conversion mechanism.
Now, there are two ways automatic conversion works:
- Using the constructor as a conversion function
- Using an operator function for conversion
Using a Constructor as a Conversion Function
A non-explicit single argument constructor acts like a conversion function. The compiler uses this constructor implicitly to perform automatic type conversion when required. These are single argument constructors. The argument typically is an object or a reference of another type. It transforms object of one type, including fundamental types, to a class type.
The copy constructor which is not declared as explicit in the class definition is a potential function to be used by the compiler for implicit conversion. Because a copy constructor takes a single argument, this argument can be converted to an object of the class in which the constructor is defined. The conversion is automatic and we need not use a cast operator. However, if we want to prevent a single argument constructor from being used for type conversion by the compiler, we must declare it as explicit. This is important; otherwise, the compiler automatically will use the constructor for conversion, resulting in an ambiguous expression that generates a compilation or runtime error.
An Example
This example code shows how a single argument constructor is used for implicit conversion.
#ifndef INTARRAY_H #define INTARRAY_H #include <iostream> class IntArray { friend std::ostream& operator<<(std::ostream &, const IntArray &); friend std::istream& operator>>(std::istream &, IntArray &); public: IntArray(int = 10); IntArray(const IntArray &); ~IntArray(); size_t size() const; const IntArray&operator=(const IntArray &); bool operator==(const IntArray &) const; bool operator!=( const IntArray &right ) const { return ! ( *this == right ); } int &operator[]( int ); int operator[]( int ) const; private: size_t max; int *iptr; }; #endif // INTARRAY_H #include "intarray.h" #include <iostream> #include <iomanip> #include <stdexcept> using namespace std; IntArray::IntArray( int amax ): max( amax > 0 ? size_t(amax): throw invalid_argument( "Size cannot be <=0" ) ), iptr( new int[ max ] ) { for ( size_t i = 0; i < max; ++i ) iptr[ i ] = 0; } IntArray::IntArray( const IntArray &obj ): max(obj.max), iptr( new int[ max ] ) { for ( size_t i = 0; i < max; ++i ) iptr[ i ] = obj.iptr[ i ]; } IntArray::~IntArray() { delete[] iptr; } size_t IntArray::size() const { return max; } const IntArray &IntArray::operator=( const IntArray &obj ) { if ( &obj != this ) { if ( max != obj.max ) { delete [] iptr; max = obj.max; iptr = new int[ max ]; } for ( size_t i = 0; i < max; ++i ) iptr[ i ] = obj.iptr[ i ]; } return *this; } bool IntArray::operator==( const IntArray &obj ) const { if ( max != obj.max ) return false; for ( size_t i = 0; i < max; ++i ) if ( iptr[ i ] != obj.iptr[ i ] ) return false; return true; } int &IntArray::operator[]( int index ) { if ( index < 0 || index >= int(max) ) throw out_of_range( "Array index out of range" ); return iptr[ index ]; } int IntArray::operator[]( int index ) const { if ( index < 0 || index >= int(max) ) throw out_of_range( "Array index out of range" ); return iptr[ index ]; } istream &operator>>( istream &in, IntArray &arr ) { for ( size_t i = 0; i < arr.max; ++i ) in >> arr.iptr[ i ]; return in; } ostream &operator<<( ostream &out, const IntArray &arr ) { for ( size_t i = 0; i < arr.max; ++i ) out <<" "<< arr.iptr[ i ]; return out; } #include <iostream> #include "intarray.h" using namespace std; void print(const IntArray &obj){ cout<<"Size: "<<obj.size()<<endl <<"Elements: "<<obj; } int main() { IntArray a1(10); for(int i=0;i<int(a1.size());i++) a1[i] = std::rand()%100; print(a1); cout<<endl; // Converts the value to an object with implicit conversion print(4); return 0; }
Conversion Operators
The conversion operators or the cast operators also are used to convert a object of one class type to another type. This is another way to produce automatic type conversion. We can overload the operator as a non-static member function and convert the object into the type desired. This is unique, because although here we do not specify a return type, the name of the operator we are overloading signifies the return type. In contrast to the constructor conversion where the destination class performs the conversion, here the source class does it. For example, the function prototype
MyClass::operator int *() const;
signifies that a overloaded cast operator function of class MyClass converts an object into a temporary int * object. The const ensures that it should not be able to modify the original object. As mentioned earlier, the returned type is implicit. Therefore, if we write something like static_cast<int *>(obj), the compiler automatically calls it as obj.operator int *() to convert the operand obj to int *.
We can convert a user-defined types to fundamental types or vice-versa. The function prototypes are as follows:
MyClass::operator char() const; MyClass::operator YourClass() const;
A Quick Example
#include <iostream> #include "intarray.h" using namespace std; class A { int infoA; public: A(int a=0, int=0):infoA(a){} }; class B { int infoB; public: B(int b):infoB(b){} operator A() const {return A(infoB);} }; void func(A){} int main(){ B bb(10); func(bb); // Converts B object to A object func(20); // Converts the value into object return 0; }
Conclusion
The important thing about automatic conversion is that the compiler calls the conversion function implicitly, without any programmer intervention. This not only leverages the readability of the code but also makes the code visually simple. Note that a single argument constructor (also if there are more than one argument but defaulted) is always treated for automatic type conversion. The only way to prevent this is by using the explicit keyword. The limitation of constructor conversion is that it cannot be used to convert a user-defined type to a built-in type. This is, however, only possible with operator overloading.