5

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?

us2012
  • 16,083
  • 3
  • 46
  • 62
Stephen Lin
  • 5,470
  • 26
  • 48
  • +1 This wording comes from the definition of `INVOKE`, which is given in 20.8.2 of the Standard. There is a question with the same intent as yours specifically about this wording in the `INVOKE` definition: http://stackoverflow.com/questions/12638393/why-does-invoke-facility-in-the-c11-standard-refer-to-data-members – jogojapan Feb 26 '13 at 02:35

1 Answers1

3

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.

Community
  • 1
  • 1
Andy Prowl
  • 124,023
  • 23
  • 387
  • 451
  • 1
    Thanks, I understand where the behavior is coming from now but I still don't follow why...why exactly would you define binding to member data that way? Just so you can retrieve a data member using std::mem_fn? If I bound f = std::mem_fn(&X::n) and then called f(&x), I would expect either an error or x.n(), not x.n...there might be a valid use case for binding to member data that way, but why not through something called std::mem_data? – Stephen Lin Feb 26 '13 at 02:49
  • @StephenLin: While I understand your point, I believe SO is not a good fit for questions of the kind "*Why are things the way they are rather than otherwise?*". I tried to answer the part that could be objectively answered. Hopefully better answers will be given. – Andy Prowl Feb 26 '13 at 02:55
  • No problem, my only reason to ask is because you linked to that answer as an explanation, which doesn't seem to explain anything (other than that someone might find this overloaded behavior useful...) – Stephen Lin Feb 26 '13 at 03:01
  • @AndyProwl There may still be objective reasons for the choice. It may well be possible to find them in proposals for the Standard, comments made by committee members, or elsewhere. – jogojapan Feb 26 '13 at 03:40