Visual Studio实现的“移动语义”和“右值引用”

5

我看到了一段关于c++11并发(第三部分)的Youtube视频和下面的代码,这段代码能够在视频中被编译并生成正确的结果。

然而,当我使用Visual Studio 2012编译这段代码时,出现了一个编译错误。编译器抱怨toSin(list<double>&&)的参数类型。如果我将参数类型更改为list<double>&,则代码可以编译。

我的问题是,在_tmain()move(list)返回的是rvalue引用还是普通引用?

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

4
建议使用至少VS2013,最好是2015年的Community Preview版本,因为VS2012已经过时了。请注意,这只是翻译,没有额外的解释或其他内容。 - Ben Voigt
2个回答

6

这似乎是MSVC2012中的一个错误。(快速检查后,MSVC2013和MSVC2015也有同样问题)

thread不直接使用完美转发,因为在原始线程中存储对数据(临时或非临时)的引用并在生成的线程中使用它将极易出错和危险。

相反,它将每个参数复制到decay_t<?>的内部数据中。

错误在于它调用工作函数时仅将该内部副本传递给您的过程。相反,它应该将该内部数据“移动”到调用中。

这在编译器版本19中似乎没有得到修复,我认为它是MSVC2015(没有再次确认),基于在此处编译代码。

这既是由于标准的措辞(它应该调用带有decay_t<Ts>...decay_t<F>--这意味着rvalue绑定,而不是lvalue绑定),也是因为线程中存储的局部数据在过程调用后将永远不会再次使用(因此在逻辑上应将其视为过期数据,而不是持久数据)。

以下是解决方法:

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

那么

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

应该可以正常工作。(在上面链接的MSVC2015在线编译器中测试过) 根据个人经验,它也应该在MSVC2013中正常工作。我不知道MSVC2012怎么样。


由于复制是在单独的线程中完成,且在完整表达式结束时未被销毁,这是否意味着这些副本不是prvalue?并且它似乎也不适用于任何会使它们成为xvalue的情况...所以它们根本不是rvalue,不能绑定到rvalue引用。 - Ben Voigt
1
@BenVoigt 这是一个实现细节。标准规定由创建的线程调用 INVOKE(DECAY_COPY(std::forward<F>(f)), DECAY_COPY(std::forward<Args>(args))...)。编译器可以自由地做任何事情,就好像发生了这种情况一样。由于 DECAY_COPY() 的返回值不能绑定到左值,但可以绑定到右值,因此 MSVC 没有执行标准规定的操作。幸运的是,无法检测到函数调用的参数是 prvalues,因此 std::move 可以在没有编译器扩展的情况下产生所需的结果(我想)。 - Yakk - Adam Nevraumont
但是传递给INVOKE的参数不是临时变量。从逻辑上讲,标准应该将此情况称为x值,但它并没有这样做。 - Ben Voigt
@BenVoigt 不,传递给INVOKE的参数是临时的。这就是标准所说的。它说INVOKE(DECAY_COPY(std::forward<F>(f)), DECAY_COPY(std::forward<Args>(args))...)DECAY_COPY返回一个临时对象,就是这样。因此根据标准,函数必须在临时对象上操作,或者表现得与在临时对象上操作完全相同。更重要的是,该临时对象必须在调用线程中创建,但是INVOKE必须在创建的线程中发生!在C++11中没有办法做到这一点,但是根据as-if规则,编译器可以使用std::move来模拟它。 - Yakk - Adam Nevraumont
@BenVoigt 是的,但没有C++编译器支持“在一个线程中创建临时变量,在另一个线程中使用它们”。因此,将其解读为“嗯,那是不可能的”是相当合理的:它只是不同意标准对代码的疯狂要求!如果有任何方法可以区分传递给operator()的临时变量和std::move的输出,那么这实际上就是标准中的缺陷,但我认为没有任何方法。因此,“明智”的转发和移动实现足够好地伪装了它... - Yakk - Adam Nevraumont
显示剩余2条评论

-1

std::move 返回的确实是一个右值引用,但这并不重要,因为 thread 构造函数没有对其参数使用完美转发。首先,它将它们复制/移动到新线程拥有的存储中。然后,在新线程内部,使用这些副本调用提供的函数。

由于这些副本不是临时对象,所以此步骤不会绑定到右值引用参数。

标准规定(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.

并且

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

数值类别丢失。


线程是否将内部副本复制/移动到函数对象的参数中? - template boy
@MooingDuck:为什么不是呢?这是std::thread指定的行为。 - Ben Voigt
@MooingDuck:可以说,由于这些是专门为新线程使用而制作的副本,因此无论原始值类别如何,它都应在所有参数上使用std::move - Ben Voigt
4
然而,请注意,DECAY_COPY 返回一个裸的 T (没有引用,没有任何内容)。使用裸的 T 调用 INVOKE 就相当于在绑定到右值上下文中进行调用。它并没有说“我将调用 DECAY_COPY,存储结果,并将存储的结果传递”,而是说“我将使用 DECAY_COPY 进行调用”。如果这个观点正确,那么你的分析就是颠倒的,而 MSVC 存在一个错误。这也是一个实际的错误,因为存储在 thread 中的参数将永远不会被再次使用,因此让它们绑定到左值非 const 引用在概念上是错误的,而绑定到右值则是正确的。 - Yakk - Adam Nevraumont
@Yakk:实际上,它确实说DECAY_COPY在与调用f不同的线程上执行。请参见我上面的评论,我说移动这些对象是“正确”的做法...但标准没有指定。 - Ben Voigt
2
@benvoigt,它执行的线程与INVOKE的语义无关。 想象一下,如果不存在该子句(不同的线程),那么INVOKE的语义将是清晰的(即哪些重载是有效的等)。 不同的线程如何改变这个? 它没有。 这使得实现变得棘手,但它不允许MSVC所做的事情。 绑定到rvalues而不是lvalues既是标准规定的,也是正确的做法。 - Yakk - Adam Nevraumont

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