csiz
csiz

Reputation: 5190

C++ templates: call whichever function matches between 2 choices

How can I call the function that matches using C++ templates? For example if I have the functions a and b:

void a_impl(string, int){}
void b_impl(int, string){}

template<typename X, typename Y>
void a(X x, Y y){
  a_impl(x, y);
}

template<typename X, typename Y>
void b(X x, Y y){
  b_impl(x, y);
}

template<typename X, typename Y>
void a_or_b(X x, Y y);

How do I implement a_or_b such that it calls a(x, y) if it matches, otherwise call b(x, y)?


What I'm trying to make is a for_each function that can handle these cases:

vector<pair<string, int>> v1 = {{"one", 1}, {"two", 2}};

for_each(v1, [](string x, int y){
  cout << x << " " << y << endl;
});

vector<int> v2 = {1, 2, 3};

for_each(v2, [](int x){
  cout << x << endl;
});

So far I have it working for both tuples and single variables independently but I want the appropriate version to be selected automatically. Here is my implementation so far; unpack is apply_from_tuple from this page http://www.cppsamples.com/common-tasks/apply-tuple-to-function.html.

template<typename Range, typename Func>
void for_each_unpack(Range && range, Func && func){
  for (auto && element : range){
    using Element = decltype(element);
    unpack(std::forward<Func>(func), std::forward<Element>(element));
  }
}

template<typename Range, typename Func>
void for_each_nounpack(Range && range, Func && func){
  for (auto && element : range){
    using Element = decltype(element);
    std::forward<Func>(func)(std::forward<Element>(element));
  }
}

Edit: Got it working thanks to @jotik. I put the code on github https://github.com/csiz/for_each.

Upvotes: 3

Views: 89

Answers (2)

prestokeys
prestokeys

Reputation: 4849

Here's an interesting variation of your idea. Pass a tuple of functions and a pack of arguments. Then call up all the functions from that tuple (from left to right), that can accept those given arguments. Furthermore, store all the return values (if any) from those functions called in a tuple. Here I'm using the void_t (std::void_t in C++17) aspect of SFINAE:

#include <iostream>
#include <tuple>
#include <type_traits>

template <typename T>
using void_t = void;

template <std::size_t N, typename VoidTFailed, typename EnableIfFailed, typename Tuple, typename... Args>
struct Try {
    static bool execute (Tuple&&, Args&&...) {
        std::cout << "No function found.\n";
        return false;
    }
};

template <std::size_t N, typename VoidTFailed, typename Tuple, typename... Args>
struct Try<N, VoidTFailed, std::enable_if_t<(N < std::tuple_size<Tuple>::value)>, Tuple, Args...> : std::conditional_t<(N < std::tuple_size<Tuple>::value - 1),
    Try<N+1, void, void, Tuple, Args...>,
    Try<N+1, int, int, Tuple, Args...>  // If N == std::tuple_size<Tuple>::value - 1, then simply inherit from the primary template above, as inheriting from Try<N+1, void, void, Tuple, Args...> will cause a compiling error when std::tuple_element_t<N+1, Tuple> is instantiated in the specialization below.
> {};

struct NoReturnValue {
    friend std::ostream& operator<< (std::ostream& os, const NoReturnValue&) {
        return os << "[no value returned]";
    }
};

template <std::size_t N, typename Tuple, typename EnableIfFailed, typename... Args>
struct Continue {
    static auto execute (Tuple&& functionTuple, Args&&... args) {
        const auto singleTuple = std::make_tuple (std::get<N>(std::forward<Tuple>(functionTuple))(std::forward<Args>(args)...));
        return std::tuple_cat (singleTuple, Try<N+1, void, void, Tuple, Args...>::execute(std::forward<Tuple>(functionTuple), std::forward<Args>(args)...));
    }
};

template <std::size_t N, typename Tuple, typename... Args>  // This specialization is only used if the function in question returns void.
struct Continue<N, Tuple, std::enable_if_t<std::is_void<decltype(std::declval<std::tuple_element_t<N, std::decay_t<Tuple>>>()(std::declval<std::decay_t<Args>>()...))>::value>, Args...> {
    static auto execute (Tuple&& functionTuple, Args&&... args) {
        std::get<N>(std::forward<Tuple>(functionTuple))(std::forward<Args>(args)...);
        return std::tuple_cat (std::make_tuple(NoReturnValue{}), Try<N+1, void, void, Tuple, Args...>::execute(std::forward<Tuple>(functionTuple), std::forward<Args>(args)...));
    }
};

template <std::size_t N, typename Tuple, typename EnableIfFailed, typename... Args>
struct Stop {
    static auto execute (Tuple&& functionTuple, Args&&... args) {
        return std::make_tuple (std::get<N>(std::forward<Tuple>(functionTuple))(std::forward<Args>(args)...));
    }
};

