【问题标题】:Perfect forwarding and the scope of temporary objects完善的转发和临时对象的范围
【发布时间】:2015-03-03 01:45:51
【问题描述】:

为了实现* 运算符的完美转发,我构建了以下示例。

#include <string>
#include <iostream>

class A {
public:
    std::string name;
    A(const A& _other)  : name(_other.name) {
        std::cout << "Copy-Construct with name: " << name << std::endl;
    }
    A(A&& _other)       : name(std::move(_other.name)) {
        std::cout << "Move-Construct with name: " << name << std::endl;
    }
    A(std::string _name): name(_name) { }
};

A operator*(const A& _lhs, const A& _rhs) {
    std::cout << "Start Operator Copy with: " << _lhs.name << " " << _rhs.name << std::endl;
    A bla(_lhs.name+" "+_rhs.name);
    return bla;
}

A&& operator*(A&& _lhs, const A& _rhs) {
    std::cout << "Start Operator Move with: " << _lhs.name << " " << _rhs.name << std::endl;
    _lhs.name += " "+_rhs.name;
    return std::move(_lhs);
}

int main() {
    A a("a");
    A b("b");
    A c("c");
    A d("d");

    A x = a*b*A("t1")*c*A("t2")*A("t3")*d; 

    std::cout << "Final result is: " << x.name << std::endl;
}

结果如我所愿,特别是只调用了一个移动构造函数,没有调用复制构造函数。

Start Operator Copy with: a b
Start Operator Move with: a b t1
Start Operator Move with: a b t1 c
Start Operator Move with: a b t1 c t2
Start Operator Move with: a b t1 c t2 t3
Start Operator Move with: a b t1 c t2 t3 d
Move-Construct with name: a b t1 c t2 t3 d
Final result is: a b t1 c t2 t3 d

现在我的问题是:这是合法的C++11 代码吗?特别是,我可以依赖第一个临时对象(由 a 和 b 构造)在分号处而不是在那之前离开其范围的事实吗?并且将作为移动引用获得的对象返回为移动引用的构造是否合法?

【问题讨论】:

  • 你指的是哪个临时对象?
  • 第一次重载会泄漏内存——从任何意义上来说,这绝对不是“完美”的。两个重载都应该按值返回。
  • @JonathanWakely:抱歉,这是另一个测试用例。我纠正了它。输出是正确的。

标签: c++ c++11 scope move


【解决方案1】:
A&& operator*(const A& _lhs, const A& _rhs) {
    std::cout << "Start Operator Copy with: " << _lhs.name << " " << _rhs.name << std::endl;
    A* bla = new A(_lhs.name+" "+_rhs.name);
    return std::move(*bla);
}

这会创建一个动态分配的对象,因此调用者负责删除它。您的示例未能做到这一点,因此泄漏了内存。这是一个可怕的功能。它应该按值返回,这样会更快,因为您不会在堆上分配对象。

A&& operator*(A&& _lhs, const A& _rhs) {
    std::cout << "Start Operator Move with: " << _lhs.name << " " << _rhs.name << std::endl;
    _lhs.name += " "+_rhs.name;
    return std::move(_lhs);
}

这不会导致内存泄漏,所以不像第一个那样完全明显错误,但它仍然是错误的。如果您使用临时对象调用它,它会返回对同一临时对象的引用,但这可能会导致引用悬空:

A&& c = A("a") * A("b");

引用 c 绑定到由 A("a") 创建的临时对象,但在语句末尾超出范围。任何使用 c 的尝试都有未定义的行为。

两个重载都应该按值返回。

对于左侧是左值而右侧是右值的情况,您可能还需要重载,因为这样可以重用右侧的对象。而且,如果您添加它,您还需要在两个操作数都是右值的情况下进行重载。基本上看std::string是如何定义operator+

【讨论】:

  • 是的,第一个很明显,我忘了把它改回来(见我的编辑)。将结果保存为移动参考确实很讨厌。 :/ 但这是这种结构的唯一缺陷吗? IE。除非我尝试保存结果的移动引用,否则我会保存以使用它吗?
  • 是的,但是你为什么要编写一个只有在安全使用时才安全的函数,否则会被巧妙地未定义?如果您正在为您的类型实现移动语义,那么移动可能很便宜(否则不要费心支持移动语义),因此只需执行return _lhs; 按值返回是安全的,并且将使用移动构造函数。返回一个悬空的引用是在玩火,在未来的某一天,有人(也许是你未来的自己)会因此而诅咒你。
  • 是的……你最喜欢的就是这个。只是我真的很想摆脱每次调用移动构造函数的这种“廉价”开销。 ://
  • 您是否实际测量过这是一个问题?如果不是,那么编写不安全的函数来避免一些想象中的问题是错误的优先级恕我直言。
猜你喜欢
  • 2012-04-26
  • 1970-01-01
  • 2022-06-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-03-20
  • 1970-01-01
相关资源
最近更新 更多