10

I want to write benchmark code for several combinations of several possible classes. If I write each combination myself it becomes an unmaintainable mess. Thus I'm looking for a way to automatically combine each type via templates, something akin to the following pseudo code:

for (typename HashFuction : Sha256, Sha512, Sa512_256, Sha3_256, Sha3_512) {
   for (typename KeyingWrapper : TwoPassKeyedHash, OnePassKeyedHash, PlainHash) {
      for (typename InstantiatedGetLeaf: GetLeaf<8>, GetLeaf<1024>) {
         for (typename algorithm : algA, algB, algC) {
            runAndTime<HashFunction,KeyingWrapper,
                       InstantiatedGetLeaf,algorithm>(someArgs);
         }
       }
    }
 }

Where Sha256,… ,TwoPassKeyedHash,… are types.

The code I'm looking for is supposed to be functionally equivalent to the following:

runAndTime<Sha256,TwoPassKeyedHash,GetLeaf<8>,algA>(someArgs);
runAndTime<Sha256,TwoPassKeyedHash,GetLeaf<8>,algB>(someArgs);
runAndTime<Sha256,TwoPassKeyedHash,GetLeaf<8>,algC>(someArgs);

runAndTime<Sha256,TwoPassKeyedHash,GetLeaf<1024>,algA>(someArgs);
runAndTime<Sha256,TwoPassKeyedHash,GetLeaf<1024>,algB>(someArgs);
runAndTime<Sha256,TwoPassKeyedHash,GetLeaf<1024>,algC>(someArgs);

runAndTime<Sha256,OnePassKeyedHash,GetLeaf<8>,algA>(someArgs);
runAndTime<Sha256,OnePassKeyedHash,GetLeaf<8>,algB>(someArgs);
runAndTime<Sha256,OnePassKeyedHash,GetLeaf<8>,algC>(someArgs);

// And 99 further lines…

With Peregring-lk's help I have come as far as

#include <iostream>

template<typename Aux_type>
void test_helper()
{}

template<typename Aux_type, typename Head, typename... Tail>
void test_helper() {
   std::cout << Head::i;
   test_helper<Aux_type, Tail...>();
}

template<typename... Args>
void test()
{
    test_helper<void, Args...>();
}

struct A{
   static const int i=1;
};

struct B{
   static const int i=2;
};

int main() {
   test<A, B>();
   return 0;
}

but I don't yet see how I could iterate that recursion to get nested loops. Any help would be appreciated.