template <std::size_t N, typename Tuple, typename... Args>  // This specialization is only used if the function in question returns void.
struct Stop<N, Tuple, std::enable_if_t<std::is_void<decltype(std::declval<std::tuple_element_t<N, std::decay_t<Tuple>>>()(std::declval<std::decay_t<Args>>()...))>::value>, Args...> {
    static auto execute (Tuple&& functionTuple, Args&&... args) {
        std::get<N>(std::forward<Tuple>(functionTuple))(std::forward<Args>(args)...);
        return std::make_tuple(NoReturnValue{});
    }
};

template <std::size_t N, typename Tuple, typename... Args>
struct Try<N, void_t<decltype(std::declval<std::tuple_element_t<N, Tuple>>()(std::declval<Args>()...))>,
        std::enable_if_t<(N < std::tuple_size<Tuple>::value)>, Tuple, Args...> : std::conditional_t<(N < std::tuple_size<Tuple>::value - 1),
    Continue<N, Tuple, void, Args...>,
    Stop<N, Tuple, void, Args...>
> {};

template <typename Tuple, typename... Args>
auto executeAllPossible (Tuple&& functionTuple, Args&&... args) {
    return Try<0, void, void, Tuple, Args...>::execute(std::forward<Tuple>(functionTuple), std::forward<Args>(args)...);
}

// Testing
int foo (int, char, bool, int, int) {std::cout << "foo\n";  return 8;}
bool bar (int, char, bool, double, long, bool) {std::cout << "bar\n";  return true;}
void goo (int, char&&, bool, float, int) {std::cout << "goo\n";}
double baz (int, const char, bool&&, double, std::size_t) {std::cout << "baz\n";  return 4.5;}

template <typename Ch, typename Tr, typename Tuple, std::size_t... Is>
void print_tuple_impl (std::basic_ostream<Ch, Tr>& os, const Tuple& t, std::index_sequence<Is...>) {
    using A = int[];
    (void)A{(void(os << (Is == 0 ? "" : ", ") << std::get<Is>(t)), 0)...};
}

template <typename Ch, typename Tr, typename... Args>
decltype(auto) operator<< (std::basic_ostream<Ch, Tr>& os, const std::tuple<Args...>& t) {
    os << "tuple{";
    print_tuple_impl(os, t, std::index_sequence_for<Args...>{});
    return os << "}";
}

int main() {
    const auto tuple = executeAllPossible (std::make_tuple(foo, bar, goo, baz), 2, 'c', true, 3.5, 8);
    std::cout << std::boolalpha << "output = " << tuple << '\n';
}

Output:

foo  goo  baz
output = tuple{8, [no value returned], 4.5}

Upvotes: 1

jotik
jotik

Reputation: 17910

Use a trailing return type with decltype and SFINAE:

#include <iostream>
#include <string>
#include <utility>


void a(std::string, int) { std::cout << "a" << std::endl; }
void b(int, std::string) { std::cout << "b" << std::endl; }

template <typename ... Args>
auto a_or_b(Args && ... args)
    -> decltype(a(std::forward<Args>(args)...))
{ return a(std::forward<Args>(args)...); }

template <typename ... Args>
auto a_or_b(Args && ... args)
    -> decltype(b(std::forward<Args>(args)...))
{ return b(std::forward<Args>(args)...); }

int main() {
    std::string s;
    int i;
    a_or_b(s, i); // calls a
    a_or_b(i, s); // calls b
}

I've used perfect forwarding in the above example because it avoids a copy for each argument, but the less general naive solution with explicit types also works:

template <typename X, typename Y>
auto a_or_b(X x, Y y) -> decltype(a(x, y))
{ return a(x, y); }

template <typename X, typename Y>
auto a_or_b(X x, Y y) -> decltype(b(x, y))
{ return b(x, y); }

How SFINAE works in this case is as follows. Note that there are 2 template definitions for a_or_b. And when you write a function call to a_or_b the compiler tries to figure out which a_or_b you meant to call. Due to SFINAE, it ignores any template a_or_b for which it is unable to deduce the type for. E.g. for the call a_or_b(s, i); the (trailing) return type decltype(b(std::forward<Args>(args)...)) for the second a_or_b definition does not work, therefore the second a_or_b definition is not considered by the compiler.

In this case the return type is required to be a trailing return type, because it depends on the function arguments. For example, the following would not compile:

template <typename ... Args>
decltype(b(std::forward<Args>(args)...)) a_or_b(Args && ... args)
{ return b(std::forward<Args>(args)...); }

Upvotes: 3

Related Questions