【问题标题】:C++11 move semantics and Microsoft Visual C++ compiling optimizationC++11 移动语义和 Microsoft Visual C++ 编译优化
【发布时间】:2012-07-05 02:07:59
【问题描述】:

让我们考虑以下 Microsoft Visual C++(Microsoft Visual Studio 2012 RC,版本 11.0.50522.1 RCREL)中自定义数组的类模板。

/*C++11 switch-on*/

#include <iostream>

template <typename element, unsigned int size>
class array
{
    private:
        element data[size];
    public:
        array(){}
        ~array(){}
        array(const array & other)(){}
        element & operator [](unsigned int i)
        {
            if(i<size)
                return data[i];
            else
                throw std::runtime_error("Out of boundary");
        }
}

请注意,构造函数、析构函数和复制构造函数被定义为什么都不做。一个普通的打印函数定义如下

/*printing*/

template <typename element, unsigned int size>
void print(test::array<element, size> & content)
{
    unsigned int i=0;
    for(std::cout<<"["<<content[i++];i<size;std::cout<<content[i++])
        std::cout<<",";
    std::cout<<"]"<<std::endl;
}

当程序运行以下主程序时

int main(int argc, char * argv[])
{
    array<int, 3> a;

    /* uniform initialization is not supported yet
     * so we bother iterating to assign to initialize
     * a to [1,2,3]
    */

    for(int i=0;i<3;i++)
        a[i]=i+1;

    /*copy*/
    auto b=a;

    /*move*/
    auto c=std::move(a);

    /*change in a*/
    a[0]=0;

    print<int, 3>(a);
    print<int, 3>(b);
    print<int, 3>(c);

    return 0;
}

根据编译优化,输出结果会有所不同。特别是,如果我编译并运行

  • 打开 /Od 开关

    a=[0,2,3]

    b=[1470797225,-2,9185596]

    c=[0,2620008,9186761]

  • 打开 /O1、/O2 或 /Ox

    a=[0,2,3]

    b=[0,2,3]

    c=[0,2,3]

现在我明白了

  • 打开 /Od 开关
    • ba 不同,因为复制构造函数在调用时什么也不做
    • ca 不同,因为复制构造函数在调用时什么也不做。但是根据移动语义,a中数组数据中元素的变化也反映到c中。所以 a[0]==c[0]==0。

但我不明白为什么 abc 在优化开关打开的情况下都相等。我可能会认为 Microsoft C++ 编译器会用移动的复制构造函数代替不执行任何操作的复制构造函数,但我不确定。

【问题讨论】:

  • 相当肯定 VS2012 RC 是编译器的 17 或 18 版本,而不是 11。
  • 无论如何,你有未定义的行为,因为你从一个你从未写过的变量中读取。编译器所做的任何事情都是正确的。
  • a[0]=0 是 UB。您将实例“a”移动到“c”。 (实际上在移动之后对“a”的任何访问)我也能够使用 VS2010 重现该行为,因此我将重新标记它,因为它不特定于 VS2012。
  • @BenVoigt 读取从未写入的变量是什么意思?关键是 abc 首先是相等的,这是不应该的,因为复制构造函数什么都不做。
  • @LeSnip3R:不是a[0] = 0;是UB,是print读取成员从未初始化过。

标签: c++ visual-c++ c++11 visual-studio-2012 move-semantics


【解决方案1】:

标准的[conv.lval] 部分规定:

非函数、非数组类型T 的泛左值可以转换为纯右值。如果T 是不完整的类型,则需要进行此转换的程序格式错误。如果泛左值引用的对象不是T 类型的对象,也不是从T 派生的类型的对象,或者如果该对象未初始化,则需要此转换的程序具有未定义的行为。如果T 是非类类型,则prvalue 的类型是T 的cv 非限定版本。否则,prvalue的类型为T

print 内部,表达式cout &lt;&lt; content[i++] 使用左值到右值的转换。当您调用print(b)print(c) 时,转换发生在从未初始化的对象上,因此您有未定义的行为。

试图描述未定义的行为是徒劳的。


注意:对象bc 由复制构造函数初始化。复制构造函数不会初始化子对象b.contentc.content,这意味着这些数组及其所有成员元素在形式上都未初始化

初始化c 时,代码实际上并没有从a 移动。 std::move 创建了一个右值引用,这使得移动成为可能,但是没有匹配的构造函数接受右值引用,所以使用了复制构造函数,a 被复制而不是移动。

【讨论】:

  • 好的。所以 b.content 和 c.content 在程序的发布版本中恰好具有相同的值 [1,2,3],而所有包括 a.content 在内的所有内容在调试中都被打乱了。嗯……
  • 嗯...我认为尝试描述语义上未定义的行为有助于我开发独立于语言学的逻辑。我和所有不会用英语编程和推理的中国程序员都想到了这一点。它使我们与众不同,并且是“对西方逻辑系统的危险利用”,尽管我们根本不是这个意思;-)
  • 对中国程序员来说还有一个额外的困难:用拉丁字母写代码对于习惯了另一组符号的人来说是非常困难的。
  • 我不确定您的文化如何反映编译器优化,但这肯定是看待它的一种方式:p
  • @Yang:即使源代码调用 UB,由特定编译器的特定执行生成的机器代码通常(但不总是)是确定性的。但是,这并没有使表达式定义明确,因为当您重新编译时,您可能会得到不同的结果。
猜你喜欢
  • 2012-02-09
  • 1970-01-01
  • 1970-01-01
  • 2011-09-25
  • 2014-08-16
  • 2023-03-26
  • 2013-12-19
  • 2019-01-02
  • 1970-01-01
相关资源
最近更新 更多