Supporting Custom C++ Types

Traits are a powerful technique that lets you create arbitrary user-defined types, among other things.


June 01, 2006
URL:http://www.drdobbs.com/cpp/logging-in-c/cpp/supporting-custom-c-types/188700800

Traits are powerful techniques with many applications to generic programming in C++. In this article, I use examples from the SOCI database library (soci.sf.net) to show how a simple TypeConversion traits class can be used to create an extensible framework that supports arbitrary user-defined types. The complete source code that implements this framework is available here.

SOCI is a database library with a minimalist syntax that mimics that of embedded SQL. (For more information, see "A Simple Oracle Call Interface," by Maciej Sobczak.) Listing One is a complete program that connects to a database (Oracle, in this case), creates a table, inserts a row, and queries the table.

Listing One
#include <soci.h>
#include <iostream>
#include <string>

using namespace SOCI;
using std::string;

int main()
{
    try
    {
        Session sql("oracle", "service=gen1 user=scott " "password=tiger");
        sql << "create table Person(id number, name varchar2(50))";
        int id(100);
        string name("Bjarne");
        sql << "insert into Person values (:ID, :NAME)", use(id), use(name);
        int id2;
        string name2;
        sql << "select id, name from Person", into(id2), into(name2);
        assert(id2 == 100);
        assert(name2 == "Bjarne");
        std::cout << name2 << " has id "<< id2 << std::endl;
        sql << "drop table person";
    }
    catch(std::exception& e)
    {
        std::cout<<e.what()<<std::endl;
    }
}

To use SOCI to insert (or update) data, you first declare and initialize variables of a type appropriate to the column into which you will be inserting. Then use the free function use() to bind the variables to placeholders in your SQL statement.

Similarly, to use SOCI to select data, you define variables of an appropriate type and pass them by reference to the free function into() to have them populated with the values returned by your select statement.

SOCI Implementation

SOCI supports native types such as int and std::string as parameters for use() and into() functions via template specialization. The framework internally makes use of an inheritance hierarchy, which is by design hidden from users.

The use() function in fact creates an instance of the class template UseType, which is specialized for each supported native type. The class UseType is derived from StandardUseType, which in turn is derived from the abstract base class UseTypeBase.

The into() function works similarly, creating instances of IntoType, which is also specialized for each built-in type. Class IntoType is derived from StandardIntoType, which in turn is derived from the abstract base class IntoTypeBase. Figure 1 presents the relevant UML class diagrams, and Listing Two implements the simplified class definitions.


Figure 1: SOCI internal class hierarchy.

Listing Two
 
class UseTypeBase
{
public:
    virtual void bind(Statement &st, int &position) = 0;
    virtual void preUse() = 0;
    virtual void postUse(bool gotData) = 0;
    virtual void cleanUp() = 0;
};

class StandardUseType : public UseTypeBase
{
public:
    StandardUseType(void *data, eExchangeType type,
                    std::string const &name = std::string());

    ~StandardUseType();
    virtual void bind(Statement &st, int &position);

private:
    virtual void preUse();
    virtual void postUse(bool gotData);
    virtual void cleanUp();

    void *data_;
    eExchangeType type_;
    eIndicator *ind_;
    std::string name_;
    StandardUseTypeBackEnd *backEnd_; //database specific
};

template <>
class UseType<int> : public StandardUseType
{
public:
    UseType(int &i, std::string const &name = std::string())
        : StandardUseType(&i, eXInteger, name) {}
};

template <>
class UseType<std::string> : public StandardUseType
{
public:
    UseType(std::string &s, std::string const &name = std::string())
        : StandardUseType(&s, eXStdString, name) {}
};

template <typename T> details::UseTypePtr use(T &t)
{
    return UseTypePtr(new UseType<T>(t));
}

// similar definitions for UseType<double> etc. not shown

class IntoTypeBase
{
public:
    virtual void define(Statement &st, int &position) = 0;
    virtual void preFetch() = 0;
    virtual void postFetch(bool gotData, bool calledFromFetch) = 0;
    virtual void cleanUp() = 0;
};

class StandardIntoType : public IntoTypeBase
{
public:
    StandardIntoType(void *data, eExchangeType type);
    virtual ~StandardIntoType();

private:
    virtual void define(Statement &st, int &position);
    virtual void preFetch();
    virtual void postFetch(bool gotData, bool calledFromFetch);
    virtual void cleanUp();

    void *data_;
    eExchangeType type_;
    StandardIntoTypeBackEnd *backEnd_;
};

