【问题标题】:Return a named object of a class from a function ( pass by value ) and implicit move rule?从函数(按值传递)和隐式移动规则返回类的命名对象?
【发布时间】:2022-07-04 12:48:28
【问题描述】:

当您从函数(按值传递)返回类的对象(不是特定的类)时,我很难理解会发生什么 在这段代码中: 示例 1

#include<iostream>
#include<vector>
#include<string>
using namespace std;
class test {
public:
    test(int y) {
        printf(" test(int y)\n");
    }
    test() {
        printf(" test()\n");
    }
    test( const test& z) {
        printf(" test( const test&z)\n");
    }
    test(test&& s)noexcept{
            printf(" test(test&& s)\n");          
    }
    test& operator=(test e) {
        printf(" test& operator=( test e)\n");
        return *this;
    }
};
test Some_thing() {
    test i;
    return i;
}
int main()
{
    Some_thing();
    return 0;
}

输出:

 test()
 test(test&& s)

前面的输出让我明白了在函数 (Some_thing()) 的作用域中创建了两个对象。第一个是一个左值对象,我们在函数的第一行( Some_thing ( ) )中创建它并给它一个名称( i )所以构造函数test ( ) 被调用。 而第二个是右值对象所以调用了构造函数test ( test&amp;&amp; s )

但是当我删除了这个构造函数test(test&amp;&amp; s)noexcept并改变了这个构造函数

test( const test&amp; z)

进入

test( test& z)

然后再次运行代码:

示例 2

#include<iostream>
#include<vector>
#include<string>
using namespace std;
class test {
public:
    test(int y) {
        printf(" test(int y)\n");
    }
    test() {
        printf(" test()\n");
    }
    test( test& z) {
        printf(" test( test&z)\n");
    }
    test& operator=(test e) {
        printf(" test& operator=( test e)\n");
        return *this;
    }
};
test Some_thing() {
    test i;
    return i;
}
int main()
{
    Some_thing();
    return 0;
}

输出:

 test()
 test( test&z)

虽然我预计这段代码不会编译,因为没有构造函数将test&amp;&amp;const test&amp; 作为参数

当我尝试在前面的代码中添加一行时,test(test&amp;&amp; z) = delete

示例 3

#include<iostream>
#include<vector>
#include<string>
using namespace std;
class test {
public:
    test(test&& z) = delete;
    test(int y) {
        printf(" test(int y)\n");
    }
    test() {
        printf(" test()\n");
    }
    test( const test& z) {
        printf(" test( test&z)\n");
    }
    test& operator=(test e) {
        printf(" test& operator=( test e)\n");
        return *this;
    }
};
test Some_thing() {
    test i;
    return i;
}
int main()
{
  Some_thing();
    return 0;
}

我试图编译它,但它没有编译,也没有运行

那么EXAMPLE 2是如何编译运行的??????以及如何使用构造函数test( test&amp;z) 而不是 test(test&amp;&amp; z)??????

