Почему подпись std:: copy_if не ограничивает тип предиката

Предположим, что мы имеем следующую ситуацию:

struct A
{
    int i;
};

struct B
{
    A a;
    int other_things;
};

bool predicate( const A& a)
{
    return a.i > 123;
}

bool predicate( const B& b)
{
    return predicate(b.a);
}

int main()
{
    std::vector< A > a_source;
    std::vector< B > b_source;

    std::vector< A > a_target;
    std::vector< B > b_target;

    std::copy_if(a_source.begin(), a_source.end(), std::back_inserter( a_target ), predicate);
    std::copy_if(b_source.begin(), b_source.end(), std::back_inserter( b_target ), predicate);

    return 0;
}

Оба вызова std::copy_if генерируют ошибку компиляции, потому что правильная перегрузка функции predicate() не может быть выведена компилятором, поскольку подпись шаблона std::copy_if принимает любой тип предиката:

template<typename _IIter, 
         typename _OIter, 
         typename _Predicate>
_OIter copy_if( // etc...

Я нашел, что разрешение перегрузки работает, если я завершаю вызов std::copy_if в более ограниченную функцию шаблона:

template<typename _IIter, 
         typename _OIter, 
         typename _Predicate = bool( const typename std::iterator_traits<_IIter>::value_type& ) >
void copy_if( _IIter source_begin, 
              _IIter source_end, 
              _OIter target,  
              _Predicate pred)
{
    std::copy_if( source_begin, source_end, target, pred );
} 

Мой вопрос: почему в STL он уже не ограничен таким образом? Из того, что я видел, если тип _Predicate не является функцией, которая возвращает bool и принимает итерированный тип ввода, она все равно будет генерировать ошибку компилятора. Итак, почему бы не поставить этот ограничитель уже в сигнатуре, чтобы разрешить перегрузку?

Ответ 1

Поскольку предикат не должен быть функцией, но он может быть и функтором. И ограничение типа функтора почти невозможно, так как оно может быть вообще чем угодно, если оно определено operator().

На самом деле я предлагаю вам преобразовать перегруженную функцию в полиморфный функтор здесь:

struct predicate {
    bool operator()( const A& a) const
    {
        return a.i > 123;
    }

    bool operator()( const B& b) const
    {
        return operator()(b.a);
    }
}

и вызовите функтор с экземпляром, т.е.

std::copy_if(a_source.begin(), a_source.end(), std::back_inserter( a_target ), predicate());
std::copy_if(b_source.begin(), b_source.end(), std::back_inserter( b_target ), predicate());
//                                                                                      ^^ here, see the ()

Затем в алгоритме будет выбрана правильная перегрузка.

Ответ 2

Эта проблема не только влияет на предикаты алгоритмов. Это происходит везде, где вычитание типа шаблона выводит перегруженную функцию. Вывод типа шаблона происходит до разрешения перегрузки, поэтому компилятору не хватает контекстной информации для устранения неоднозначности.

Правильно написанное ограничение было бы ужасно сложным, так как ему нужно было бы учитывать преобразования аргументов и возвращаемых типов, привязки, lambdas, функторы, mem_fn и т.д.

Простым способом решения двусмысленности (ИМХО) является вызов предиката через лямбда.

std::copy_if(a_source.begin(), a_source.end(), 
         std::back_inserter( a_target ), 
         [](auto&& x){ return predicate(std::forward<decltype(x)>(x)); });

Это отклоняет разрешение перегрузки до тех пор, пока вы не выберете тип шаблона.

Что делать, если я откажусь (или мой босс откажется) до перехода на С++ 14

Затем выполните ручную печать той же лямбда:

struct predicate_caller
{
  template<class T>
  decltype(auto) operator()(T&& t) const 
  {
    return predicate(std::forward<T>(t));
  }
};

и вызывайте так:

std::copy_if(b_source.begin(), b_source.end(), 
             std::back_inserter( b_target ), 
             predicate_caller());