template <> class IntoType<int> : public StandardIntoType
{
public:
    IntoType(int &i) : StandardIntoType(&i, eXInteger) {}
};

template <> class IntoType<std::string> : public StandardIntoType
{
public:
    IntoType(std::string &s) : StandardIntoType(&s, eXStdString) {}
};

template <>
class IntoType<std::tm> : public StandardIntoType
{
public:
    IntoType(std::tm &t) : StandardIntoType(&t, eXStdTm) {}
};

template <typename T> IntoTypePtr into(T &t)
{
    return IntoTypePtr(new IntoType<T>(t));
}

// similar definitions for IntoType<double> etc. not shown

The Problem: Supporting Additional Types

SOCI provides explicit template specializations of UseType and IntoType for the Standard C++ types short, int, char, unsigned long, double, char*, std::string, and std::tm.

Of course, in practice, users will often prefer to work with other types, such as nonstandard String, Money, or Date classes. For example, if your development team has adopted the Boost libraries, you might like to use boost::gregorian::date ( which can be found at boost.org) directly with SOCI, as in Listing Three.

Listing Three
 

// Requires support for boost::gregorian::date to be added, either
// manually as in Listing Four, or via TypeConversion<T> as in Listing Six.
#include "listing6.h"
#include <soci.h>
#include <boost/date_time/gregorian/gregorian.hpp>
#include <iostream>

using namespace SOCI;
using boost::gregorian::date;
using boost::gregorian::months_of_year;

int main()
{
    try
    {
        Session sql("oracle", "service=gen1 user=scott " "password=tiger");
        sql << "create table person(id number, name varchar2(50),"
                 " birthday date)";
        int id(100);
        std::string name("Bjarne");
        date bday(2001, boost::gregorian::Jan, 1);
        sql << "insert into person values(:id, :name, :birthday)",
               use(id), use(name), use(bday);
        std::tm bd;
        sql<<"select birthday from person", into(bd);
        date bday2(boost::gregorian::not_a_date_time);
        sql << "select birthday from person", into(bday2);
        assert(bday2 == bday);
        std::cout<<name<<"'s birthday is "<<bday2<<std::endl;        

        sql << "drop table person";
    }
    catch(std::exception& e)
    {
        std::cout<<e.what()<<std::endl;
    }
}
#ifndef LISTING6_H
#define LISTING6_H

SOCI could of course be made to support boost::gregorian::date "out of the box," but that wouldn't solve the larger problem. One of the realities of C++ is that, for better or worse, there are tens—if not hundreds—of String, Numeric, and Date classes in widespread use.

Since the SOCI implementation is driven by template specialization, there is a natural way you could add support for your preferred custom type. For example, to add support for boost::gregorian::date, you could provide explicit specializations for UseType<date> and IntoType<date>, as in Listing Four.

Listing Four
#include <boost/date_time/gregorian/gregorian.hpp>
using boost::gregorian::months_of_year;
using boost::gregorian::date;

template <> class IntoType<date> : public IntoType<std::tm>
{
public:
    IntoType(std::time_t &t) : IntoType<std::tm>(tm_), t_(t) {}

    virtual void postFetch(bool gotData, bool calledFromFetch)
    {
        IntoType<std::tm>::postFetch(gotData, calledFromFetch);
        if (gotData)
        {
            std::tm* t = static_cast<std::tm*>(data_);
	        value_ = date(t->tm_year + 1900, 
			  static_cast<months_of_year>(t.tm_mon + 1),
			  t->tm_mday);	
        }
    }
private:
    date& value_;
};

template <>
class UseType<std::time_t> : public UseType<std::tm>
{
public:
    UseType(std::time_t &t, std::string const &name = std::string())
        : UseType<std::tm>(tm_, name), t_(t) {}

    virtual void preUse()
    {
        std::tm* t = static_cast<std::tm*>(data_);
        t->tm_isdst = -1;
        t->tm_year = value_.year() - 1900;
        t->tm_mon = value_.month() - 1;
        t->tm_mday = value_.day();
        t->tm_hour = 0;
        t->tm_min = 0;
        t->tm_sec = 0;
        std::mktime(t); 

	UseType<std::tm>::preUse();
    }

    virtual void postUse()
    {
	UseType<std::tm>::postUse();

	std::tm* t = static_cast<std::tm*>(data_);
	value_ = date(t->tm_year + 1900, 
		      static_cast<months_of_year>(t->tm_mon + 1),
		      t->tm_mday);
    }

private:
    date& value_;
};

