Mapping database tables to C++ classes is a common challenge. Let us try to map a database table person to a class.
using namespace boost::gregorian;
using namespace std::string;
class person {
public:
string first_name;
string last_name;
date date_of_birth;
int height;
float weight;
};
Under perfect conditions this would work just fine. But in practice when rolling out our database layer (with elements of RDBMS-2-OO) several problems arise. One of them is handing null values.
In C# we would use (...and strongly typed datasets do) nullable types.
public class Person {
public:
string FirstName;
string LastName;
date DateOfBirth;
int? Height; // Nullable type.
float? Weight; // Nullable type.
};
However there is no similar language construct in C++. So we will have to create it. Our todays' goal is to add built in value types the ability to be null. And to be able to detect when they are null.
We will create new types but we desire -just like in C#- that they behave like existing built in types.
So why not deriving from built in types?
class int_ex : int {
}
I've been thinking long and hard about the appropriate answer to this good question. And finally I came up with the perfect argument why this is a bad idea. It's because it won't compile.
Luckily we have templates. Let us start by declaring our class having a simple copy constructor.
template <typename T>
class nullable {
private:
T _value;
public:
// Copy constructor.
nullable<T>(T value) {
_value=value;
}
};
With this code we have wrapped built in value type into a template. Let us explore possibilities of this new type.
int n=10;
nullable<int> i=20; // This actually works!
nullable<int> j=n; // This too.
nullable<int> m; // This fails because we have no default constructor.
This is a good start. Since we have not defined a default constructor and we have defined a copy constructor the compiler hasn't generated the default constructor for us. Thus last line won't compile. Adding the default constructor will fix this. The behavior of the ctor will be to make value type equal to null. For this we will also add _is_null member to our class. Last but not least we will add assignment operator to the class.
template <typename T>
class nullable {
private:
T _value;
bool _is_null;
public:
// Copy ctor.
nullable<T>(T value) {
_value=value;
_is_null=false;
}
// Default ctor.
nullable<T>() {
_is_null=true;
}
// Assignment operator =
nullable<T>& operator=(const nullable<T>& that) {
if (that._is_null) // Make sure null value stays null.
_is_null=true;
else {
_value=that._value;
_is_null=false;
}
return *this;
}
};
Now we can do even more things to our class.
int n=10;
nullable<int> i=20; // Works. Uses copy constructor.
nullable<int> j; // Works. Uses default ctor. j is null.
j=n; // Works. Uses copy constructor + assignment operator.
j=i; // Works. Uses assignment operator.
Next we would like to add the ability to assign a special value null to variables of our nullable type. We will use a trick to achieve this. We will first create a special type to differentiate nulls' type from all other types. Then we will add another copy constructor further specializing template the null type.
class nulltype {
};
static nulltype null;
template <typename T>
class nullable {
private:
T _value;
bool _is_null;
public:
// Assigning null.
nullable<T>(nulltype& dummy) {
_is_null=true;
}
// Copy ctor.
nullable<T>(T value) {
_value=value;
_is_null=false;
}
// Default ctor.
nullable<T>() {
_is_null=true;
}
// Assignment operator =
nullable<T>& operator=(const nullable<T>& that) {
if (that._is_null) // Make sure null value stays null.
_is_null=true;
else {
_value=that._value;
_is_null=false;
}
return *this;
}
};
Let us further torture our new type.
nullable<int>m=100; // Works! m=100!
nullable<int>j; // Works! j is null.
nullable<int>n=j=m; // Works! All values are 100.
nullable<int>i=null; // Works! i is null.
Our type is starting to look like a built in type. However we still can't use it in expressions to replace normal value type. So let's do something really dirty. Let us add a cast operator into type provided as template argument.
// Cast operator.
operator T() {
if (!_is_null)
return _val;
}
Automatic casts are nice and they work under perfect conditions. But what if the value of nullable type is null? We'll test for the null condition in our cast operator and throw a null_exception.
Throwing an exception is an act of brutality. We would like to allow user to gracefully test our type for null value without throwing the exception. For this we will also add is_null() function to our class.
class null_exception : public std::exception {
};
class null_type {
};
static null_type null;
template <typename T>
class nullable {
private:
T _value;
bool _is_null;
public:
// Assigning null.
nullable<T>(null_type& dummy) {
_is_null=true;
}
// Copy ctor.
nullable<T>(T value) {
_value=value;
_is_null=false;
}
// Default ctor.
nullable<T>() {
_is_null=true;
}
// Assignment operator =
nullable<T>& operator=(const nullable<T>& that) {
if (that._is_null) // Make sure null value stays null.
_is_null=true;
else {
_value=that._value;
_is_null=false;
}
return *this;
}
// Cast operator.
operator T() {
if (!_is_null)
return _value;
else
throw (new null_exception);
}
// Test value for null.
bool is_null() {
return _is_null;
}
};
We're almost there. Now the following code will now work with new nullable type.
nullable<int> i; // i is null
nullable<int> j=10;
i=2*j; // i and j behave like integer types.
j=null; // you can assign null to nullable type
if (j.is_null()) { // you can check j
int n=j+1; // Will throw null_exception because j is null
}
There are still situations in which nullable types do not act or behave like the built in value types.
// This will fail...
for(nullable<int> i=0; i<10;i++) {
}
To fix this we need to implement our own ++ operator.
// Operator ++ and --
nullable<T>& operator++() {
if (!_is_null) {
_value++;
return (*this);
} else
throw (new null_exception);
}
nullable<T> operator++(int) {
if (!_is_null) {
nullable<T> temp=*this;
++(*this);
return temp;
} else
throw (new null_exception);
}
This is it. We have developed this type to a point where it serves our purpose. To enable us to write database layer.
using namespace boost::gregorian;
using namespace std::string;
class person {
public:
string first_name;
string last_name;
date date_of_birth;
nullable<int> height;
nullable<float> weight;
};
There's still work to be done. Implementing remaining operators (such as ==). Finding new flaws and differences between our type and built in types. I leave all that to you, dear reader. If you extend the class please share your extensions in the comments section for others to use. Thank you.
Categories c++ , compilation , patterns
Subscribe to:
Post Comments
(
Atom
)
Is there a way to extend this nullable to string data types?
Anonymous said...
9:05 AM
String data type and other object types are by default nullable (you can assign null to them). To make it consistent - we could add apropriate cast to the nulltype class. Or we could specialize template "nullable" with string and in it override default behaviour.
Tomaž Štih said...
10:44 PM