I'm trying to write a template function that will perform a set of dynamic_casts()
based on its template parameter. I've got the following to illustrate:
class FooData
{
public:
virtual ~FooData() {};
};
class DerivedFooData: public FooData{};
class Other: public FooData{};
void bar(DerivedFooData* d1, DerivedFooData* d2) {}
void bar(DerivedFooData* d1, Other*, DerivedFooData* d2, Other*, Other*) {}
int main()
{
DerivedFooData d1,d2;
std::vector<FooData*> container1{&d1, &d2};
std::vector<FooData*> container2{&d1, &d2};
// I want bar to be called with container1[0] cast to DerivedFooData and container2[1] cast to DerivedFooData
// the first two template params are each container size
foo<1, 1, DerivedFooData, DerivedFooData>(container, container2);
// I want bar to be called with
// container1[0] cast to DerivedFooData
// container1[1] cast to Other
// container2[0] cast to DerivedFooData
// container2[1] cast to Other
// container2[2] cast to Other
foo<2, 3, DerivedFooData, Other, DerivedFooData, Other, Other>(container, container2);
}
I can manually create some of those:
template <int N, int M, typename U, typename V>
void foo(const std::vector<FooData*>& input, const std::vector<FooData*>& output)
{
bar(dynamic_cast<U*>(input[0]), dynamic_cast<V*>(output[0]));
}
template <int N, int M, typename U, typename V, typename W, typename X, typename Y>
void foo(const std::vector<FooData*>& input, const std::vector<FooData*>& output)
{
bar(dynamic_cast<U*>(input[0]), dynamic_cast<V*>(input[1]), dynamic_cast<W*>(output[0]), dynamic_cast<X*>(output[1]), dynamic_cast<Y*>(output[2]));
}
But I can't figure out how to specify in a generic way all combinations of N
and M
. I assume variadic templates will come in somewhere, but I would need some guidance.
The basic idea behind any template recursion is to handle the arguments one at a time, recurse on the function with one input type argument removed, and then terminate when the template argument list is empty.
A common way to handle two variadic type lists is to define a "pack" type that you can specialize on, where the pack takes a variable number of template arguments. This gives you the ability to easily separate multiple sets of variadic type arguments.
Here, I demonstrate this example by declaring type_pack
without any implementation (which is fine, we only use it as a type and we never instantiate it) and then I declare multiple specializations of foo_fn
that are designed to:
dynamic_cast
.bar()
with the computed arguments when both packs are empty.template <typename...>
struct type_pack;
// Base declaration that we will specialize.
template <int, int, typename, typename>
struct foo_fn;
// Specialization handling when the first pack is not empty.
template <int OffsetA, int OffsetB, typename THead, typename... T1, typename... T2>
struct foo_fn<OffsetA, OffsetB, type_pack<THead, T1...>, type_pack<T2...>> {
template <typename... Args>
static void f(foovec_t const & input, foovec_t const & output, Args && ... args) {
return foo_fn<
OffsetA + 1,
OffsetB,
type_pack<T1...>,
type_pack<T2...>
>::f(input, output, std::forward<Args>(args)..., dynamic_cast<THead *>(input[OffsetA]));
}
};
// Specialization handling when the first pack is empty and the second
// pack is not empty.
template <int OffsetA, int OffsetB, typename THead, typename... T>
struct foo_fn<OffsetA, OffsetB, type_pack<>, type_pack<THead, T...>> {
template <typename... Args>
static void f(foovec_t const & input, foovec_t const & output, Args && ... args) {
return foo_fn<
OffsetA,
OffsetB + 1,
type_pack<>,
type_pack<T...>
>::f(input, output, std::forward<Args>(args)..., dynamic_cast<THead *>(output[OffsetB]));
}
};
// Specialization handling the terminating case (all packs empty).
template <int OffsetA, int OffsetB>
struct foo_fn<OffsetA, OffsetB, type_pack<>, type_pack<>> {
template <typename... Args>
static void f(foovec_t const &, foovec_t const &, Args && ... args) {
bar(std::forward<Args>(args)...);
}
};
// Helper type to provide the two initial integer values.
template <typename, typename>
struct foo;
template <typename... T1, typename... T2>
struct foo<type_pack<T1...>, type_pack<T2...>> {
static void f(foovec_t const & input, foovec_t const & output) {
foo_fn<0, 0, type_pack<T1...>, type_pack<T2...>>::f(input, output);
}
};
You would call this like foo<type_pack<DerivedFooData, Other>, type_pack<DerivedFooData, Other, Other>>::f(container, container2)
in your second example. Note that you don't have to provide any sizes; these are inferred from the size of each pack.
See this demo and note that the pointer arguments where the type doesn't match come through as null.
I don't attempt to define bar()
as I assume you have already done this, or know how to do it. The bar()
in my example only accepts specific pointer types (for the purposes of testing that the casts were correctly performed).
This code uses only C++11 features.
Note that std::forward
is not strictly necessary because the cast values are always pointers. However, it's good to get in the habit of using it when forwarding a variable-size argument list. If the values were huge strings/vectors then forwarding at each step would eliminate a ton of useless copying.