(Edit: Code restructuring and inclusion of Peregring-lk's answer.)

Perseids
  • 12,584
  • 5
  • 40
  • 64
  • [Boost MPL?](http://www.boost.org/doc/libs/1_55_0b1/libs/mpl/doc/index.html) – Andriy Tylychko Jun 03 '14 at 12:57
  • @AndyT: I haven't found anything useful, but I am not experienced with Boost MPL (and Boost in general), so I might have overlooked something. – Perseids Jun 03 '14 at 13:05
  • use `mpl::vector` and `mpl::for_each` to iterate over it. example: http://stackoverflow.com/questions/8386494/boostmpl-vector-and-for-each-how-to-print-avector-as-a-tuple – Andriy Tylychko Jun 03 '14 at 13:41
  • @AndyT: Apparently I am unable to wrap my head around how I can iterate the `for_each` to get nested loops. Could you be so kind to give me an example? – Perseids Jun 03 '14 at 15:08
  • A nice academic exercise (which I fear has no solution), but isn't the practical solution some scripting to generate the code? – david.pfx Jun 03 '14 at 15:43
  • @david.pfx: True. I'll do that if no one else has a solution. – Perseids Jun 03 '14 at 16:15
  • 1
    Some links that may be helpful: [How to create the Cartesian product of a type list?](http://stackoverflow.com/q/9122028/3043539), [TMP: how to generalize a Cartesian Product of Vectors?](http://stackoverflow.com/q/13813007/3043539) and [Avoid nested for-loops when searching parameter space](http://stackoverflow.com/q/21186904/3043539). – Constructor Jun 03 '14 at 20:20

3 Answers3

10

Sometimes it helps to have an idea of what you are aiming for:

  • you need several parameter types
  • and for each parameter types, several possible "values"

And want to apply something on every single combination of values (one per parameter type at a time).

This looks like it could be expressed:

combine<
    Set<Sha256, Sha512, Sa512_256, Sha3_256, Sha3_512>,
    Set<TwoPassKeyedHash, OnePassKeyedHash, PlainHash>,
    Set<GetLeaf<8>, GetLeaf<1024>>,
    Set<algA, algB, algC>
>(runAndTime);

if runAndTime is an instance of:

struct SomeFunctor {
   template <typename H, typename W, typename L, typename A>
   void operator()(cons<H>{}, cons<W>{}, cons<L>{}, cons<A>{});
};

and cons is just a way to pass a type as a regular parameter (much easier).

Let's go ?


First, some way to pass around types (cheaply):

template <typename T>
struct cons { using type = T; };

template <typename... T>
struct Set {};

An explicit bind (with no magic inside):

template <typename F, typename E>
struct Forwarder {
    Forwarder(F f): inner(f) {}

    template <typename... Args>
    void operator()(Args... args) { inner(cons<E>{}, args...); }

    F inner;
}; // struct Forwarder

And now we delve into the real task at hand:

  • we need to iterate on sets of types
  • within a set, we need to iterate on its elements (types too)

That calls for two levels of dispatch:

template <typename FirstSet, typename... Sets, typename F>
void combine(F func);

template <typename Head, typename... Tail, typename... Sets, typename F>
void apply_set(F func, Set<Head, Tail...>, Sets... others);

template <typename... Sets, typename F>
void apply_set(F func, Set<>, Sets... others);

template <typename E, typename NextSet, typename... Sets, typename F>
void apply_item(F func, cons<E>, NextSet, Sets...);

template <typename E, typename F>
void apply_item(F func, cons<E> e);

Where combine is the outer (exposed) function, apply_set is used to iterate on the sets and apply_item is used to iterate on the types within a set.

The implementations are simple:

template <typename Head, typename... Tail, typename... Sets, typename F>
void apply_set(F func, Set<Head, Tail...>, Sets... others) {
    apply_item(func, cons<Head>{}, others...);

    apply_set(func, Set<Tail...>{}, others...);
} // apply_set

template <typename... Sets, typename F>
void apply_set(F, Set<>, Sets...) {}

template <typename E, typename NextSet, typename... Sets, typename F>
void apply_item(F func, cons<E>, NextSet ns, Sets... tail) {
    Forwarder<F, E> forwarder(func);

    apply_set(forwarder, ns, tail...);
}

template <typename E, typename F>
void apply_item(F func, cons<E> e) {
    func(e);
} // apply_item


template <typename FirstSet, typename... Sets, typename F>
void combine(F func) {
    apply_set(func, FirstSet{}, Sets{}...);
} // combine

For each of apply_set and apply_item we have a recursive case and a base case, though it's some kind of co-recursion here as apply_item calls back to apply_set.

And a simple example:

struct Dummy0 {}; struct Dummy1 {}; struct Dummy2 {};
struct Hello0 {}; struct Hello1 {};

struct Tested {
    Tested(int i): value(i) {}

    void operator()(cons<Dummy0>, cons<Hello0>) { std::cout << "Hello0 Dummy0!\n"; }
    void operator()(cons<Dummy0>, cons<Hello1>) { std::cout << "Hello1 Dummy0!\n"; }
    void operator()(cons<Dummy1>, cons<Hello0>) { std::cout << "Hello0 Dummy1!\n"; }
    void operator()(cons<Dummy1>, cons<Hello1>) { std::cout << "Hello1 Dummy1!\n"; }
    void operator()(cons<Dummy2>, cons<Hello0>) { std::cout << "Hello0 Dummy2!\n"; }
    void operator()(cons<Dummy2>, cons<Hello1>) { std::cout << "Hello1 Dummy2!\n"; }

    int value;
};

int main() {
    Tested tested(42);
    combine<Set<Dummy0, Dummy1, Dummy2>, Set<Hello0, Hello1>>(tested);
}

Which you can witness live on Coliru prints:

Hello0 Dummy0!
Hello1 Dummy0!
Hello0 Dummy1!
Hello1 Dummy1!
Hello0 Dummy2!
Hello1 Dummy2!

Enjoy :)

Note: it was presumed that the functor was cheap to copy, otherwise a reference can be used, both when passing and when storing it in Forwarder.

Edit: removed the cons around Set (everywhere it appeared), it's unnecessary.

Matthieu M.
  • 287,565
  • 48
  • 449
  • 722
  • Great answer, would upvote twice if I could :D. For the sake of completeness the compiler options from Coliru: `-std=c++11 -O2 -Wall -pedantic`, tested with clang++ 3.4 and g++ 4.8.2 – Perseids Jun 03 '14 at 18:35
  • A further question. What is the purpose of your `cons` class? Transportation of types without default constructors, for example? or there's other dark purposes? – ABu Jun 03 '14 at 21:02
  • ah ok, avoiding heavy copies (for example). That's make sense. – ABu Jun 03 '14 at 21:14
  • 2
    @Peregring-lk: Yes, `cons` is just a way to pass a type without actually building an object; both because said object might not have a default constructor and because it is unclear whether a reference could be passed (or if the benchmark calls for created a new one each time). By the way I did some surgery on the solution: I transformed all the `cons>` into just `Set<...>` because the `cons` was redundant there. I updated the live example on Coliru as well and it yields the same result. – Matthieu M. Jun 04 '14 at 06:12
3

Functions doesn't allow partial specializations, unless the specialization is complete. Every new different function signature declares a new overload, unless their signatures are exactly the same.

Try instead the following code:

#include <iostream>

template<typename Aux_type>
void test_helper()
{}

template<typename Aux_type, typename Head, typename... Tail>
void test_helper() {
   std::cout << Head::i;
   test_helper<Aux_type, Tail...>();
}

template<typename... Args>
void test()
{
    test_helper<void, Args...>();
}

struct A{
   static const int i=1;
};

struct B{
   static const int i=2;
};

int main() {
   test<A, B>();
   return 0;
}

and it does compile (and prints 12).

Anyway, I've not understood your pseudocode sample.

ABu
  • 10,423
  • 6
  • 52
  • 103
  • Thank you, that is very enlightening. I have edited my question to make the core problem more clear. – Perseids Jun 03 '14 at 13:47
0

I think that C++ is not the right tool, when it comes to convenient and flexible code generation ... Just write a simple utility in a scripting language of your choice, like in python:

generate_test_code.py:

#!/usr/bin/python
for HashFuction in {"Sha256", "Sha512", "Sa512_256", "Sha3_256", "Sha3_512"}:
    for KeyingWrapper in {"TwoPassKeyedHash", "OnePassKeyedHash", "PlainHash"}:
        for InstantiatedGetLeaf in {"GetLeaf<8>", "GetLeaf<1024>"}:
            for Algorithm in {"algA", "algB", "algC"}:
                print("runAndTime<{},{},{},{}>(someArgs);".format(HashFuction,KeyingWrapper,InstantiatedGetLeaf,Algorithm))

... then in your Makefile:

generated_test_code.cpp: generate_test_code.py
    python generate_test_code.py > generated_test_code.cpp

... and in your c++ code, simply #include "generated_test_code.cpp" where you want it.

Dangelov
  • 701
  • 6
  • 14
  • As you are using python3 I would propose `#!/usr/bin/python3` for better compatibility. Thanks for the additional, more practical answer. – Perseids Jun 22 '14 at 13:02
  • I've just tried to run it under python2.7, and it seems to work fine. – Dangelov Jun 22 '14 at 13:09
  • Oh right, as there is only one argument to the python3 `print()` function the parenthesis after the python2 `print` command will not be interpreted as a tuple, but as a superfluous bracket. I should have actually tested it with python2 before criticizing, sorry. – Perseids Jun 22 '14 at 14:43