Some context: I have code that looks like this (minor issue noted here):
Statement select("SELECT * FROM People WHERE ID > ? AND ID < ?");
select.execute(1462, 1477, [](int ID, std::string const& person, double item1, float item2){
std::cout << "Got Row:"
<< ID << ", "
<< person << ", "
<< item1 << ", "
<< item2 << "\n";
});
Anyway this connects to the MySQL DB and starts pulling data from the server. So inside execute I loop over the results and call the lambda for each row:
template<typename Action, typename ...Args>
void execute(Args... param, Action action)
{
// STUFF TO SET up connection.
// Start retrieving rows.
while(row = results->getNextRow())
{
call(action, row);
}
}
So here row gets a single row from the socket connection with mysql (so it calls the lambda as it receives each row (no pulling the rows into memory first)). So the code I want to review is pulling the data and calling the lambda.
// Statement::call
template<typename Action>
void call(Action action, std::unique_ptr<ResultSetRow>& row)
{
typedef CallerTraits<decltype(action)> trait;
typedef typename trait::AllArgs AllArgs;
Caller<trait::size, 0, AllArgs, Action>::call(action, row);
}
This utilizes the helper class CallerTraits
and Caller
to pull the required rows from the stream and then call the lambda:
// CallerTraits
// Get information about the arguments in the lambda
template <typename T>
struct CallerTraits
: public CallerTraits<decltype(&T::operator())>
{};
template<typename C, typename ...Args>
struct CallerTraits<void (C::*)(Args...) const>
{
static const int size = sizeof...(Args);
typedef std::tuple<Args...> AllArgs;
};
Then the Caller
:
// Caller::call()
// Reads the next argument required by the lambda from the stream.
// An exception will be generated if the next argument on the stream
// does not match the type expected by the lambda.
template<int size, int index, typename ArgumentTupple, typename Action, typename ...Args>
struct Caller
{
static void call(Action action, std::unique_ptr<ResultSetRow>& row, Args... args)
{
// Get the next argument type required by the lambda.
// As defined by index. Then remove all ref and const
// bindings.
typedef typename std::tuple_element<index, ArgumentTupple>::type NextArgBase;
typedef typename std::remove_reference<NextArgBase>::type NextArgCont;
typedef typename std::remove_const<NextArgCont>::type NextArg;
// Read the next value from the stream.
NextArg val;
row->getValue(val);
// Recursively call Caller::call() (via doCall())
// To get the next argument we need. All the arguments
// are accumulated in the var args parameter `args`
doCall<size-1, index+1, ArgumentTupple>(action, row, args..., val);
}
};
Specialization when no more args need to be retrieved:
// Specialization of Caller::call() when we have got all the arguments.
// This simply calls the lambda with the arguments we have accumulated.
template<int index, typename ArgumentTupple, typename Action, typename ...Args>
struct Caller<0, index, ArgumentTupple, Action, Args...>
{
static void call(Action action, std::unique_ptr<ResultSetRow>&, Args... args)
{
action(args...);
}
};
Function to deduce parameter types:
// Function template needed because we
// can not deduce the Args... parameter manually in the call.
// so we let the compiler deduce it for us.
template<int size, int index, typename ArgumentTupple, typename Action, typename ...Args>
void doCall(Action action, std::unique_ptr<ResultSetRow>& row, Args... args)
{
Caller<size, index, ArgumentTupple, Action, Args...>::call(action, row, args...);
}
2 Answers 2
I find your implementation a bit more complex than necessary. What you want to do is
fetch arguments from your "result set"
row
by calling itsgetValue()
in a particular order;use them (as arguments) to call
operator()
on function objectaction
.
This can be done without recursion in two lines:
Do{row->getValue(std::get<N>(args))...};
action(std::get<N>(args)...);
where args
is a tuple.
Range
Ok, now let's step back to see how this is possible. First, we learn how to count from 0
to a given number L
, in order to construct range 0, ..., L-1
:
// holds any number of size_t parameters
template <size_t... N>
struct sizes { using type = sizes <N...>; };
// given L>=0, generate sequence <0, ..., L-1>
template <size_t L, size_t I = 0, typename S = sizes <> >
struct Range;
template <size_t L, size_t I, size_t... N>
struct Range <L, I, sizes <N...> > : Range <L, I+1, sizes <N..., I> > { };
template <size_t L, size_t... N>
struct Range <L, L, sizes <N...> > : sizes <N...> { };
This is a very common task, actually borrowed from here. There's a better implementation with logarithmic (rather than linear) template depth, but I want to keep it simple here.
"Do"?
Next, an extremely helpful struct
lets us evaluate expressions in a given order:
// using a list-initializer constructor, evaluate arguments in order of appearance
struct Do { template <typename... T> Do(T&&...) { } };
But beware, due to a bug since at least version 4.7.0, GCC evaluates in the opposite order, right-to-left. A workaround is to provide a range in the opposite order, L-1, ..., 0
, but I'm not doing this here.
Caller
Now, Caller
has a generic definition with only two actual parameters, ArgumentTuple
and Action
. It also reads that tuple's size, say L
, and constructs range 0, ..., L-1
in a third parameter:
// generic Caller
template<
typename ArgumentTuple, typename Action,
typename Indices = typename Range<std::tuple_size<ArgumentTuple>{}>::type
>
struct Caller;
Finally, a specialization deduces the generated range as variadic size_t
parameters N...
. A local tuple of type ArgumentTuple
is used to store the arguments, and std::get<N>
accesses its N
-th element. That's it:
// Caller specialization, where indices N... have been deduced
template<typename ArgumentTuple, typename Action, size_t... N>
struct Caller<ArgumentTuple, Action, sizes<N...> >
{
static void call(Action action, std::unique_ptr<ResultSetRow>& row)
{
ArgumentTuple args;
Do{row->getValue(std::get<N>(args))...};
action(std::get<N>(args)...);
}
};
Please note that all the above code compiles but I have not seen it in action since I don't have the database infrastructure. I have just made a minimal definition
struct ResultSetRow { template<typename T> void getValue(T) { } };
So I can only hope it works for you.
I am sorry if this looks like a complete rewrite rather than a review, but I couldn't help it :-) At least I've kept the part of your code where you deduce ArgumentTuple
from the lambda.
PS-1 If your ResultSetRow::getValue()
is void
, then you need to adjust its variadic call to
Do{(row->getValue(std::get<N>(args)), 0)...};
so that each sub-expression evaluates to int
rather than void
(you cannot have a list-initializer made of void
arguments).
PS-2 I suspect you're not really managing resources here, so you don't need std::unique_ptr
; a plain ResultSetRow&
would suffice.
-
\$\begingroup\$ @iavr: Do you have any good references for using templates in the way you do. I am still having trouble reading the templates above (even after studying them, ie. I could probably not write that from scratch myself). \$\endgroup\$Loki Astari– Loki Astari2014年03月26日 05:00:40 +00:00Commented Mar 26, 2014 at 5:00
-
2\$\begingroup\$ @LokiAstari Well, I'm a bit self-taught. The best resource I know on templates is the book by Vardevoorde and Josuttis, but even this has only one (limited) chapter on metaprogramming, and unfortunalety there's no edition covering C++11 (yet). Stroustrup's 4th edition of "the C++ programming language" does cover C++11 and again has one chapter on metaprogramming. Anyhow, these are good places to start. Specifically for
Do
, check variadic templates and look forstruct pass
. There's some explanation there. \$\endgroup\$iavr– iavr2014年03月26日 09:16:34 +00:00Commented Mar 26, 2014 at 9:16 -
\$\begingroup\$ @LokiAstari For the "logarithmic depth" version of
Range
, check this answer and look formake_indexes
. \$\endgroup\$iavr– iavr2014年03月26日 09:20:29 +00:00Commented Mar 26, 2014 at 9:20
I would have probably applied the following changes:
- Make
size
astatic constexpr
variable inCallerTraits
instead of simplystatic const
. Wherever a function simply passes variadic arguments whose types have been deduced, I would have passed
args
by universal reference (now officially called forwarding reference) and usedstd::forward
to forward the results to the following functions:template<int size, int index, typename ArgumentTupple, typename Action, typename ...Args> void doCall(Action action, std::unique_ptr<ResultSetRow>& row, Args&&... args) { Caller<size, index, ArgumentTupple, Action, Args...>::call(action, row, std::forward<Args>(args)...); }
It's a bit hard and quite long to explain how it works exactly - you can find a great explanation in the answer linked above -, but the main point is that using this particular recipe implements perfect forwarding:
template<typename X> void foo(X&& arg) { bar(std::forward<X>(arg)); }
The type of the parameters of
X&&...
infoo
will have the sameconst
and reference qualifications than the type of the corresponding parameters inbar
. Anyway, the link is by far clearer than I am. Simply remember the recipe and that for this recipe to work, the typeX
has to be deduced by the function; it may not work ifX
is known from somewhere else.Instead of creating functions that take a
std::unique_ptr<ResultSetRow>&
parameters, I would have hadCaller<...>::call
anddoCall
them take aResultSetRow&
and dereferencedrow
right away. I don't know what is the exact return type ofresults->getNextRow()
so I won't try to assume anything about it and the type that the maincall
should take as a parameter.
-
1\$\begingroup\$ @LokiAstari For universal references: isocpp.org/blog/2012/11/… \$\endgroup\$Yuushi– Yuushi2014年03月26日 00:04:06 +00:00Commented Mar 26, 2014 at 0:04