Search code examples
c++c++17variant

"No matching function for call" error when creating a vector of function variants


I'm trying to create a std::vector that can hold std::function objects of different signatures using std::variant.

Why does the following code not compile:

#include <functional>
#include <variant>
#include <vector>

int main()
{
  std::vector<std::variant<
      std::function< int (const std::vector<float>&, int) >,
      std::function< float (const std::vector<float>&, int) >
  >> func_vector;

  func_vector.emplace_back( [] (const std::vector<float>& ret, int index) { return ret.size(); });

  return 0;
}

The problem happens during the emplace_back(). Compiling this gives a long list of errors, the first one listed being:

error: no matching function for call to ‘std::variant<std::function<int(const std::vector<float, std::allocator<float> >&, int)>, std::function<float(const std::vector<float, std::allocator<float> >&, int)> >::variant(main()::<lambda(const std::vector<float>&, int)>)’
  { ::new((void *)__p) _Up(std::forward<_Args>(__args)...); }
    ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

It says that it cannot find a matching function, but for what call exactly?

The lambda I'm trying to emplace has exactly the signature of one of the types I specified in the variant, so all should be fine, shouldn't it?


Solution

  • emplace_back should forward the lambda directly to the variant initialization. And there is a converting constructor that can initialize a member of the variant from any argument convertible to the member type. The issue, however, is that both members of the variant can be initialized from this lambda, creating an ambiguity.

    Yes, your lambda is a valid initializer for a std::function< float (const std::vector<float>&, int) >. This is due to the way a std::function performs type erasure. It casts the result of the callable it holds to the return type it is specified with. The callable just has to be able to accept the argument list of the std::function.

    To illustrate this, if we were to add a third argument to one of the std::function types,

    std::vector<std::variant<
      std::function< int (const std::vector<float>&, int) >,
      std::function< float (const std::vector<float>&, int, int) >
    >> func_vector;
    

    then there would be no ambiguity. The lambda is a valid initializer for only one variant member now.

    The workarounds are to either cast to the exact function type you wish to hold, or tell the emplaced variant which option it should initialize, for instance:

    func_vector.emplace_back( std::in_place_index<0>, [] (const std::vector<float>& ret, int ) { return ret.size(); });