Expressivity
C++ helps a developer to write user defined types or classes that can be as expressive as the built-in types of the programming languages. This enables one to write a arbitrary-precision arithmetic class (monikered as BigInteger/BigFloat in some languages), which contains all the features of a double or float. For the sake of explanation, we have defined a SmartFloat class that wraps IEEE double precision floating point numbers and most of the operators available to the double data type is overloaded. The following code snippets show that one can write types that mimic the semantics of built-in types such as int, float, or double:
//---- SmartFloat.cpp
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
class SmartFloat {
double _value; // underlying store
public:
SmartFloat(double value) : _value(value) {}
SmartFloat() : _value(0) {}
SmartFloat( const SmartFloat& other ) { _value = other._value; }
SmartFloat& operator = ( const SmartFloat& other ) {
if ( this != &other ) { _value = other._value;}
return *this;
}
SmartFloat& operator = (double value )
{ _value = value; return *this;}
~SmartFloat(){ }
The SmartFloat class wraps a double value and has defined some constructors and assignment operators to initialize instances properly. In the following snippet, we will define some operators that help to increment the value. Both the prefix and postfix variants of operators are defined:
SmartFloat& operator ++ () { _value++; return *this; }
SmartFloat operator ++ (int) { // postfix operator
SmartFloat nu(*this); ++_value; return nu;
}
SmartFloat& operator -- () { _value--; return *this; }
SmartFloat operator -- (int) {
SmartFloat nu(*this); --_value; return nu;
}
The preceding code snippets implement increment operators (both prefix and postfix) and are meant for demonstration purposes only. In a real-world class, we will check for floating point overflow and underflow to make the code more robust. The whole purpose of wrapping a type is to write robust code!
SmartFloat& operator += ( double x ) { _value += x; return *this;}
SmartFloat& operator -= ( double x ) { _value -= x;return *this; }
SmartFloat& operator *= ( double x ) { _value *= x; return *this;}
SmartFloat& operator /= ( double x ) { _value /= x; return *this;}
The preceding code snippets implement C++ style assignment operators and once again, to make the listing short, we have not checked whether any floating point overflow or underflow is there. We do not handle exceptions as well here to keep the listing brief.
bool operator > ( const SmartFloat& other )
{ return _value > other._value; }
bool operator < ( const SmartFloat& other )
{return _value < other._value;}
bool operator == ( const SmartFloat& other )
{ return _value == other._value;}
bool operator != ( const SmartFloat& other )
{ return _value != other._value;}
bool operator >= ( const SmartFloat& other )
{ return _value >= other._value;}
bool operator <= ( const SmartFloat& other )
{ return _value <= other._value;}
The preceding code implements relational operators and most of the semantics associated with double precision floating points have been implemented as shown:
operator int () { return _value; }
operator double () { return _value;}
};
For the sake of completeness, we have implemented conversion operators to int and double. We will write two functions to aggregate values stored in an array. The first function expects an array of double as parameter and the second one expects a SmartFloat array as parameter. The code is identical in both routines and only the type changes. Both will produce the same result:
double Accumulate( double a[] , int count ){
double value = 0;
for( int i=0; i<count; ++i) { value += a[i]; }
return value;
}
double Accumulate( SmartFloat a[] , int count ){
SmartFloat value = 0;
for( int i=0; i<count; ++i) { value += a[i]; }
return value;
}
int main() {
// using C++ 1z's initializer list
double x[] = { 10.0,20.0,30,40 };
SmartFloat y[] = { 10,20.0,30,40 };
double res = Accumulate(x,4); // will call the double version
cout << res << endl;
res = Accumulate(y,4); // will call the SmartFloat version
cout << res << endl;
}
The C++ language helps us write expressive types that augment the semantics of basic types. The expressiveness of the language also helps one to write good value types and reference types using a myriad of techniques supported by the language. With support for operator overloading, conversion operators, placement new, and other related techniques, the language has taken the class design to a higher level compared to other languages of its time. But, with power comes responsibility and the language sometimes gives you enough rope to shoot yourself in the foot.