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

2

5

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?

Stortz answered 28/11, 2019 at 21:3 Comment(0)
P
5

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(); });
Prudence answered 28/11, 2019 at 21:18 Comment(0)
B
2

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.

Butyraceous answered 28/11, 2019 at 21:20 Comment(2)
Thanks a lot for the great answer! It would have been even better if you had shown how to fix this.Stortz
@Stortz A clean solution that doesn't require modification of your emplace_back use would probably be to write a wrapper class around std::function that enables its constructor only if types match exactly. I don't have time to write that out right now.Butyraceous

© 2022 - 2024 — McMap. All rights reserved.