Elegantly switching template arguments for a set of functions

Viewed 140

I am writing some code that uses an external library, where several functions are defined approximately like this:

// Library.h

template<typename T>
void foo(int arg1, bool arg2);

template<typename T>
int bar(float arg);

(examples are given to illustrate that both argument lists and return value types are diverse, but do not contain the template type T).

In my code, I want to be able to call different template instances offoo and bar, depending on some internal mapping logic. This can be e.g. a mapping from an enum representing data types, but, importantly, this logic is the same for foo, bar, or anything else form this library.

A simple way to achieve this would be something like

// MyCode.h

enum class MyType { BOOL, CHAR };

void foo_wrapper(MyType type, int arg1, bool arg2)
{
  if (type == MyType::BOOL)
    return foo<bool>(arg1, arg2);
  else if (type == MyType::CHAR)
    return foo<char>(arg1, arg2);
  else
    throw std::runtime_error("oops");
}

int bar_wrapper(MyType type, float arg)
{
  if (type == MyType::BOOL)
    return bar<bool>(arg);
  else if (type == MyType::CHAR)
    return bar<char>(arg);
  else
    throw std::runtime_error("oops");
}

However, this is a lot of logic duplication and correcting the arg names, etc., when it would be needed for another function, leaving plenty of possibilities for missing something. My current solution is to have a static map of relevant template instantiations in each wrapper function:

void foo_wrapper(MyType type, int arg1, bool arg2)
{
  using FunctionType = std::function<void(int, bool)>;
  static const std::unordered_map<MyType, FunctionType> functionMap{
    {BOOL, foo<bool>}, 
    {CHAR, foo<char>}
  };
  if (!functionMap.count(type))
    throw std::runtime_error("oops");
  return functionMap.at(type)(arg1, arg2);
}

int bar_wrapper(MyType type, float arg)
{
  using FunctionType = std::function<int(float)>;
  static const std::unordered_map<MyType, FunctionType> functionMap{
    {BOOL, bar<bool>}, 
    {CHAR, bar<char>}
  };
  if (!functionMap.count(type))
    throw std::runtime_error("oops");
  return functionMap.at(type)(arg);
}

Upside: Arguments are passed only in one place in code, the mapping is "centralized" at the beginning of each wrapper instead of distributed in wrapper function code. Also, less code of the choice logic is being copied around.

But: We still need to duplicate the mapping correspondencies - now in the shape of a map declaration - across multiple wrappers (imagine a dozen library functions used in this way...).

Ideally, I would like to have a magic switch_type_for_func implemented that would allow doing something like

void foo_wrapper(MyType type, int arg1, bool arg2)
{
  return switch_type_for_func<foo>(type, arg1, arg2);
}

int bar_wrapper(MyType type, float arg)
{
  return switch_type_for_func<bar>(type, arg);
}

I see that this cannot work because foo is a template, but it intuitively feels as if there should be some solution that would eliminate code duplication in this case.

I can almost imagine a macros doing the job (because what I need is just the name of the function, not much more), but AFAIU these are not exactly best practice... Maybe I am just stuck in my way of thinking about it and there is something more appropriate. Any feedback/advice is appreciated!

4 Answers

Convert your enum values to std::type_identity<T> and put them into a conducible context. std::variant, std::visit enables to dispatch easily. This solution is much more elegant than my previous one.

This needs C++20 support, if you implement your own std::type_identity, which is simple, C++17 is enough.

using VType = std::variant<std::type_identity<bool>,
                           std::type_identity<char>>;

static const std::map<MyType, VType> dispatcher = {
    {MyType::BOOL, std::type_identity<bool>{}},
    {MyType::CHAR, std::type_identity<char>{}}
};

void foo_wrapper(MyType type, int arg1, bool arg2)
{
    return std::visit([&](auto v){
        foo<typename decltype(v)::type>(arg1, arg2);
    }, dispatcher.at(type));
}

Demo

First of all, it's clear that there is no 100% elegant solution because you can't really do anything with function templates except instantiating them. You can't abstract over them whatsoever. So at least a small amount of boilerplate code for every function template is required.

But I think lambdas provide a neat way to accomplish this with the least amount of typing overhead possible. In C++20, you can do this:

template <class F>
auto dispatcher(F&& f, MyType t) {
    switch (t) {
        case MyType::BOOL:
            return f.template operator()<bool>();
        case MyType::CHAR:
            return f.template operator()<char>();
    };
}

