dimanche 9 mai 2010

Variadic templates

Ça a été chaud, mais j'ai enfin réussi à faire quelque chose d'utile avec mes variadic templates. J'ai templatisé le code de base de données pour laisser le compilo générer les classes de transactions à ma place.

En utilisant le système de classes de transactions de pqxx, l'on se retrouve à écrire beaucoup de classes qui se ressemblent beaucoup. L'on a un constructeur dans lequel on passe ses paramètres d'entrée et de sortie, l'opérateur parenthèses qui effectue l'appel en lui-même. J'y rajoute généralement la préparation de ma requête. Un exemple parmi d'autres:


class CheckAccount : public pqxx::transactor<pqxx::transaction<> >
{
public:
CheckAccount(const boost::shared_ptr<Logger> & logger,
const std::string & login,
const std::string & password,
bool & success,
UserId & userId):
m_logger(logger),
m_login(login),
m_password(password),
m_success(success),
m_userId(userId)
{
}

void operator()(argument_type & T)
{
TimedLogger t(m_logger, "Executed \"" + getRequest() + "\"");
pqxx::result result = T.prepared(getPlan())(m_login)(m_password).exec();
if(result.size() == 1)
{
m_success = true;
m_userId = UserId(result.at(0).at(0).as<uint32_t>());
}
else
{
m_success = false;
}
}

static std::string getPlan()
{
return "checkAccount_plan";
}

static std::string getRequest()
{
return "select checkAccount($1, $2)";
}

static void prepare(const boost::shared_ptr<pqxx::connection> & dbConn)
{
dbConn->prepare(getPlan(), getRequest())
("varchar(255)", pqxx::prepare::treat_string)
("varchar(255)", pqxx::prepare::treat_string);
}

private:
boost::shared_ptr<Logger> m_logger;
std::string m_login;
std::string m_password;
bool & m_success;
UserId & m_userId;
};


Le problème, c'est que c'est très verbeux, et qu'en écrire 2 ou 3, ça passe encore, mais des dizaines, ça enlève tout le fun du code.

J'ai donc profité des variadic templates pour tenter de réduire un petit peu la chose. L'idée est de créer une classe policy, qui va contenir les informations importantes de la requête:


class CheckAccount
{
public:
static std::string getPlan()
{
return "checkAccount_plan";
}

static std::string getRequest()
{
return "select checkAccount($1, $2)";
}

typedef std::tuple<std::string, std::string> InputT;
typedef std::tuple<int> OutputT;
};


Cette classe étant définie, on l'utilise pour créer la classe requête attendue, et on la lance.


typedef Request<CheckAccount> CheckAccountRequest;
CheckAccountRequest::prepare(dbConn);
dbConn->perform(CheckAccountRequest(std::make_tuple(login, password), result));


La difficulté est de correctement gérer la préparation d'une part, et l'appel d'autre part. Voici à quoi ressemble la classe de requête:


template<class POLICY>
class Request : public pqxx::transactor<pqxx::transaction<> >
{
public:
Request(const typename POLICY::InputT & input,
typename POLICY::OutputT & output):
m_input(input),
m_output(output)
{
}

void operator()(argument_type & T)
{
pqxx::prepare::invocation invocation = T.prepared(POLICY::getPlan());
internals::call(invocation, m_input);
invocation.exec();
}

static void prepare(const boost::shared_ptr<pqxx::connection> & dbConn)
{
internals::prepare(dbConn,
POLICY::getPlan(),
POLICY::getRequest(),
typename POLICY::InputT());
}

private:
const typename POLICY::InputT & m_input;
typename POLICY::OutputT & m_output;
};


Toute la logique se trouve dans les fonctions templates "prepare" et "call". Regardons donc la préparation:


template<typename ... Args>
class Prepare;

template<typename T, typename ... Args>
class Prepare<T, Args...>
{
public:
void operator()(const pqxx::prepare::declaration & declaration)
{
Prepare<Args...>()(declaration
(SqlType<T>::getSqlType(),
SqlType<T>::getParamTreatment()));
}
};

template<>
class Prepare<>
{
public:
void operator()(const pqxx::prepare::declaration & declaration)
{
}
};

template<typename ... Args>
void prepare(const boost::shared_ptr<pqxx::connection> & dbConn,
const std::string & plan,
const std::string & request,
const std::tuple<Args...> & )
{
Prepare<Args...>()(dbConn->prepare(plan, request));
}


L'on appelle récursivement Prepare, en dépaquetant le template variadique au fur et à mesure. A chaque appel, l'on utilise une classe utilitaire SqlType qui renvoie le nom SQL et traitement à effectuer sur le paramètre en fonction du type passé (par exemple, std::string s'appelle text en SQL, et nécessite d'être échappé).

Le code déroule donc le tuple, et utilise le type de tête pour gérer la préparation.

Jetez un coup d'oeil dans le <a href="http://adh.svn.sourceforge.net/viewvc/adh/framework/trunk/utils/Db.h?view=markup">dépôt subversion</a> sur la manière de gérer la partie "call": plutôt que de dérouler le tuple, ma récursion se fait sur un paramètre template entier qui correspond à l'index de l'élément du tuple qui m'intéresse.

Malheureusement, C++0x est encore très expérimental, et cela se sent: il faut beaucoup tripoter pour que les choses marchent, et la bibliothèque standard est assez pauvre sur les nouveaux types (pas de "get_head" et "get_tail" pour std::tuple, par exemple).

Mais je ne regrette pas de m'y être mis. Je n'ai plus qu'à gérer les paramètres de sortie, et à moi les requêtes solides et typesafe en un clin d'œil!

1 commentaire:

Anonyme a dit…

besoin de verifier:)