C++ std::function type and templates

TL;DR std::function works great when we know exact types of the arguments and the return value. It breaks down miserably in presence of templates, and the error messages are not great, to put it mildly. I wasted quite a few minutes today because of this problem in our production code.

Consider this example. In C++, lambda has an unspecified implementation-dependent type, but we can pass it to as a getKey parameter, because std::function has a templated constructor that can take any callable type. The compiler knows what type is required, and performs implicit conversion for us.

#include <string>
#include <vector>
#include <unordered_map>
#include <functional>
#include <iostream>

using namespace std;

auto to_map(vector<string> const& items, function<int(string const&)> getKey) {
  unordered_map<int, string> map;
  for (auto const& item : items) {
    map[getKey(item)] = item;
  }
  return map;
}

int main()
{
  vector<string> v = {"a", "bb", "ccc"};
  auto map = to_map(v, [](auto const& s) { return s.size(); });
  cout<< map[3] << endl; // prints 'ccc'
  return 0;
}

Unfortunately, it all breaks down if we switch to templates:

// includes skipped for brevity

template<class TKey, class TValue>
auto to_map(vector<TValue> const& items, function<TKey(TValue const&)> getKey) {
  unordered_map<TKey, TValue> map;
  for (auto const& item : items) {
    map[getKey(item)] = item;
  }
  return map;
}

int main()
{
  vector<string> v = {"a", "bb", "ccc"};
  // the line below does not compile with a crytpic message
  // ‘main()::&)>’ is not derived from ‘std::function’
  auto map = to_map(v, [](auto const& s) { return s.size(); }); // not good

  cout<< map[3] << endl; // prints 'ccc'
  return 0;
}

The compiler no longer knows the exact type of the getKey parameter, so implicit conversion is not possible. Unlike languages like C#, in C++ a lambda is not a function, it is some hidden implementation-specific type. We can add an explicit conversion, but then we lose the benefit of automatic type deduction, and the code becomes even more verbose than usual:

function<int(string const&)> getKey = [](auto const& s) { return s.size(); };
auto map = to_map(v, getKey);

Alternatively, we can make function type itself a template parameter:

// includes skipped for brevity

template<class TGetKeyFunction, class TValue>
auto to_map(vector<TValue> const& items, TGetKeyFunction getKey) {
  using TKey = decltype(getKey(items[0]));
  unordered_map<TKey, TValue> map;
  for (auto const& item : items) {
    map[getKey(item)] = item;
  }
  return map;
}

int main()
{
  vector<string> v = {"a", "bb", "ccc"};
  auto map = to_map(v, [](auto const& s) { return s.size(); }); // works again!

  cout<< map[3] << endl; // prints 'ccc'
  return 0;
}

Note that now we lose all type safety inside the template and any errors related to calling getKey will arise at the time of instantiation. We also lose easy access to the result type and the argument types. The result type still can be retrieved using the decltype trick, but we’re out of luck with argument types.

It is a little said that C++ does not have a good representation of a functional object that works in all contexts, but it’s hardly surprising given the history of the language.

Leave a Reply

Your email address will not be published. Required fields are marked *