(我的意思是test( test&amp;z)不是test( const test&amp;z)所以不能用test( test&amp;z)代替test(test&amp;&amp; z)

编辑: 此代码编译并运行: 示例 4

#include<iostream>
#include<vector>
#include<string>
using namespace std;
class test {
public:
    test(test&& z) = delete;
    test(int y) {
        printf(" test(int y)\n");
    }
    test() {
        printf(" test()\n");
    }
    test(const test& z) {
        printf(" test( test&z)\n");
    }
    test& operator=(test e) {
        printf(" test& operator=( test e)\n");
        return *this;
    }
};

int main()
{
    test u;
    test r(u);
    return 0;
}

输出:

 test()
 test( test&z)

【问题讨论】:

  • 对象切片发生。永远不要这样做。
  • 什么是对象切片??????在我的例子中什么时候发生???? @user207421
  • 视情况而定。在较早的 C++ 标准(C++17 之前)中,按值返回的语义是将副本返回给调用者。但是,这些标准明确允许(但不要求)编译器在某些情况下删除(省略)对象的副本(例如,如果检查副本的唯一方法是通过跟踪调用构造函数和析构函数)和一些编译器(具有相关的优化设置)实现了返回值优化以在某些情况下删除副本,而有些则没有。从 C++17 开始,复制省略在多种情况下成为强制要求。
  • @user207421 对象切片是如何发生的?这里没有继承。
  • 我认为示例 2 的关键误解是您认为复制构造函数必须使用 const,但事实并非如此。 C++ 标准的“class.copy”部分明确指出像 test(test&amp;) 这样的构造函数是复制构造函数。

标签: c++


【解决方案1】:

你的程序的行为可以在Automatic move from local variables and parameters的帮助下理解:

如果表达式是一个(可能带括号的)id 表达式,它命名一个变量,其类型为

  • 非易失性对象类型或

  • 对对象类型的非易失性右值引用(C++20 起)

并且声明了变量

  • 在体内

  • 作为参数

    最里面的封闭函数或 lambda 表达式,

然后重载决议选择用于初始化返回值的构造函数,或者对于 co_return,选择 promise.return_value() 的重载(C++20 起)执行两次

  • 首先好像表达式是一个右值表达式(因此它可以选择移动构造函数),然后
  • 如果第一次重载解决失败
  • 它成功了,但没有选择移动构造函数(形式上,所选构造函数的第一个参数不是对(可能是 cv 限定的)表达式类型的右值引用)(C++20 前)
  • 然后重载解析像往常一样执行,表达式被视为左值(因此它可以选择复制构造函数)。

现在,让我们根据具体情况将其应用于您的代码 sn-p。

示例 1

在这种情况下,由于移动 ctor 可用且可行,因此满足条件“首先好像表达式是右值表达式”,因此选择了移动 ctor,我们得到了上述输出。

class test {
public:
    test(int y) {
        printf(" test(int y)\n");
    }
    test() {
        printf(" test()\n");
    }
    test( const test& z) {
        printf(" test( const test&z)\n");
    }
    test(test&& s)noexcept{
            printf(" test(test&& s)\n");          
    }
    test& operator=(test e) {
        printf(" test& operator=( test e)\n");
        return *this;
    }
};
test Some_thing() {
    test i;
    return i;
}
int main()
{
    Some_thing();
    return 0;
}

示例 2

在这种情况下,由于您提供了复制 ctor test::test( test&amp;),编译器不会为我们合成移动 ctor。请注意,没有合成的移动 ctor 与删除的移动 ctor 不同。因此条件 “如果第一次重载解析失败” 得到满足(因为没有移动 ctor),然后第二次执行重载解析,现在将选择提供的复制 ctor,因此提到的输出。

class test {
public:
    test(int y) {
        printf(" test(int y)\n");
    }
    test() {
        printf(" test()\n");
    }
    test( test& z) {
        printf(" test( test&z)\n");
    }
    test& operator=(test e) {
        printf(" test& operator=( test e)\n");
        return *this;
    }
};
test Some_thing() {
    test i;
    return i;
}
int main()
{
    Some_thing();
    return 0;
}

示例 3

在这种情况下,您已明确删除移动 ctor。也就是说,您的意图是,如果有人尝试使用 move ctor,那么它应该会失败。因此,在这里,当第一次发生重载解决方案时,选择了移动 ctor,但是由于您已明确将其标记为已删除,因此立即失败并因此出现错误。

class test {
public:
    test(test&& z) = delete;
    test(int y) {
        printf(" test(int y)\n");
    }
    test() {
        printf(" test()\n");
    }
    test( const test& z) {
        printf(" test( test&z)\n");
    }
    test& operator=(test e) {
        printf(" test& operator=( test e)\n");
        return *this;
    }
};
test Some_thing() {
    test i;
    return i;
}
int main()
{
  Some_thing();
    return 0;
}

【讨论】:

  • 首先非常感谢您回答我的问题。当我尝试使用不同的编译器(C++14 编译器)编译代码(示例 1)并且没有出现“test(test&&s)”时,我还有一个问题。那是由于复制省略,对吗? (我在我的问题中使用了 C++98 编译器)@Anoop Rana
  • @f877576 是的,这是由于copy elison。您甚至可以通过向编译器提供-fno-elide-constructors 标志来确认这一点。例如,当您提供此-fno-elide-constructors 标志时,您将获得输出test() test(test&amp;&amp; s)Demo with flag。如果您不提供此标志,则允许编译器省略复制/移动构造。 Demo without flag。不客气。
  • (ctor)是(构造函数)这个词的缩写吗? @Anoop Rana
  • @f877576 是的,“ctor”代表构造函数,而“dtor”代表析构函数。
  • (合成移动构造函数)是什么意思? @Anoop Rana
猜你喜欢
  • 1970-01-01
  • 2020-09-17
  • 2011-08-25
  • 2013-05-06
  • 2019-10-05
  • 1970-01-01
  • 1970-01-01
  • 2021-08-06
  • 2016-11-09
相关资源
最近更新 更多