Нужно ли С++ lambdas правильно выбирать перегруженные функции?
У меня есть функция, которая выполняет итерацию над контейнером и передает каждый элемент в предикат для фильтрации. Перегрузка этой функции также передает индекс каждого элемента в предикат.
template<typename TContainer>
void DoSomethingIf(TContainer &c, std::function<bool (const typename TContainer::const_reference)> predicate);
template<typename TContainer>
void DoSomethingIf(TContainer &c, std::function<bool (const typename TContainer::const_reference, int)> predicate);
Я обнаружил, что попытка вызвать любую из этих функций с обнаженной лямбда вызовет ошибку компилятора в VC11, в то время как использование объекта std:: function будет успешным:
void foo()
{
std::vector<int> v;
// fails
DoSomethingIf(v, [](const int &x) { return x == 0; });
// also fails
auto lambda = [](const int &x) { return x == 0; };
DoSomethingIf(v, lambda);
// success!
std::function<bool (const int &)> fn = [](const int &x) { return x == 0; };
DoSomethingIf(v, fn);
}
1>c:\users\moswald\test.cpp(15): error C2668: 'DoSomethingIf' : ambiguous call to overloaded function
1> c:\users\moswald\test.cpp(8): could be 'void DoSomethingIf<std::vector<_Ty>>(TContainer &,std::function<_Fty>)'
1> with
1> [
1> _Ty=int,
1> TContainer=std::vector<int>,
1> _Fty=bool (const int &,int)
1> ]
1> c:\users\moswald\test.cpp(5): or 'void DoSomethingIf<std::vector<_Ty>>(TContainer &,std::function<_Fty>)'
1> with
1> [
1> _Ty=int,
1> TContainer=std::vector<int>,
1> _Fty=bool (const int &)
1> ]
1> while trying to match the argument list '(std::vector<_Ty>, foo::<lambda_8EADDE04A8D35A3C>)'
1> with
1> [
1> _Ty=int
1> ]
1>c:\users\moswald\test.cpp(19): error C2668: 'DoSomethingIf' : ambiguous call to overloaded function
1> c:\users\moswald\test.cpp(8): could be 'void DoSomethingIf<std::vector<_Ty>>(TContainer &,std::function<_Fty>)'
1> with
1> [
1> _Ty=int,
1> TContainer=std::vector<int>,
1> _Fty=bool (const int &,int)
1> ]
1> c:\users\moswald\test.cpp(5): or 'void DoSomethingIf<std::vector<_Ty>>(TContainer &,std::function<_Fty>)'
1> with
1> [
1> _Ty=int,
1> TContainer=std::vector<int>,
1> _Fty=bool (const int &)
1> ]
1> while trying to match the argument list '(std::vector<_Ty>, foo::<lambda_8EADDE04A8D35A3D>)'
1> with
1> [
1> _Ty=int
1> ]
Можно ли это ожидать? Есть ли другой способ перегрузить эти функции (не дожидаясь переименования, чтобы быть "DoSomethingIfWithIndex
"?
Ответы
Ответ 1
Ожидается неопределенность перегрузки.
std::function
имеет шаблон конструктора преобразования, который принимает любой аргумент. Только после создания экземпляра шаблона конструктора компилятор может определить, что он отклонит аргумент.
В обоих первом и втором примерах требуется преобразование, определенное пользователем, для преобразования неопределенного лямбда-типа в каждый из типов std::function
. Ни одно преобразование не лучше (они являются как пользовательскими преобразованиями), поэтому компилятор сообщает о неоднозначности перегрузки.
В вашем третьем примере (том, который работает), нет никакой двусмысленности, потому что шаблон конструктора std::function
не используется. Вместо этого используется его конструктор копирования (и, при прочих равных условиях, nontemplates предпочтительнее шаблонов).
Ответ 2
std::function
имеет свои применения в двоичных разграничениях, но не как параметр общего использования для функторов. Как вы только что узнали, его конструктор преобразования плохо взаимодействует с разрешением перегрузки (и это не имеет ничего общего с лямбда-выражениями). Поскольку DoSomethingIf
уже является шаблоном, я не вижу проблемы с каноническим решением принятия обобщенных функторов:
template<typename TContainer, typename Predicate>
void DoSomethingIf(TContainer& c, Predicate&& predicate);
Как вы можете заметить, эта версия не может быть перегружена и просто примет что-либо как предикат, даже int
. Перегрузка легко разрешается с помощью SFINAE, как обычно:
template<
typename Container
, typename Predicate
, typename = typename std::enable_if<
is_callable<Predicate, bool(typename Container::const_reference)>::value
>::type
>
void
DoSomethingIf(Container& container, Predicate&& predicate);
template<
typename Container
, typename Predicate
, typename = typename std::enable_if<
is_callable<Predicate, bool(typename Container::const_reference, int)>::value
>::type
// dummy parameter to disambiguate this definition from the previous one
, typename = void
>
void
DoSomethingIf(Container& container, Predicate&& predicate);
Это все еще вызывает раздражающую проблему: если кто-то передает предикат (или действительно что-либо), который не удовлетворяет нашим условиям, мы получаем ошибку "отсутствие совпадающей функции" (ошибка разрешения перегрузки), а не полезную ошибку. Если вы хотите решить эту проблему, вы можете добавить перегрузку "catch-all":
template<
typename Container
, typename Predicate
, typename = typename std::enable_if<
!is_callable<Predicate, bool(typename Container::const_reference)>::value
&& !is_callable<Predicate, bool(typename Container::const_reference, int)>::value
>::type
// more dummies
, typename = void, typename = void
>
void DoSomethingIf(Container&, Predicate&&)
{ static_assert( dependent_false_type<Container>::value,
"Put useful error message here" ); }
(dependent_false_type
просто должен быть, например, типом, наследующим от std::false_type
, мы не можем static_assert
просто false
или это будет срабатывать каждый раз, а не только при создании экземпляра шаблона, поскольку мы В качестве альтернативы вы можете повторить условие, которое у нас есть внутри std::enable_if
здесь, которое работает как документация внутри кода, но не улучшает сама функциональность.)
Осталось только найти is_callable<Functor, Signature>
, так как это не стандартная черта. Это относительно легко реализовать, если вы когда-либо писали тест SFINAE раньше, но немного утомительны, поскольку вам нужно частично специализироваться на возврате void
. Я не ставил специализацию здесь, так как этот ответ достаточно длинный, как есть.
Если вы найдете это решение мощным, но слишком подробным, то, возможно, вам понравятся концепции:)