Figure 2 presents the corresponding UML class diagram.


Figure 2: Manually adding support for custom types.

In these example specializations, I've chosen to leverage the existing database access support for std::tm by having UseType<date> inherit from UseType<std::tm> and IntoType<date> inherit from IntoType<std::tm>.

A member variable value_ of type date holds a reference to the parameter, which is passed to into() and use(). In the case of IntoType<date>, the value_ data member is populated inside postFetch(), which is called by the framework after the database-related code is done executing.

Similarly, for UseType<date>, the value_ member is copied into StandardIntoType::data_ in preUse() before any database-related code executes. Because the parameter passed to the use() function can also be written to when used in conjunction with stored procedures, the value_ data member is also populated inside postUse();.

Traits, TypeConversion<T>, & Generalized Support for New Types

Adding support for new types in the manner just described is effective, but it requires you to have significant knowledge of the relevant library internals. After studying code that used the method in Listing Four, I realized that the implementation technique could be generalized. I therefore made several enhancements to the SOCI library to make defining new types simpler for library users. The key to the enhancements is a traits class called TypeConversion.

Defined in terms of its structure, a traits class is simply a class template, which contains only typedefs and static functions, and has no modifiable state or virtuals (see C++ Coding Standards by Herb Sutter and Andrei Alexandrescu, Addison-Wesley, 2004).

Nathan Meyers, who first introduced the traits concept, defines a traits class as: "A class used in place of template parameters. As a class, it aggregates useful types and constants; as a template, it provides an avenue for that 'extra level of indirection' that solves all software problems" (www.cantrip.org/traits.html).

Andrei Alexandrescu has written that traits are intended to "consolidate pieces of code that, depending upon a type...sport slight variations in terms of structure and/or behavior. To achieve this end, traits rely on explicit template specialization" (erdani.org/publications/traits.html).

There are three required class members for a valid TypeConversion trait class specialization:

Listing Five shows the relevant changes to the SOCI framework.

Listing Five
class StandardIntoType : public IntoTypeBase
{
public:
    StandardIntoType(void *data, eExchangeType type);
    virtual ~StandardIntoType();

private:
    virtual void define(Statement &st, int &position);
    virtual void preFetch();
    virtual void postFetch(bool gotData, bool calledFromFetch);
    virtual void cleanUp();

    // conversion hook (from base type to arbitrary user type)
    virtual void convertFrom() {}

    void *data_;
    eExchangeType type_;
    StandardIntoTypeBackEnd *backEnd_;
};

class StandardUseType : public UseTypeBase
{
public:
    StandardUseType(void *data, eExchangeType type,
        std::string const &name = std::string());

    virtual ~StandardUseType();
    virtual void bind(Statement &st, int &position);

private:
    virtual void preUse();
    virtual void postUse(bool gotData);
    virtual void cleanUp();

    // conversion hooks (from arbitrary user type to base type)
    virtual void convertTo() {}
    virtual void convertFrom() {}

    void *data_;
    eExchangeType type_;
    std::string name_;

    details::StandardUseTypeBackEnd *backEnd_;
};

struct BaseValueHolder
{
    typename TypeConversion<T>::base_type val_;
};

// Automatically create an IntoType from a TypeConversion
template <typename T> class IntoType
    : private BaseValueHolder<T>,
      public IntoType<typename TypeConversion<T>::base_type>
{
public:
    typedef typename TypeConversion<T>::base_type BASE_TYPE;

    IntoType(T &value)
        : IntoType<BASE_TYPE>(BaseValueHolder<T>::val_),
          value_(value) {}

private:
    void convertFrom()
    {
        value_ = TypeConversion<T>::from(BaseValueHolder<T>::val_);
    }

    T &value_;
};

// Automatically create a UseType from a TypeConversion
template <typename T>
class UseType
    : private details::BaseValueHolder<T>,
      public UseType<typename TypeConversion<T>::base_type>
{
public:
    typedef typename TypeConversion<T>::base_type BASE_TYPE;

    UseType(T &value)
        : UseType<BASE_TYPE>(BaseValueHolder<T>::val_),
          value_(value) {}

private:
    void convertFrom()
    {
        value_ = TypeConversion<T>::from(BaseValueHolder<T>::val_);
    }
    void convertTo()
    {
        details::BaseValueHolder<T>::val_ = 
		TypeConversion<T>::to(value_);
    }

    T &value_;
};

