Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

c++

c++17

variant

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?

like image 766
Alex Avatar asked Nov 28 '19 21:11

Alex


2 Answers

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(); });
like image 94
StoryTeller - Unslander Monica Avatar answered Nov 14 '22 23:11

StoryTeller - Unslander Monica


std::variant's converting constructor behaves similar to overload resolution in order to determine which type to construct.

If you had two functions

void f(std::function< int (const std::vector<float>&, int) >);
void f(sstd::function< float (const std::vector<float>&, int) >);

then the call

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

would be ambiguous as well, because the constructor of std::function participates in overload resolution if the argument is callable with types const std::vector<float>& and int and return type implicitly convertible to int or float. The types are not required to be exactly the same.

Both overloads are therefore possible for your lambda and since each of them requires one user-defined conversion (from lambda type to std::function), the overload resolution is ambiguous.

like image 45
walnut Avatar answered Nov 14 '22 23:11

walnut