按引用传递的参数:这是std::thread和std::bind之间的不一致性吗?

15

std::bindstd::thread共享一些设计原则。由于它们都存储与传递参数对应的本地对象,如果需要引用语义,我们需要使用std::refstd::cref

void f(int& i, double d) { /*...*/ }

void g() {
    int x = 10;
    std::bind(f, std::ref(x), _1) (3.14);
    std::thread t1(f, std::ref(x), 3.14);
    //...
}

但是我对最近的个人发现感到好奇:即使这不是通常想要的情况,std::bind仍然允许你传递一个值。

    std::bind(f, x, _1) (3.14); // Usually wrong, but valid.

然而,这并不适用于std::thread。以下内容将会触发编译错误。
    std::thread t2(f, x, 3.14); // Usually wrong and invalid: Error!

乍一看我以为这是编译器的bug,但错误确实是合法的。似乎由于30.3.1.2所施加的复制衰减要求(将int&转换为int),模板化版本的std::thread构造函数无法正确推导参数。

问题是:为什么不要求类似于std::bind的参数?或者这个明显的不一致是有意的吗?

注意:在下面的评论中解释了为什么这不是重复的。


2
这不是重复的问题。那个问题解释了 我的 问题的前提是什么。此外,它没有涉及到 std::thread。事实上,我指出的是那里的一些评论的反例(关于 std::thread 的构造函数)。 - Leandro T. C. Melo
3个回答

10
bind返回的函数对象是为了重复使用而设计的(即可以被多次调用);因此,它必须将其绑定的参数作为lvalue传递,因为您不希望从这些参数移动或后续调用会看到已移动的绑定参数。 (同样,您也希望将函数对象作为一个lvalue来调用。)
但是,这个问题对于std::thread和相关工具是不适用的。线程函数只会被调用一次,并提供了参数。从它们移动是完全安全的,因为没有其他东西会查看它们。它们实际上是临时副本,仅用于新线程。因此,函数对象被调用为rvalue,参数被作为rvalue传递。

2
这是因为第一段中的原因,你可以将std::bind的结果用作有状态的函数对象,因为它具有可传递和修改的非静态数据成员,并在调用之间保留状态。对于被复制捕获的变量,可变lambda表达式具有类似的属性。这是设计上的考虑,而不是std::bind的缺陷。 - Jonathan Wakely

6
据我所知,thread 最初的规则与 bind 基本相同,但在2010年被 N3090 修改以承担您所确定的约束条件。
使用该方法来剖析各种贡献,我认为您正在寻找LWG 929问题。
具有讽刺意味的是,意图似乎是使 thread 构造函数的限制更少。当然,没有提到 bind,虽然后来也将此措辞应用于async ("Clean-up" section after LWG 1315),因此我会说 bind 被落下了。
不过这确实很难确定,所以我建议询问委员会本身。

6

std::bind的出现在很大程度上已经过时,因为有了Lambda表达式。随着C++14和C++17 std::apply的改进,bind 剩余的用例几乎都被淘汰了。

即使在C++11中,bind 解决问题的情况,也相对较少,Lambda表达式大多数情况下都可以胜任。

另一方面,std::thread解决了略微不同的问题,它并不需要像bind那样灵活地“解决每个问题”,而只需阻止通常会产生错误代码。

bind的情况下,传递给f的引用将不是x,而是对x内部存储副本的引用。这非常令人惊讶。

void f(int& x) {
    ++x;
    std::cout << x << '\n';
};

int main() {
    int x = 0;
    auto b = std::bind(f, x);
    b();
    b();
    b();
    std::cout << x << '\n';
}

打印
1
2
3
0

其中最后一个 0 表示原始的 x,而 123 是存储在 f 中的递增副本。

使用 lambda,可以清晰地区分可变存储状态和外部引用之间的差异。

auto b = [&x]{ f(x); };

vs

auto b = [x]()mutable{ f(x); };

其中一个复制了x,然后在其上重复调用f,另一个将对x的引用传递给f
使用bind实现这一点实际上是不可能的,除非允许f作为引用访问存储的x副本。
对于std::thread,如果您想要这种可变局部副本行为,只需使用lambda表达式即可。
std::thread t1([x]()mutable{ f(x); });

事实上,我认为C++11中大部分的INVOKE语法似乎是由于该语言没有C++14强大的lambda和std::apply而遗留下来的。在C++中,很少有情况无法通过lambda和std::apply来解决(需要apply是因为lambda不容易支持将包移入其中然后在内部提取它们)。
但是我们没有时间机器,所以我们在C++中有多种并行的方法来表达在特定上下文中调用某些东西的想法。

1
std::bind直到今天仍然不过时。这就是为什么它与lambda一起在C++11中被引入的原因。实际上,随着lambda的引入,像std::mem_fn或std::bind1st(std::bind2nd)这样的东西已经过时了。事实上,std::bind是C++中部分函数应用的唯一机制(https://en.wikipedia.org/wiki/Partial_application)。 - Leandro T. C. Melo
1
@LeandroT.C.Melo [x](auto&&...args)->decltype(auto){ return f(x, decltype(args)(args)...); } 我只是部分地将 x 应用到了 f 上,并没有使用 bind。更复杂的情况可以在更一般的情况下使用,比如绑定一组参数。 - Yakk - Adam Nevraumont
1
有趣的解决方案。不过我认为这个方案不能直接用于成员函数,对吧?无论如何,我应该说的是唯一的库组件。从“去糖”(de-sugaring)的角度来看,我们可以认为很多东西都已经“过时”了。 - Leandro T. C. Melo
1
@LeandroT.C.Melo [x,this]. 或者 [x,bob]{ return bob->f(x, decltype(args)(args...); },或者如果你需要完整的 INVOKE,std::invoke(f, x, decltype(args)(args)...); }。重点是 lambda 很好地绑定了参数并创建了即时代码。只有在大规模泛型的情况下才会变得冗长,而 std::bind 由于其简单疯狂的递归怪癖,在完全通用的情况下无法工作(boost::bind 有一个洗衣系统(忘记叫什么了)来摆脱这种怪癖,但 std 没有)。更不用说重载集问题了! - Yakk - Adam Nevraumont
2
当然,我同意。但是你需要一个不同的lambda表达式。这就是库组件的作用。并不是说std::bind是完美的,但这就是我们所拥有的。 - Leandro T. C. Melo

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