5

I came across a Youtube video on c++11 concurrency (part 3) and the following code, which compiles and generates correct result in the video.

However, I got a compile error of this code using Visual Studio 2012. The compiler complains about the argument type of toSin(list<double>&&). If I change the argument type to list<double>&, the code compiled.

My question is what is returned from move(list) in the _tmain(), is it a rvalue reference or just a reference?

#include "stdafx.h"
#include <iostream>
#include <thread>
#include <chrono>
#include <list>
#include <algorithm>
using namespace std;

void toSin(list<double>&& list)
{
    //this_thread::sleep_for(chrono::seconds(1));
    for_each(list.begin(), list.end(), [](double & x)
    {
        x = sin(x);
    });

    for_each(list.begin(), list.end(), [](double & x)
    {
        int count = static_cast<int>(10*x+10.5);
        for (int i=0; i<count; ++i)
        {
            cout.put('*');
        }
        cout << endl;
    });
}    

int _tmain(int argc, _TCHAR* argv[])
{
    list<double> list;

    const double pi = 3.1415926;
    const double epsilon = 0.00000001;
    for (double x = 0.0; x<2*pi+epsilon; x+=pi/16)
    {
        list.push_back(x);
    }

    thread th(&toSin, /*std::ref(list)*/std::move(list));
    th.join();    

    return 0;
}

2 Answers2

6

This appears to be a bug in MSVC2012. (and on quick inspection, MSVC2013 and MSVC2015)

thread does not use perfect forwarding directly, as storing a reference to data (temporary or not) in the originating thread and using it in the spawned thread would be extremely error prone and dangerous.

Instead, it copies each argument into decay_t<?>'s internal data.

The bug is that when it calls the worker function, it simply passes that internal copy to your procedure. Instead, it should move that internal data into the call.

This does not seem to be fixed in compiler version 19, which I think is MSVC2015 (did not double check), based off compiling your code over here

This is both due to the wording of the standard (it is supposed to invoke a decay_t<F> with decay_t<Ts>... -- which means rvalue binding, not lvalue binding), and because the local data stored in the thread will never be used again after the invocation of your procedure (so logically it should be treated as expiring data, not persistent data).

Here is a work around:

template<class F>
struct thread_rvalue_fix_wrapper {
  F f;
  template<class...Args>
  auto operator()(Args&...args)
  -> typename std::result_of<F(Args...)>::type
  {
      return std::move(f)( std::move(args)... );
  }
};
template<class F>
thread_rvalue_fix_wrapper< typename std::decay<F>::type >
thread_rvalue_fix( F&& f ) { return {std::forward<F>(f)}; }

then

thread th(thread_rvalue_fix(&toSin), /*std::ref(list)*/std::move(list));

