为什么std :: copy_if签名不限制谓词类型

12

想象一下我们有以下情况:

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
你的限制太强了(不需要const,一些转换是允许的(例如从intbool))。decltype可以允许正确的要求(或概念),但这种方法在C++11之前就已经完成了。 - Jarod42
2个回答

12

由于谓词不必是一个函数,而可以是一个函数对象。并且限制函数对象类型几乎是不可能的,因为只要它定义了 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 ()

然后算法内部将选择正确的重载。


这非常有趣。两个问题:(1) 为什么在这种情况下选择了正确的重载?(2) 是否可以将 operator() 设为静态的? - nyarlathotep108
2
@nyarlathotep108,广告(1),现在模板在一个包中获取了两个重载,并且选择发生在模板的深处,那里实际上有足够的信息来完成它。广告(2),是的,应该是这样,但这并没有什么区别,因为你仍然需要创建虚拟实例。 - Jan Hudec
而且,实际上,lambda表达式是基于函数对象定义的,只有非捕获lambda才有函数指针转换定义(你必须在某个地方存储捕获的值!) - JohannesD
@JohannesD,即非捕获非“auto”lambda。 - Yakk - Adam Nevraumont
@Yakk 没错,确实是这样。我只考虑了 C++11,因为问题没有标记 C++14。 - JohannesD

5
这个问题不仅影响到算法谓词,还会出现在模板类型推导中推导出重载函数的任何地方。模板类型推导发生在重载解析之前,因此编译器缺乏上下文信息来解决歧义。
正确编写的约束条件将非常复杂,因为它需要考虑参数和返回类型转换、绑定、lambda、functor、mem_fn等等。
解决歧义的简单方法(在我看来)是通过lambda调用谓词。
std::copy_if(a_source.begin(), a_source.end(), 
         std::back_inserter( a_target ), 
         [](auto&& x){ return predicate(std::forward<decltype(x)>(x)); });

这将推迟重载解析直到模板类型推导完成。

如果我(或我的老板)拒绝升级到c++14会怎样?

那么手动编写相同的lambda表达式:

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());

您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - Jan Hudec
@JanHudec 我想不出有哪个C++11编译器无法无痛升级到C++14。我的观点是,坚持使用C++11本身就是一种反模式。然而,一个带有模板调用运算符的函数对象就足够了。 - Richard Hodges
@JanHudec 为所有坚定不移的人提供了手动编写的模板函数对象。 - Richard Hodges

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接