Search code examples
c++templatesfunctional-programmingc++17c++20

What is the idiomatic way to create a U to V mapper function template in C++?


I am learning about C++ right now, and trying out some of the templating features. I am trying to create a generic template that receives a function F from U to V, a std::array of type U, and then returns (via NRVO) an array of type V.

This is what I came up with on a first pass. It seems reasonable to my untrained eye, but the compiler doesn't like it:

template <typename F, typename U, typename V, std::size_t N> std::array<V, N> map(F mapper, const std::array<U, N> &elements)
{
  std::array<V, N> newArray{};
  for (std::size_t i = 0; i < N; i++) {
    newArray[i] = mapper(elements[i]);
  }
  return newArray;
}

// ...

int main()
{
  std::array<int, 10> ints1 = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
  auto doubles1 = map([](int x) -> double { return x*2.5; }, ints1);
}

I am using clang 16.0.6, and it tells me:

functional.cpp:87:19: error: no matching function for call to 'map'
  auto doubles1 = map([](int x) -> double { return x*2.5; }, ints1);
                  ^~~~~~~~~~~~
functional.cpp:22:79: note: candidate template ignored: couldn't infer template argument 'V'
template <typename F, typename U, typename V, std::size_t N> std::array<V, N> map(F mapper, std::array<U, N> &elements)
                                                                              ^
1 error generated.

To my mind, it seems like there should be something I can do to hint to the compiler that the lambda returns a double, and hence the auto should be deduced as std::array<double, N>. Is there something easy I am missing? Thank you.

I expected the code to compile of course, but something is not quite right. I am still pretty new to C++ lingo, so I am having trouble knowing what exactly to google to resolve this. One possible solution I came up with was to use an out-parameter:

template <typename F, typename U, typename V, std::size_t N> void map_outParam(F mapper, std::array<U, N> &elements, std::array<V, N> &out)
{
  for (std::size_t i = 0; i < N; i++) {
    out[i] = mapper(elements[i]);
  }
}

This compiles just fine, and works as expected. However, it's not as clean as I would like.


Solution

  • Basically, you can just use the result type of your callback function to figure out the type for the result array.

    #include <type_traits>
    #include <array>
    
    template <typename F, typename U, std::size_t N> auto map(F mapper, std::array<U, N> &elements)
    {
      std::array<std::invoke_result_t<F, U>, N> newArray{};
      for (std::size_t i = 0; i < N; i++) {
        newArray[i] = mapper(elements[i]);
      }
      return newArray;
    }
    
    // ...
    
    int main()
    {
      std::array<int, 10> ints1 = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
      auto doubles1 = map([](int x) -> double { return x*2.5; }, ints1);
    }