The first step to add support for TypeConversion within the framework was to use the Template Method pattern (www.ddj.com/dept/cpp/184401436) by adding private virtual functions convertFrom() and convertTo() to classes StandardIntoType and StandardUseType. These new member functions are called from StandardUseType::preUse(),StandardUseType::post Use(), and StandardIntoType::postFetch().

I used the Template Method pattern here to isolate the code, which is unique to UseType and IntoType subclasses inside the convertFrom() and convertTo() private member functions. This simplifies the implementation of subclasses because they no longer need to include calls to their corresponding base class methods.

The next step was to partially specialize classes IntoType and UseType so that they inherit from IntoType<TypeConversion<T>::base_type> and UseType<TypeConversion<T>:: base_type>, respectively. These new classes also override convertTo() and convertFrom() and call TypeConversion <T>::to() and TypeConversion<T>::from().

An additional implementation detail is that these new IntoType and UseType specializations also inherit from the BaseValueHolder struct, which is used to hold the val_ member of type TypeConversion<T>::base_type. This ensures the correct order of initialization for the relevant member variables.

With the framework enhanced in this way, if you want to add SOCI support for boost::gregorian::date, you need only provide the appropriate TypeConversion specialization. IntoType<date> and UseType<date> are automatically generated by the compiler.

Compare this new method for adding custom types (Listing Six) to the old method (Listing Four). The obvious advantages are simplicity and separation of responsibilities. With the new method, you are not burdened with knowing the details of the SOCI implementation. The code that you need to provide to add support for a new type is isolated to the TypeConversion class template.

Listing Six

#include <iostream>
#include <soci.h>
#include <boost/date_time/gregorian/gregorian.hpp>

using boost::gregorian::months_of_year;
using boost::gregorian::date;

namespace SOCI
{
template<> struct TypeConversion<date>
{
    typedef std::tm base_type;
    static date from(std::tm& t)
    {
        date d( t.tm_year + 1900, 
             static_cast<months_of_year>(t.tm_mon + 1), t.tm_mday );
        return d;
    }
    static std::tm to(date& d)
    {
        std::tm t;
        t.tm_isdst = -1;
        t.tm_year = d.year() - 1900;
        t.tm_mon = d.month() - 1;
        t.tm_mday = d.day();
        t.tm_hour = 0;
        t.tm_min = 0;
        t.tm_sec = 0;
        std::mktime(&t); 
        return t;
    }
};
};
#endif

Pass-Through Version of TypeConversion

SOCI also provides a Row class and a ColumnProperties class, which you can use together as a means for selecting data from columns that are not known at compile time. For example, the code in Listing Seven selects data into a Row and creates an XML document from it.

Listing Seven
#include "listing6.h"

#include <soci.h>
#include <boost/date_time/gregorian/gregorian.hpp>

using namespace SOCI;
using boost::gregorian::date;

int main()
{
    try
    {
        Session sql("oracle", "service=gen1 user=scott"
                " password=tiger");

        sql << "create table person(id number, name varchar2(20),"
               " birthday date)";

        int id = 100;
        std::string name("Bjarne");
        date d(2001, boost::gregorian::Jan, 1);

        sql << "insert into person values(:id, :name, :bday)",
           use(id), use(name), use(d);

        Row r;
        sql << "select * from person", into(r);

        std::ostringstream doc;
        doc << "<ROW>" << std::endl;
        for(size_t i=0; i<r.size(); ++i)
        {
            const ColumnProperties& props = r.getProperties(i);
            doc << '<' << props.getName() << '>';
            switch(props.getDataType())
            {
            case eString:
                doc << r.get<std::string>(i);
                break;
            case eDouble:
                doc << r.get<double>(i);
                break;
            case eInteger:
                doc << r.get<int>(i);
                break;
            case eDate:
                doc << r.get<date>(i);
                break;
            default:
                throw std::runtime_error("unknown column type");
            }
            doc << "</" << props.getName() << '>' << std::endl;  
        }
        doc << "</ROW>";
        std::cout<<doc.str()<<std::endl;

        sql << "drop table person";
    }
    catch(std::exception& e)
    {
        std::cout<<e.what()<<std::endl;
    }
}

First, the select statement is executed, passing an instance of Row by reference to the into() function. Then the Row's ColumnProperties are used to determine the name and data type of each column. The data for each column is retrieved from the Row by calling Row::get<T>(), where T is based on the data type of the column.

To enable Row::get<T>() to work with both custom types and native types, SOCI provides a default definition of TypeConversion<T>, where the base_type typedef is also T; see Listing Eight.

