带有指向数据成员的std::thread

5
我在阅读 cppreference 的 std::thread 文档时(我知道它并不总是百分之百正确),注意到当将“指向数据成员”的指针(而不是“指向成员函数”的指针)作为其第一个参数(f)传递给 std::thread 并将所需类的对象作为其第二个参数(复制到线程局部存储后的 t1)时,std::thread 表现的定义如下: 如果 N == 1 且 f 是指向类的成员数据对象的指针,则可以访问该成员。对象的值被忽略。实际上,执行以下代码: - 如果 t1 的类型是 T、T 的引用或派生自 T 的类型,则执行 t1.*f。 - 否则执行 (*t1).*f。 现在,我并不打算以这种方式使用 std::thread,但是我被这个定义搞糊涂了。显然,唯一发生的事情是访问了数据成员并忽略了其值,这似乎根本没有任何可观察的副作用,也就是说(据我所知),它可能是一个无操作。(我可能错过了一些显而易见的东西……?)
一开始,我以为这可能是打印错误,并且意思是访问数据成员然后调用它(因为它可能是一个可调用对象,即使它不是一个函数),但我在GCC-4.7中使用以下代码测试了一下,确实没有调用:
#include <iostream>
#include <thread>

struct S 
{
    void f() {
        std::cout << "Calling f()" << std::endl;
    }

    struct {
        void operator()() {
            std::cout << "Calling g()" << std::endl;
        }
    } g;
};

int main(int, char**)
{
    S s;
    s.f(); // prints "Calling f()"
    s.g(); // prints "Calling g()"

    std::cout << "----" << std::endl;

    auto x = &S::f; // ptr-to-mem-func
    auto y = &S::g; // ptr-to-data-mem

    (s.*x)(); // prints "Calling f()"
    (s.*y)(); // prints "Calling g()"

    std::cout << "----" << std::endl;

    std::thread t(x, &s);
    t.join();
    // "Calling f()" printed by now
    std::thread u(y, &s);
    u.join();
    // "Calling g()" not printed

    return 0;
}

这个定义似乎没有任何作用,是否可以将传递“指向数据成员可调用”的行为与“指向成员函数”的行为相同,将传递“指向数据成员不可调用”的行为视为错误?实际上,这似乎是最容易实现的方式,因为在其他上下文中调用“指向数据成员可调用”具有与调用“指向成员函数”的等效语法(除非模板特化和SFINAE规则中存在某些复杂性使它们难以等效处理...?)。我不需要实际代码,但这个定义的存在让我怀疑自己是否缺少一些基本的东西,这让我担心...有人能给我解释一下吗?

+1 这个措辞来自于标准的 20.8.2 节中所给出的 INVOKE 定义。有一个与您意图类似的问题,特别是针对 INVOKE 定义中这个措辞的问题:https://dev59.com/02jWa4cB1Zd3GeqPv_k3。 - jogojapan
1个回答

3
这是因为C++11标准中的通用绑定机制,它不仅定义了如何启动线程,还定义了std::bindstd::function的工作方式。

事实上,C++11标准的第30.3.1.2/3段规定了类std::thread的可变参数构造函数:

template <class F, class ...Args> explicit thread(F&& f, Args&&... args);

效果:构造一个类型为thread的对象。新的执行线程使用INVOKE (DECAY_- COPY ( std::forward<F>(f)), DECAY_COPY (std::forward<Args>(args))...)进行调用, 调用DECAY_COPY在构造线程中被评估。此调用的任何返回值都将被忽略。[...]

忽略DECAY_COPY的作用(它与问题无关),这就是第20.8.2段如何定义INVOKE伪函数的方式:
定义 INVOKE (f, t1, t2, ..., tN) 如下所示:
  • 当 f 是类 T 的成员函数指针,t1 是类型为 T 的对象或类型为 T 或从 T 派生的对象的引用时,调用 (t1.*f)(t2, ..., tN);
  • 当 f 是类 T 的成员函数指针,t1 不是上一条中描述的类型之一时,调用 ((*t1).*f)(t2, ..., tN);
  • 当 N == 1 且 f 是类 T 的成员数据指针,t1 是类型为 T 的对象或类型为 T 或从 T 派生的对象的引用时,调用 t1.*f;
  • 当 N == 1 且 f 是类 T 的成员数据指针,t1 不是上一条中描述的类型之一时,调用 (*t1).*f;
  • 在所有其他情况下,调用 f(t1, t2, ..., tN)。
现在问题变成了:
为什么C++11标准这样定义INVOKE设施呢?答案在这里

1
谢谢,我现在明白这种行为是从哪里来的了,但我仍然不明白为什么...为什么你要以那种方式定义绑定到成员数据?只是为了使用std::mem_fn检索数据成员吗?如果我绑定f = std::mem_fn(&X::n),然后调用f(&x),我会期望出现错误或者x.n(),而不是x.n...也许有一种有效的用例可以通过绑定到成员数据实现,但为什么不通过名为std::mem_data的东西呢? - Stephen Lin
@StephenLin:虽然我理解你的观点,但我认为SO不适合那种“为什么事情是这样而不是其他方式?”的问题。我试图回答可以客观回答的部分。希望会有更好的答案。 - Andy Prowl
没问题,我问的唯一原因是因为你将那个答案链接作为解释,但它似乎并没有解释任何东西(除了某些人可能会发现这种过载行为有用...) - Stephen Lin
@AndyProwl 选择这个的原因可能仍然是客观存在的。这些原因很可能可以在标准提案、委员会成员的评论或其他地方找到。 - jogojapan

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