should work. (tested in MSVC2015 online compiler linked above) Based off personal experience, it should also work in MSVC2013. I don't know about MSVC2012.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • Since the copy is made in a separate thread, and **is not destroyed at the end of the full-expression**, doesn't that imply the copies are not prvalues? And it doesn't seem to fit into any of the cases that would make them xvalues... so then they aren't rvalues at all and cannot bind to rvalue references. – Ben Voigt Feb 04 '15 at 21:27
  • 1
    @BenVoigt That is an implementation detail. The standard mandates that `INVOKE( DECAY_COPY(std::forward(f)), DECAY_COPY(std::forward(args))... )` be called by the created thread. The compiler is free to do anything as-if that happened. As the return value of `DECAY_COPY()` cannot bind to lvalues, and can bind to rvalues, MSVC not doing what the standard mandates. Luckily it is impossible to detect that the arguments to a function call are prvalues, so `std::move` produces the required result without compiler extensions (I think). – Yakk - Adam Nevraumont Feb 04 '15 at 22:15
  • But the parameters passed to INVOKE are not temporaries. Logically the Standard should call this case out as an x value but it hasn't done so. – Ben Voigt Feb 05 '15 at 19:34
  • @BenVoigt no, the parameters passed to INVOKE are temporaries. That is what the standard says. It says `INVOKE( DECAY_COPY(std::forward(f)), DECAY_COPY(std::forward(args))... )`. `DECAY_COPY` returns a temporary, period. So under the standard, the function must operate on a temporary, or behave identical to as-if it was operating on a temporary. What more, that temporary must have been created **in the calling thread**, but the `INVOKE` must occur in the created thread! There is no way to do this in C++11, *but* under the as-if rule the compiler can fake it with a `std::move`. – Yakk - Adam Nevraumont Feb 05 '15 at 19:55
  • Now, practically, the as-if implementation (where the `std::thread` creates a tuple of decayed arguments in the free store, then forwards the arguments into it, then passes that tuple to the worker thread, then the worker thread `move`s the arguments out) is the practical one, as barring the creation of a thread and passing a function pointer to start executing and a `void*` to the thread, most everything can be done as a library without language extensions. I'm just stating that the standard is clear about the semantics -- invoke of a temporary copy of f with temporary copies of each arg. – Yakk - Adam Nevraumont Feb 05 '15 at 19:58
  • Hmm I guess a closer reading of 12.2 supports that: "The temporary to which the reference is bound or the temporary that is the complete object of a subobject to which the reference is bound persists for the lifetime of the reference except: **A temporary object bound to a reference parameter in a function call persists until the completion of the full-expression containing the call.**" And the function call is in the child thread. – Ben Voigt Feb 05 '15 at 20:03
  • @BenVoigt yep, but no C++ compiler supports "create temporaries in one thread, and use them in another". So reading it as "well, that is impossible" is quite reasonable: it just disagrees with the crazy requirement the standard places on the code! If there was any way to distinguish between a temporary and the output of `std::move` when passed to `operator()` this would be effectively a defect in the standard, but I don't think there is any way. So the "sensible" forwarding and move implementation fakes it well enough... – Yakk - Adam Nevraumont Feb 05 '15 at 20:13
-1

What is returned from std::move is indeed an rvalue reference, but that doesn't matter because the thread constructor does not use perfect forwarding for its arguments. First it copies/moves them to storage owned by the new thread. Then, inside the new thread, the supplied function is called using the copies.

Since the copies are not temporary objects, this step won't bind to rvalue-reference parameters.

What the Standard says (30.3.1.2):

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.

and

In several places in this Clause the operation DECAY_COPY(x) is used. All such uses mean call the function decay_copy(x) and use the result, where decay_copy is defined as follows:

template <class T> decay_t<T> decay_copy(T&& v)
{ return std::forward<T>(v); }

The value category is lost.

Community
  • 1
  • 1
Ben Voigt
  • 277,958
  • 43
  • 419
  • 720
  • Does the thread copy/move the internal copies into the functors parameters? – template boy Feb 04 '15 at 20:18
  • @MooingDuck: Why would it be? This is the behavior specified for `std::thread`. – Ben Voigt Feb 04 '15 at 20:19
  • @MooingDuck: Arguably, since these are copies made expressly for the use of the new thread, it should use `std::move` on ALL the arguments regardless of the original value category. – Ben Voigt Feb 04 '15 at 20:26
  • 4
    But wait, `DECAY_COPY` returns a raw `T` (no references, no nothing). Calling `INVOKE` with a raw `T` is calling it in a bind-to-rvalue context. It doesn't say "I will call `DECAY_COPY`, store the result, and pass that stored result on", it says "I will invoke with a `DECAY_COPY`". If this argument is right, your analysis is upside down, and MSVC has a bug. It is also a practical error, because the parameters stored in `thread` will never be used again, so having them bind to lvalue non-const references is conceptually wrong, and binding to rvalues is right. – Yakk - Adam Nevraumont Feb 04 '15 at 20:29
  • @Yakk: Actually, it does say that DECAY_COPY is performed on a different thread from the call to `f`. See my above comment where I said that moving these objects would be the "right" thing to do... but the Standard doesn't specify that. – Ben Voigt Feb 04 '15 at 21:22
  • 2
    @benvoigt what thread it is performed in has nothing to do with the semantics of the INVOKE. Imagine that clause (the different thread) did not exist: would it be clear what the semantics of the INVOKE would mean (ie, what overloads are valid, etc)? How does it happening in a different thread change this? It does not. It makes the implementation tricky, but it doesn't permit what MSVC does. It binding to rvalues and NOT lvalues is both mandated by the standard, *and* the right thing to do. – Yakk - Adam Nevraumont Feb 04 '15 at 22:13