【问题标题】:Why does the std::copy_if signature not constrain the predicate type为什么 std::copy_if 签名不约束谓词类型
【发布时间】:2017-01-26 22:55:16
【问题描述】:

假设我们有以下情况:

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 并接受迭代输入类型的函数,那么无论如何它都会生成编译器错误。那么为什么不把这个约束放在签名中,以便重载解析可以工作呢?

【问题讨论】:

  • 您的约束太强(const 不需要,允许进行一些转换(intbool))。 decltype 将允许正确的要求(或概念),但该方法是在 c++11 之前完成的。

标签: c++ c++11 stl stl-algorithm


【解决方案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 ()

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

【讨论】:

  • 这很有趣。两个问题:(1)为什么在这种情况下选择正确的重载? (2) 是否可以将operator() 设为静态?
  • @nyarlathotep108, ad (1),模板现在在一个包中获得了两个重载,并且选择发生在模板深处,实际上有足够的信息来执行它。广告(2),是的,应该是,但它并没有太大区别,因为无论如何您都需要创建虚拟实例。
  • 而且,实际上,lambdas 是根据函数对象定义的,只有非捕获 lambdas 才能转换为定义的函数指针(您必须将捕获的值存储在某个地方!)
  • @JohannesD 非捕获非auto lambdas 即。
  • @Yakk 没错,确实。我只考虑 C++11,因为问题没有 C++14 标签。
【解决方案2】:

这个问题不仅仅影响算法的谓词。它发生在模板类型推导推导出重载函数的任何地方。模板类型推导发生在重载解析之前,因此编译器缺少上下文信息来解决歧义。

正确编写的约束会非常复杂,因为它需要考虑参数和返回类型转换、绑定、lambda、仿函数、mem_fns 等等。

解决歧义(恕我直言)的一种简单方法是通过 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());

【讨论】:

  • 然而,这仅适用于 C++14 通用 lambda。哪个足够新,您的本地编译器可能还没有它。
  • @JanHudec 我想不出一个没有轻松升级到 c++14 的 c++11 编译器。我的观点是,坚持使用 c++11 本身就是一种反模式。但是,具有模板化调用运算符的函数对象就足够了。
  • @JanHudec 为所有顽固分子提供的手动模板仿函数。
猜你喜欢
  • 2021-08-26
  • 1970-01-01
  • 1970-01-01
  • 2010-11-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多