Can I forward (the type of) a generic overloaded function without creating syntax monsters?

Viewed 111

This is just an educational question, which could otherwise be avoided entirely by leveraging on tools such as range-v3.

Anyway, consider this example in which a variadic number of containers is passed to a function which returns a tuple of iterators:

#include <tuple>
#include <iostream>
#include <vector>

template <typename ...T> 
auto begin(T&... containers) { return std::tuple( begin(containers)... ); }

int main() {
  std::vector<int> a{1,2,3};
  std::vector<long> b{1000,2000,3000};

  auto bg = begin(a,b);
  (void) bg;
  return 0;
}

Quite a lot of code duplication will be produced by adding the functions for all the other iterator makers (std::[end, cbegin, cend, rbegin, rend]). Therefore I was looking for a way to forward the generic iterator maker function into my function. I managed to come up with this:

template <auto F, typename ...T>
auto make(T&... containers) { return std::tuple( F(containers)... ); }
// called as this:
auto bg = make<[](auto& c){return std::begin(c);}> (a,b);

...which is more general but comes with an horrendous syntax for the user, Ideally the call should be something like:

auto bg = make<std::begin>(a, b); // or
auto bg = make(std::begin, a, b);

but I have not been able to make these beauties work...

3 Answers

but I have not been able to make these beauties work...

There is no way to make those beauties work currently. The root of it all is that std::begin et al are not functions. They are function templates. Meaning they represent not a single function but an entire family of them. The same is true when you have regular overloads as well. The moment a function name means more than a single function, it can't be passed. An overload set is not a tangible thing we can pass around like a type or a function reference. It's not a callable really.

Wrapping the actual overloaded call into a lambda bypasses the issue entirely. Because a lambda is an object with a type, and those can be passed around just fine. This essentially lifts the overload set up from being a second class citizen. But it comes with boilerplate.

There was a proposal made (p1170) to automatically lift an overload set into a callable, but so far it didn't seem to gain traction. So C++20 doesn't have the means.

As for the boilerplate, we can cut on it if we are willing to employ macros. A simple macro that lifts an overload set correctly can look something like this:

#define LIFT(...) [](auto&& ...args)                                           \
        noexcpet(noexcpet(__VA_ARGS__(std::forward<decltype(args)>(args)...))) \
     -> decltype(auto) {                                                       \
    return __VA_ARGS__(std::forward<decltype(args)>(args)...);                 \
}

Granted, that's a lot of boilerplate by itself, but it handles the exception specification, as well as return type deduction for functions that don't return by value. It also perfect forwards. So while it is indeed fairly ugly, it allows us to write:

auto bg = make(LIFT(std::begin), a, b);

You could wrap those functions within lambdas so you could use it in the next fashion

make(std::begin, a, b);

i.e

template <typename F, typename ...T> 
decltype(auto) make(F func, T&... containers) { return std::tuple( func(containers)... ); }

namespace tl {

auto begin = [](auto& c) {
    return std::begin(c);
};

}

int main() {
  std::vector<int> a{1,2,3};
  std::vector<long> b{1000,2000,3000};

  auto bg = make(tl::begin, a, b);
  (void) bg;

  cout << *++std::get<0>(bg) << ' ' << *std::get<1>(bg);

  return 0;
}

Depending on your definition of monster, this is easily achievable:

int main() 
{
    auto v1 = std::vector{1,2,3};
    auto v2 = std::vector{4,5,6};

    auto begins = std::tie(v1, v2) >> notstd::begin;
    auto ends = std::tie(v1, v2) >> notstd::end;

  return 0;
}

Here's the boilerplate:

#include <vector>
#include <tuple>
#include <utility>

namespace notstd
{
    namespace detail
    {
        template<class Tuple, class Function, std::size_t...Is>
        auto transform_elements(Tuple&& tup, Function f, std::index_sequence<Is...>)
        {
            return std::make_tuple(f(std::get<Is>(std::forward<Tuple>(tup)))...);
        }
    }

    template<class Tuple, class Transform>
    auto operator >> (Tuple&& tup, Transform t)
    {
        using tuple_type = std::decay_t<Tuple>;

        constexpr auto size = std::tuple_size<tuple_type>::value;

        return detail::transform_elements(std::forward<Tuple>(tup), 
                                          t, 
                                          std::make_index_sequence<size>());
    }

    constexpr auto begin = [](auto&& x)
    {
        return std::begin(x);
    };

    constexpr auto end = [](auto&& x)
    {
        return std::end(x);
    };
}

This is by no means a style recommendation.

Related