Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

std::thread with pointer to data member

Tags:

c++

std

c++11

I was reading through the std::thread documentation at cppreference (not always 100% accurate, I know) and noticed the following definition for the behavior of std::thread when passed a "pointer-to-data-member" (not "pointer-to-member-function") as its first argument (f) and an object of the required class as its second argument (t1 after copying to thread-local-storage):

If N == 1 and f is pointer to a member data object of a class, then it is accessed. The value of the object is ignored. Effectively, the following code is executed: t1.*f if and the type of t1 is either T, reference to T or reference to type derived from T. (*t1).*f otherwise.

Now, I don't plan on using std::thread in this way, but I am flummoxed by this definition. Apparently, the only thing that happens is that the data member is accessed and the value ignored, which doesn't seem like it could have any observable side-effects at all, meaning (as far I can tell) it might as well be a no-op. (I might be missing something obvious...?)

At first, I thought this might be a misprint, and meant to say that the data member is accessed and then called (since it might be a callable object, even if it's not a function) but I tested it with the following code in GCC-4.7 and indeed there is no call:

#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;
}

Is there any purpose to this definition which doesn't seem to accomplish anything? Why not instead make passing a "pointer-to-data-member-callable" act just like a "pointer-to-member-function", and make passing a "pointer-to-data-member-noncallable" an error? In fact, it seems like this would be the easiest way to implement it, since calling a "pointer-to-data-member-callable" has equivalent syntax to calling as a "pointer-to-member-function" in other contexts (unless there's something in the vagarities of template specialization and SFINAE rules which makes it difficult to treat them equivalently...?)

This is not something I need for actual code, but the fact that this definition exists leaves me suspecting that I am missing something fundamental, which worries me...can anyone enlighten me about this?

like image 565
Stephen Lin Avatar asked Oct 21 '22 16:10

Stephen Lin


1 Answers

This is because of the generic binding facility in terms of which the C++11 Standard defines not only how a thread is started, but also how std::bind and std::function work.

In fact, Paragraph 30.3.1.2/3 of the C++11 Standard specifies about the variadic constructor of class std::thread:

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

Effects: Constructs an object of type thread. The new thread of execution executes INVOKE (DECAY_- COPY ( std::forward<F>(f)), DECAY_COPY (std::forward<Args>(args))...) with the calls to DECAY_COPY being evaluated in the constructing thread. Any return value from this invocation is ignored. [...]

Ignoring what DECAY_COPY does (it is not relevant to the question), this is how Paragraph 20.8.2 defines the INVOKE pseudo-function:

Define INVOKE (f, t1, t2, ..., tN) as follows:

— (t1.*f)(t2, ..., tN) when f is a pointer to a member function of a class T and t1 is an object of type T or a reference to an object of type T or a reference to an object of a type derived from T;

— ((*t1).*f)(t2, ..., tN) when f is a pointer to a member function of a class T and t1 is not one of the types described in the previous item;

— t1.*f when N == 1 and f is a pointer to member data of a class T and t1 is an object of type T or a reference to an object of type T or a reference to an object of a type derived from T;

(*t1).*f when N == 1 and f is a pointer to member data of a class T and t1 is not one of the types described in the previous item;

— f(t1, t2, ..., tN) in all other cases.

Now the question becomes:

Why does the C++11 Standard defines the INVOKE facility that way?

And the answer is here.

like image 75
Andy Prowl Avatar answered Oct 26 '22 23:10

Andy Prowl