3

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!

mikhail
  • 517
  • 5
  • 16
  • a wrapper for `foo` that maps some integer to the right `T` is rather simple. Doing it at runtime is not so simple – 463035818_is_not_an_ai Jul 26 '22 at 14:09
  • I dont understand your last snippet, it doesnt work because `type` is a runtime parameter that you cannot use as template argument for `switch_type_for_func`, That `foo` is a tempalte isnt the issue. I am not sure if you are really asking for runtime switch or if selecting the function at compile time would be fine. – 463035818_is_not_an_ai Jul 26 '22 at 14:12
  • @463035818_is_not_a_number good point about `type` in the template args, I edited the post. – mikhail Jul 26 '22 at 15:17
  • I want runtime switching (based on the `MyType type` argument) for multiple library functions without extensive code duplication / copying, since the switching logic is always the same. My point about `foo` being a template is that, regardless of how I implement `switch_type_for_func`, I will not be able to pass `foo` to it either as a template parameter or as an argument, AFAIU, because `foo` itself does not exist without ``, which I do not have where it would be needed. – mikhail Jul 26 '22 at 15:21
  • 1
    there are template template parameters https://stackoverflow.com/questions/213761/what-are-some-uses-of-template-template-parameters. `switch_type_for_func` isnt the issue – 463035818_is_not_an_ai Jul 27 '22 at 07:55
  • Cool, I did not know about this, thanks, I will play around with it! – mikhail Jul 27 '22 at 12:19

4 Answers4

2

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

Nimrod
  • 2,908
  • 9
  • 20
  • The `foo_warapper` indeed looks nice, thanks for the suggestion! But: If I need to do this for any other library function (e.g. `bar()` in my examples, but you can imagine having a few more), I will need to duplicate the `*_helper()` code, which I would like to avoid. – mikhail Jul 27 '22 at 12:15
  • Yes, that's a bit pitiful. Passing a template function pointer is invalid, so you can wrap template functions into template structs to be passed into. I have no idea how to avoid the rest duplications... Maybe it's necessary. – Nimrod Jul 27 '22 at 14:45
  • 1
    @mikhail Take a look at my new method. Much more elegant now. – Nimrod Jul 30 '22 at 10:35
  • Nice, indeed look much better now! +1 line per new type and +1 visit call per new function to wrap. – mikhail Aug 02 '22 at 12:26
  • The only downside that I see compared to solution with a templated dispatcher _function_ is that this only works for direct 1-to-1 mapping logic (which is exactly what I need in my practical case, so perfect for me :D). If I wanted more complex logic somewhere, with ifs/etc., it would be difficult to squeeze into this solution. In any case, thanks for the suggestion, I upvote but keep the solution tick for the other option because it is more flexible in theory – mikhail Aug 02 '22 at 12:33
  • It's fair enough for me:) – Nimrod Aug 02 '22 at 12:48
1

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.

Max Langhof
  • 23,383
  • 5
  • 39
  • 72
  • 1
    For completeness, please add `return` inside the lambdas so that the return type would correspond to that of the wrapped functions :) You then also need a throw in case `switch` does not hit to make it compile, but that's trivial – mikhail Aug 02 '22 at 13:41
1

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

Patrick Roberts
  • 49,224
  • 10
  • 102
  • 153
0

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!

mikhail
  • 517
  • 5
  • 16