Неужели в C ++ лямбды неправильно выбирают перегруженные функции? - PullRequest
12 голосов
/ 18 февраля 2012

У меня есть функция, которая перебирает контейнер и передает каждый элемент в предикат для фильтрации. Перегрузка этой функции также передает индекс каждого элемента в предикат.

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»?

Ответы [ 2 ]

12 голосов
/ 18 февраля 2012

Ожидается неоднозначность перегрузки.

std::function имеет шаблон конструктора преобразования, который принимает любой аргумент. Только после создания шаблона конструктора компилятор может определить, что он отклонит аргумент.

Как в первом, так и во втором примерах пользовательское преобразование требуется для преобразования неопределенного лямбда-типа в каждый из типов std::function. Ни одно из преобразований не является лучшим (оба они являются преобразованиями, определяемыми пользователем), поэтому компилятор сообщает о неоднозначности перегрузки.

В вашем третьем примере (тот, который работает) нет никакой двусмысленности, потому что шаблон конструктора std::function не используется. Вместо этого используется его конструктор копирования (и, при прочих равных, не шаблоны предпочтительнее шаблонов).

5 голосов
/ 18 февраля 2012

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 возвратах. Я не ставлю здесь специализацию, так как этот ответ достаточно длинный.

Если вы находите это решение мощным, но слишком многословным, возможно, вам понравятся концепции:)

...