auto foo_wrapper(MyType type, int arg1, bool arg2) {
    return dispatcher([&]<class T>() { return foo<T>(arg1, arg2); }, type);
}

auto bar_wrapper(MyType type, float arg) {
    return dispatcher([&]<class T>() { return bar<T>(arg); }, type);
}

https://godbolt.org/z/796xETr3o

That's two lines per new MyType value, and one lambda (inside the wrapper function you were going to write anyway) per function you want to wrap. I don't think you'll get much better than this.

The default by-reference capture should be perfectly fine - it would only fail if you were to take something by value in the wrapper which the original function takes by ref (and even then it would have to store the reference in global state to cause danger), which you obviously shouldn't do.

FWIW, you can emulate this before C++20 (but with C++14) using polymorphic lambdas instead, it's just more ugly (if you don't want to go all crude):

        // ...
        case MyType::BOOL:
            return f(std::type_identity<bool>{});
        // ...
        return dispatcher([&](auto ti) { return foo<decltype(ti)::T>(arg1, arg2); }, type);

https://godbolt.org/z/7cjdzx681

Quite a bit more awkward to type/decipher, but doesn't require templated lambdas.

You can implement a dispatch with a fold expression by defining an overloaded operator or for std::optional<T> using std::optional<T>::or_else in C++23:

#include <optional>
#include <type_traits>
#include <utility>
#include <variant>

template <auto Key, class T>
struct pair {};

template <class... Pairs>
struct table {};

template <class Optional, class E, E Key, class T>
constexpr Optional try_dispatch(E key, pair<Key, T>) {
  return key == Key ? Optional{std::type_identity<T>{}} : std::nullopt;
}

template <class T>
constexpr std::optional<T> operator or(std::optional<T> lhs,
                                       std::optional<T> rhs) {
  return lhs.or_else([=] { return rhs; });
}

template <class F, class E, E... Keys, class... Ts>
constexpr auto dispatch(F &&f, E key, table<pair<Keys, Ts>...>) {
  using Optional = std::optional<std::variant<std::type_identity<Ts>...>>;

  const auto variant =
      (... or try_dispatch<Optional>(key, pair<Keys, Ts>{})).value();

  return std::visit(std::forward<F>(f), variant);
}

template <class T>
void foo(int arg1, bool arg2);

template <class T>
int bar(float arg);

enum class MyType { BOOL, CHAR };

constexpr auto my_table = table<pair<MyType::BOOL, bool>,
                                pair<MyType::CHAR, char>>{};

void foo_wrapper(MyType key, int arg1, bool arg2) {
  dispatch(
      [=]<class T>(std::type_identity<T>) {
        foo<T>(arg1, arg2);
      },
      key, my_table);
}

int bar_wrapper(MyType key, float arg) {
  return dispatch(
      [=]<class T>(std::type_identity<T>) {
        return bar<T>(arg);
      },
      key, my_table);
}

Compiler Explorer

Ok, thanks to the comment from @463035818_is_not_a_number about template template parameters and the thread here, I came to sort of a solution that technically achieved the goal of not duplicating the choice logic, but requires something that feels like too much wrapper code to make it work (basically a trivial struct to wrap each needed library function) - see full code here:

template<class T>
struct Foo
{
    static auto run(int arg1, bool arg2)
    {
        return foo<T>(arg1, arg2);
    }
};

template<class T>
struct Bar
{
    static auto run(float arg)
    {
        return bar<T>(arg);
    }
};

template<template <typename> class T, typename ... Args>
auto switch_type_for_func(MyType type, Args... args)
{
    if (type == MyType::BOOL)
      return T<bool>::run(args...);
    else if (type == MyType::CHAR)
      return T<char>::run(args...);
    else
      throw std::runtime_error("oops");
}

void foo_wrapper(MyType type, int arg1, bool arg2)
{
  return switch_type_for_func<Foo>(type, arg1, arg2);
}

int bar_wrapper(MyType type, float arg)
{
  return switch_type_for_func<Bar>(type, arg);
}

Slightly simplifying this, we could call switch_type_for_func something more pretty and get rid of the foo_wrapper and bar_wrapper, directly calling in the main code e.g. lib_helper<Bar>(type, arg); (see full code here).

If anyone sees a way to avoid the ugly structs, please add your ideas!

Related