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
.