【问题标题】:How to `std::bind()` a standard library algorithm?如何`std::bind()`一个标准库算法?
【发布时间】:2014-12-29 18:25:19
【问题描述】:

我的问题的简短版本是这样的:如何将std::bind() 之类的东西与标准库算法一起使用?

由于简短版本有点缺乏细节,这里有点解释:假设我有算法std::transform(),现在我想实现std::copy()(是的,我意识到有std::copy()在标准 C++ 库中)。由于我非常懒惰,我显然想使用std::transform() 的现有实现。当然,我可以这样做:

struct identity {
    template <typename T>
    auto operator()(T&& value) const -> T&& { return std::forward<T>(value); }
};  
template <typename InIt, typename OutIt>
auto copy(InIt begin, InIt end, OutIt to) -> OutIt {
    return std::transform(begin, end, to, identity());
}

不知何故,这个实现有点像算法的配置。例如,似乎std::bind() 应该能够完成这项工作,但仅仅使用std::bind() 是行不通的:

namespace P = std::placeholders;
auto copy = std::bind(std::transform, P::_1, P::_2, P::_3, identity());

问题在于编译器不能仅从算法中确定适当的模板参数,是否存在&amp; 无关紧要。有什么东西可以使使用std::bind() 之类的方法起作用吗?由于这是前瞻性的,我很高兴有一个解决方案可以处理已经提议包含在 C++ 标准中的任何内容。此外,为了摆脱我的懒惰,我很乐意在前面做一些工作,以便以后使用。可以这样想:作为库实施者,我将把所有东西放在一起,这样每个库用户都可以偷懒:我是一个忙碌的实施者但是一个懒惰的用户。

如果您想要一个现成的测试台:这里有一个完整的程序。

#include <algorithm>
#include <functional>
#include <iostream>
#include <iterator>
#include <utility>
#include <vector>

using namespace std::placeholders;

struct identity {
    template <typename T>
    T&& operator()(T&& value) const { return std::forward<T>(value); }
};


int main()
{
    std::vector<int> source{ 0, 1, 2, 3, 4, 5, 6 };
    std::vector<int> target;

#ifdef WORKS
    std::transform(source.begin(), source.end(), std::back_inserter(target),
                   identity());
#else
    // the next line doesn't work and needs to be replaced by some magic
    auto copy = std::bind(&std::transform, _1, _2, _3, identity());
    copy(source.begin(), source.end(), std::back_inserter(target));
#endif
    std::copy(target.begin(), target.end(), std::ostream_iterator<int>(std::cout, " "));
    std::cout << "\n";
}

【问题讨论】:

  • @πάνταῥεῖ:嘿 - 这是一个真正的问题......! (当然,双重目标确实起作用)
  • 标签组合是什么让我怀疑 ;-) ...(当然这是个好问题)
  • 通用 lambdas 几乎是我唯一能想到的,但我认为这还不够 bind-like...
  • 您一定听说过 C++14 技术通过泛型 lambda 传递重载集。这种方法有什么问题或不足?
  • @dyp: 你想使用吗?啧啧……!但是,是的,这更接近我的想法。另外,我目前正在尝试完成类似的工作,但我使用的实现似乎对转发占位符并不完全满意。

标签: c++ c++11 c++14 c++17


【解决方案1】:

当尝试std::bind() 重载函数时,编译器无法确定要使用哪个重载:在评估bind()-表达式时,函数参数是未知的,即重载决议无法决定哪个重载选择。 C++ [还没有?] 中没有直接的方法将重载集视为对象。函数模板只是为每个可能的实例化生成一个重载集和一个重载集。也就是说,无法std::bind() 任何标准 C++ 库算法的整个问题都围绕着标准库算法是函数模板这一事实。

std::bind()算法具有相同效果的一种方法是使用 C++14 generic lambdas 进行绑定,例如:

auto copy = [](auto&&... args){
    return std::transform(std::forward<decltype(args)>(args)..., identity());
};

虽然这可行,但它实际上相当于函数模板的花哨实现,而不是配置现有函数。但是,使用通用 lambda 在合适的标准库命名空间中创建主要函数对象可以使实际的底层函数对象随时可用,例如:

namespace nstd {
    auto const transform = [](auto&&... args){
        return std::transform(std::forward<decltype(args)>(args...));
    };
}

现在,通过实现transform() 的方法,使用std::bind() 构建copy() 实际上是微不足道的:

auto copy = std::bind(nstd::transform, P::_1, P::_2, P::_3, identity());

尽管看起来和使用通用 lambda,但值得指出的是,仅使用 C++11 可用的功能来创建相应的函数对象实际上需要大致相同的努力:

struct transform_t {
    template <typename... Args>
    auto operator()(Args&&... args) const
        -> decltype(std::transform(std::forward<decltype(args)>(args)...)) {
        return std::transform(std::forward<decltype(args)>(args)...);
    }
};
constexpr transform_t transform{};

是的,它需要更多类型,但它只是使用通用 lambda 的一个合理的小常数因素,即,如果对象使用通用 lambda,C++11 版本也是。

当然,一旦我们有了算法的函数对象,实际上甚至不必std::bind() 它们可能是整洁的,因为我们需要提及所有未绑定的参数。在示例情况下,它是currying(好吧,我认为currying 仅适用于绑定第一个参数,但它是第一个参数还是最后一个参数似乎有点随机)。如果我们让curry_first()curry_last() 柯里化第一个或最后一个参数怎么办? curry_last() 的实现也很简单(为简洁起见,我使用的是通用 lambda,但可以使用与上面相同的重写使其可用于 C++11):

template <typename Fun, typename Bound>
auto curry_last(Fun&& fun, Bound&& bound) {
    return [fun = std::forward<Fun>(fun),
            bound = std::forward<Bound>(bound)](auto&&... args){
        return fun(std::forward<decltype(args)>(args)..., bound);
    };
}

现在,假设 curry_last() 位于同一命名空间中,nstd::transformidentity() 的定义可能变为:

auto const copy = curry_last(nstd::transform, identity());

好的,也许这个问题对我没有任何帮助,但也许我会得到一些支持,将我们的标准库算法转换为函数对象,并可能添加一些很酷的方法来创建所述算法的绑定版本。我认为这种方法比该领域的一些proposals 更理智(尽管上述形式可能不完整)。

【讨论】:

  • 非标准扩展(即使是好的扩展)的存在并不意味着它在 C++ 中是必需的! (标准不需要无状态 lambda 上的 constexpr 创建/移动/复制构造函数。如果我没记错的话,也不排除)
  • @Yakk:是的,我被一个扩展欺骗了(实际上只在 gcc 中而不是在 clang 中)。
  • @Yakk lambdas 可能不会出现在常量表达式中,我认为这排除了“允许但不可移植”
  • @dyp:是的,你是对的。我将它们更改为const。不过,我不太喜欢这样:无法在正常初始化之前构造,这意味着即使使用 C++14,也需要使用使用适当函数对象而不是通用 lambda 的方法...
  • @Yakk 闭包类型明确不是每个 [expr.prim.lambda]/p3 的文字类型,constexpr 对象声明需要文字类型 ([dcl.constexpr]/p9)。
猜你喜欢
  • 1970-01-01
  • 2015-11-11
  • 2017-03-19
  • 1970-01-01
  • 2013-11-15
  • 2014-10-06
  • 2021-04-04
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多