Listing Eight
namespace SOCI
{
template<typename T> struct TypeConversion
{
    typedef T base_type;
    static T from(T const &t) { return t; }
    // pass-through version of to() is not needed by SOCI
};
};

The definition of Row::get<T>() calls TypeConversion<T>::from(). When you call Row::get<T>() where T is a type for which TypeConversion is specialized (such as boost::date::gregorian in the previous examples), the expected conversion takes place.

On the other hand, if you call Row::get<T>() where T is a type that is natively supported by SOCI (such as std::string or int), the compiler doesn't find a specialization of TypeConversion—instead it falls back to the nonspecialized definition, which simply acts as a pass through.

Aggregate Types and Object-Relational Mapping

The potential custom types I have discussed so far all map directly to fundamental database types. For example, no matter what your preferred String class is, you would add support for it by defining a TypeConversion<> specialization using the base_type std::string. SOCI in turn uses it to read/write to a single character-based column in the database.

In fact, you can also provide a specialization of the TypeConversion traits class to support custom aggregate types—SOCI provides a class called Values specifically to facilitate this. The Values class acts as an intermediary between your custom aggregate class and the database. It provides get() and set() methods that allow you to define how your class maps to columns in the database. Listing Nine is a mapping of a Person struct to its database columns.

Listing Nine
#include "listing6.h"

#include <soci.h>
#include <boost/date_time/gregorian/gregorian.hpp>
using boost::gregorian::date;

struct Person
{
    int id;
    std::string name;
    date birthday;
};

namespace SOCI
{
template<> struct TypeConversion<Person>
{
    typedef Values base_type;
    static Person from(Values const &v)
    {
        Person p;
        p.id = v.get<int>("ID");
        p.name = v.get<std::string>("NAME");
        p.birthday = v.get<date>("BIRTHDAY");
        return p;
    }
    static Values to(Person &p)
    {
        Values v;
        v.set("ID", p.id);
        v.set("NAME", p.name);
        v.set("BIRTHDAY", p.birthday);
        return v;
    }
};
};

TypeConversion-based mappings to the Values class are simple to define, yet they provide a powerful and flexible means of abstraction. In fact, they provide a way to implement object-relational mapping, which is an area of much interest in the world of database programming using object-oriented languages (see Martin Fowler's Patterns of Enterprise Application Architecture; Addison-Wesley, 2002).

Listing Ten is a simple database access class that makes use of the TypeConversion<Person> specialization. The PersonDBStorage class provides methods GetPersonByName(), AddPerson(), and UpdatePerson(). As you can see, its implementation is simplified by the use of the TypeConversion-based object-relational mapping. If your class has more than a few data members, the simplicity gain from using such a mapping will be more pronounced.

Listing Ten
#include "listing6.h"
#include "listing9.h"

#include <soci.h>
#include <boost/date_time/gregorian/gregorian.hpp>

using namespace SOCI;
using boost::gregorian::date;

class PersonDBStorage
{
public:
    PersonDBStorage(SOCI::Session& sql) : mSql(sql) {}
    
    virtual Person getPersonByName(std::string& name)
    {
        Person p;
        mSql << "select * from Person where name = :NAME", use(name),
	        into(p);
	return p;
    }
	
    virtual void addPerson(Person& p)
    {
        mSql << "insert into Person values(:ID, :NAME, :BIRTHDAY)",
	        use(p);
    }

    virtual void updatePerson(Person& p)
    {
        mSql << "update Person set name = :NAME, " 
		"birthday = :BIRTHDAY where id = :ID", use(p);
    }
private:
    Session& mSql;
};

int main()
{
    try
    {
        Session sql("oracle", "service=gen1 user=scott"
                    " password=tiger");

        sql << "create table person(id number(5), name varchar2(20),"
               " birthday date)";

        PersonDBStorage storage(sql);

        Person p;
        p.id = 100;
        p.name = "Bjarne";
        p.birthday = date(2001, boost::gregorian::Jan, 1);
        storage.addPerson(p);

        Person p2 = storage.getPersonByName(p.name);
        assert(p2.birthday == p.birthday);
        std::cout << p2.name << "'s birthday is " << p2.birthday 
		  <<std::endl;

        sql << "drop table person";
    }
    catch(std::exception& e)
    {
        std::cout << e.what() << std::endl;
    }
}

Conclusion

Supporting user customization via TypeConversion<T> is viable and potentially beneficial for any library that is driven by template specialization. The benefit is that it gives library users a succinct and noninvasive mechanism for making the library work transparently with their preferred user-defined types.

If you plan to implement TypeConversion support within your library, one technique to consider is to utilize a partial class template specialization for type TypeConversion<T>::base_type, which derives from the class template specialization for type T.

Acknowledgments

Thanks to Maciej Sobczak, founder of the SOCI project, for suggesting the use of the BaseValueHolder class in Listing Five.


Stephen is a Senior Software Engineer for Factor 5. He can be reached at www.featurecomplete.com.

Traits, TypeConversion<T>, & Generalized Support for New Types

Adding support for new types in the manner just described is effective, but it requires you to have significant knowledge of the relevant library internals. After studying code that used the method in Listing Four, I realized that the implementation technique could be generalized. I therefore made several enhancements to the SOCI library to make defining new types simpler for library users. The key to the enhancements is a traits class called TypeConversion.

Defined in terms of its structure, a traits class is simply a class template, which contains only typedefs and static functions, and has no modifiable state or virtuals (see C++ Coding Standards by Herb Sutter and Andrei Alexandrescu, Addison-Wesley, 2004).

Nathan Meyers, who first introduced the traits concept, defines a traits class as: "A class used in place of template parameters. As a class, it aggregates useful types and constants; as a template, it provides an avenue for that 'extra level of indirection' that solves all software problems" (www.cantrip.org/traits.html).

Andrei Alexandrescu has written that traits are intended to "consolidate pieces of code that, depending upon a type...sport slight variations in terms of structure and/or behavior. To achieve this end, traits rely on explicit template specialization" (erdani.org/publications/traits.html).

There are three required class members for a valid TypeConversion trait class specialization:

Listing Five shows the relevant changes to the SOCI framework.

class StandardIntoType : public IntoTypeBase
{
public:
    StandardIntoType(void *data, eExchangeType type);
    virtual ~StandardIntoType();

private:
    virtual void define(Statement &st, int &position);
    virtual void preFetch();
    virtual void postFetch(bool gotData, bool calledFromFetch);
    virtual void cleanUp();

    // conversion hook (from base type to arbitrary user type)
    virtual void convertFrom() {}

    void *data_;
    eExchangeType type_;
    StandardIntoTypeBackEnd *backEnd_;
};

class StandardUseType : public UseTypeBase
{
public:
    StandardUseType(void *data, eExchangeType type,
        std::string const &name = std::string());

    virtual ~StandardUseType();
    virtual void bind(Statement &st, int &position);

private:
    virtual void preUse();
    virtual void postUse(bool gotData);
    virtual void cleanUp();

    // conversion hooks (from arbitrary user type to base type)
    virtual void convertTo() {}
    virtual void convertFrom() {}

    void *data_;
    eExchangeType type_;
    std::string name_;

    details::StandardUseTypeBackEnd *backEnd_;
};

struct BaseValueHolder
{
    typename TypeConversion<T>::base_type val_;
};

// Automatically create an IntoType from a TypeConversion
template <typename T> class IntoType
    : private BaseValueHolder<T>,
      public IntoType<typename TypeConversion<T>::base_type>
{
public:
    typedef typename TypeConversion<T>::base_type BASE_TYPE;

    IntoType(T &value)
        : IntoType<BASE_TYPE>(BaseValueHolder<T>::val_),
          value_(value) {}

private:
    void convertFrom()
    {
        value_ = TypeConversion<T>::from(BaseValueHolder<T>::val_);
    }

    T &value_;
};

// Automatically create a UseType from a TypeConversion
template <typename T>
class UseType
    : private details::BaseValueHolder<T>,
      public UseType<typename TypeConversion<T>::base_type>
{
public:
    typedef typename TypeConversion<T>::base_type BASE_TYPE;

    UseType(T &value)
        : UseType<BASE_TYPE>(BaseValueHolder<T>::val_),
          value_(value) {}

private:
    void convertFrom()
    {
        value_ = TypeConversion<T>::from(BaseValueHolder<T>::val_);
    }
    void convertTo()
    {
        details::BaseValueHolder<T>::val_ = 
		TypeConversion<T>::to(value_);
    }

    T &value_;
};
Listing Five

The first step to add support for TypeConversion within the framework was to use the Template Method pattern (www.ddj.com/dept/cpp/184401436) by adding private virtual functions convertFrom() and convertTo() to classes StandardIntoType and StandardUseType. These new member functions are called from StandardUseType::preUse(),StandardUseType::post Use(), and StandardIntoType::postFetch().

I used the Template Method pattern here to isolate the code, which is unique to UseType and IntoType subclasses inside the convertFrom() and convertTo() private member functions. This simplifies the implementation of subclasses because they no longer need to include calls to their corresponding base class methods.

The next step was to partially specialize classes IntoType and UseType so that they inherit from IntoType<TypeConversion<T>::base_type> and UseType<TypeConversion<T>:: base_type>, respectively. These new classes also override convertTo() and convertFrom() and call TypeConversion <T>::to() and TypeConversion<T>::from().

An additional implementation detail is that these new IntoType and UseType specializations also inherit from the BaseValueHolder struct, which is used to hold the val_ member of type TypeConversion<T>::base_type. This ensures the correct order of initialization for the relevant member variables.

With the framework enhanced in this way, if you want to add SOCI support for boost::gregorian::date, you need only provide the appropriate TypeConversion specialization. IntoType<date> and UseType<date> are automatically generated by the compiler.

Compare this new method for adding custom types (Listing Six) to the old method (Listing Four). The obvious advantages are simplicity and separation of responsibilities. With the new method, you are not burdened with knowing the details of the SOCI implementation. The code that you need to provide to add support for a new type is isolated to the TypeConversion class template.


#include <iostream>
#include <soci.h>
#include <boost/date_time/gregorian/gregorian.hpp>

using boost::gregorian::months_of_year;
using boost::gregorian::date;

namespace SOCI
{
template<> struct TypeConversion<date>
{
    typedef std::tm base_type;
    static date from(std::tm& t)
    {
        date d( t.tm_year + 1900, 
             static_cast<months_of_year>(t.tm_mon + 1), t.tm_mday );
        return d;
    }
    static std::tm to(date& d)
    {
        std::tm t;
        t.tm_isdst = -1;
        t.tm_year = d.year() - 1900;
        t.tm_mon = d.month() - 1;
        t.tm_mday = d.day();
        t.tm_hour = 0;
        t.tm_min = 0;
        t.tm_sec = 0;
        std::mktime(&t); 
        return t;
    }
};
};
#endif
Listing Six

Pass-Through Version of TypeConversion

SOCI also provides a Row class and a ColumnProperties class, which you can use together as a means for selecting data from columns that are not known at compile time. For example, the code in Listing Seven selects data into a Row and creates an XML document from it.

#include "listing6.h"

#include <soci.h>
#include <boost/date_time/gregorian/gregorian.hpp>

using namespace SOCI;
using boost::gregorian::date;

int main()
{
    try
    {
        Session sql("oracle", "service=gen1 user=scott"
                " password=tiger");

        sql << "create table person(id number, name varchar2(20),"
               " birthday date)";

        int id = 100;
        std::string name("Bjarne");
        date d(2001, boost::gregorian::Jan, 1);

        sql << "insert into person values(:id, :name, :bday)",
           use(id), use(name), use(d);

        Row r;
        sql << "select * from person", into(r);

        std::ostringstream doc;
        doc << "<ROW>" << std::endl;
        for(size_t i=0; i<r.size(); ++i)
        {
            const ColumnProperties& props = r.getProperties(i);
            doc << '<' << props.getName() << '>';
            switch(props.getDataType())
            {
            case eString:
                doc << r.get<std::string>(i);
                break;
            case eDouble:
                doc << r.get<double>(i);
                break;
            case eInteger:
                doc << r.get<int>(i);
                break;
            case eDate:
                doc << r.get<date>(i);
                break;
            default:
                throw std::runtime_error("unknown column type");
            }
            doc << "</" << props.getName() << '>' << std::endl;  
        }
        doc << "</ROW>";
        std::cout<<doc.str()<<std::endl;

        sql << "drop table person";
    }
    catch(std::exception& e)
    {
        std::cout<<e.what()<<std::endl;
    }
}
Listing Seven

First, the select statement is executed, passing an instance of Row by reference to the into() function. Then the Row's ColumnProperties are used to determine the name and data type of each column. The data for each column is retrieved from the Row by calling Row::get<T>(), where T is based on the data type of the column.

To enable Row::get<T>() to work with both custom types and native types, SOCI provides a default definition of TypeConversion<T>, where the base_type typedef is also T; see Listing Eight.

namespace SOCI
{
template<typename T> struct TypeConversion
{
    typedef T base_type;
    static T from(T const &t) { return t; }
    // pass-through version of to() is not needed by SOCI
};
};
Listing Eight

The definition of Row::get<T>() calls TypeConversion<T>::from(). When you call Row::get<T>() where T is a type for which TypeConversion is specialized (such as boost::date::gregorian in the previous examples), the expected conversion takes place.

On the other hand, if you call Row::get<T>() where T is a type that is natively supported by SOCI (such as std::string or int), the compiler doesn't find a specialization of TypeConversion—instead it falls back to the nonspecialized definition, which simply acts as a pass through.

Aggregate Types and Object-Relational Mapping

The potential custom types I have discussed so far all map directly to fundamental database types. For example, no matter what your preferred String class is, you would add support for it by defining a TypeConversion<> specialization using the base_type std::string. SOCI in turn uses it to read/write to a single character-based column in the database.

In fact, you can also provide a specialization of the TypeConversion traits class to support custom aggregate types—SOCI provides a class called Values specifically to facilitate this. The Values class acts as an intermediary between your custom aggregate class and the database. It provides get() and set() methods that allow you to define how your class maps to columns in the database. Listing Nine is a mapping of a Person struct to its database columns.

#include "listing6.h"

#include <soci.h>
#include <boost/date_time/gregorian/gregorian.hpp>
using boost::gregorian::date;

struct Person
{
    int id;
    std::string name;
    date birthday;
};

namespace SOCI
{
template<> struct TypeConversion<Person>
{
    typedef Values base_type;
    static Person from(Values const &v)
    {
        Person p;
        p.id = v.get<int>("ID");
        p.name = v.get<std::string>("NAME");
        p.birthday = v.get<date>("BIRTHDAY");
        return p;
    }
    static Values to(Person &p)
    {
        Values v;
        v.set("ID", p.id);
        v.set("NAME", p.name);
        v.set("BIRTHDAY", p.birthday);
        return v;
    }
};
};
Listing Nine

TypeConversion-based mappings to the Values class are simple to define, yet they provide a powerful and flexible means of abstraction. In fact, they provide a way to implement object-relational mapping, which is an area of much interest in the world of database programming using object-oriented languages (see Martin Fowler's Patterns of Enterprise Application Architecture; Addison-Wesley, 2002).

Listing Ten is a simple database access class that makes use of the TypeConversion<Person> specialization. The PersonDBStorage class provides methods GetPersonByName(), AddPerson(), and UpdatePerson(). As you can see, its implementation is simplified by the use of the TypeConversion-based object-relational mapping. If your class has more than a few data members, the simplicity gain from using such a mapping will be more pronounced.

#include "listing6.h"
#include "listing9.h"

#include <soci.h>
#include <boost/date_time/gregorian/gregorian.hpp>

using namespace SOCI;
using boost::gregorian::date;

class PersonDBStorage
{
public:
    PersonDBStorage(SOCI::Session& sql) : mSql(sql) {}
    
    virtual Person getPersonByName(std::string& name)
    {
        Person p;
        mSql << "select * from Person where name = :NAME", use(name),
	        into(p);
	return p;
    }
	
    virtual void addPerson(Person& p)
    {
        mSql << "insert into Person values(:ID, :NAME, :BIRTHDAY)",
	        use(p);
    }

    virtual void updatePerson(Person& p)
    {
        mSql << "update Person set name = :NAME, " 
		"birthday = :BIRTHDAY where id = :ID", use(p);
    }
private:
    Session& mSql;
};

int main()
{
    try
    {
        Session sql("oracle", "service=gen1 user=scott"
                    " password=tiger");

        sql << "create table person(id number(5), name varchar2(20),"
               " birthday date)";

        PersonDBStorage storage(sql);

        Person p;
        p.id = 100;
        p.name = "Bjarne";
        p.birthday = date(2001, boost::gregorian::Jan, 1);
        storage.addPerson(p);

        Person p2 = storage.getPersonByName(p.name);
        assert(p2.birthday == p.birthday);
        std::cout << p2.name << "'s birthday is " << p2.birthday 
		  <<std::endl;

        sql << "drop table person";
    }
    catch(std::exception& e)
    {
        std::cout << e.what() << std::endl;
    }
}
Listing Ten

Conclusion

Supporting user customization via TypeConversion<T> is viable and potentially beneficial for any library that is driven by template specialization. The benefit is that it gives library users a succinct and noninvasive mechanism for making the library work transparently with their preferred user-defined types.

If you plan to implement TypeConversion support within your library, one technique to consider is to utilize a partial class template specialization for type TypeConversion<T>::base_type, which derives from the class template specialization for type T.

Acknowledgments

Thanks to Maciej Sobczak, founder of the SOCI project, for suggesting the use of the BaseValueHolder class in Listing Five.

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.