【问题标题】:Two-step copy elision to capture rvalue in constructor call as instance variable两步复制省略以在构造函数调用中捕获右值作为实例变量
【发布时间】:2014-05-29 22:10:00
【问题描述】:

我正在尝试获取此类的 rvalue 实例:

#include <iostream>
#define msg(x) std::cout << x " constructor\n"

struct X {
    int i;
    X(int i) : i(i) {msg("X");}
    X(const X& x) : i(x.i) {std::cout << "X copy\n";}
    X(X&& x) {std::swap(i, x.i); std::cout << "X move\n";}
};

进入该类的实例变量x

struct A {
    X x;
    A(X x) : x(x) {msg("A");}
};

像这样:

int main() {
    A a(X(1));
    std::cout << a.x.i << "\n\n";
}

不进行任何复制或移动

根据这些参考资料,

还有很多关于 SO 的帖子(所以请在标记为重复之前阅读到最后),我应该依赖 复制省略,如果我应该满足其条件按值传递。请注意,需要两个复制省略,即:

constructor call -&gt; constructor local variable -&gt; instance variable

在关闭复制省略时可以看到(使用 g++-4.8 -std=c++11 -fno-elide-constructors 编译):

X constructor
X move
X copy
A constructor
1

所以有一个move 步骤和一个copy 步骤,如果我打开复制省略(使用g++-4.8 -std=c++11 -O3 编译),它们都应该消失:

X constructor
X copy
A constructor
1

糟糕,copy 步骤仍然存在!


我可以通过std::move()std::forward 的任何其他变体或传递为rvalue-reference 来获得更好的结果吗?

struct B {
    X x;
    B(X x) : x(std::move(x)) {msg("B");}
};

struct C {
    X x;
    C(X x) : x(std::forward<X>(x)) {msg("C");}
};

struct D {
    X x;
    D(X&& x) : x(std::move(x)) {msg("D");}
};

int main() {
    B b(X(2));
    std::cout << b.x.i << "\n\n";

    C c(X(3));
    std::cout << c.x.i << "\n\n";

    D d(X(4));
    std::cout << d.x.i << "\n\n";
}

产生输出:

X constructor
X move
B constructor
2

X constructor
X move
C constructor
3

X constructor
X move
D constructor
4

好的,我把copy变成了move但这并不令人满意!


接下来,我尝试将实例变量x 设为引用X&amp;

struct E {
    X& x;
    E(X x) : x(x) {msg("E");}
};

int main() {
    E e(X(5));
    std::cout << e.x.i << "\n\n";
}

产生:

X constructor
E constructor
1690870696

坏主意!我摆脱了move,但x 引用的rvalue 实例在我的座位下被破坏,所以最后一行打印垃圾而不是5。两个注意事项:

  • g++-4.8 没有警告我任何事情,即使是 -pedantic -Wall -Wextra
  • 程序在使用-O0 编译时打印5

所以这个错误可能会在很长一段时间内被忽视!


那么,这是一个没有希望的案例吗?好吧不:

struct F {
    X& x;
    F(X& x) : x(x) {msg("F");}
};

int main() {
    X x(6);
    F f(x);
    std::cout << f.x.i << "\n";
}

打印:

X constructor
F constructor
6

真的吗?没有花哨的新 C++11 功能,编译器不会自行决定复制省略,只是普通的旧 FORTRAN66 样式的传递引用可以满足我的需求并且可能会表现最好?

这是我的问题:

  • 有什么方法可以让rvalues 工作吗?我错过了任何功能吗?
  • lvalue-reference 版本真的是最好的吗,还是X x(6) 步骤中存在隐藏成本?
  • xf建成后生活会不会有什么不便?
  • 我可以为使用lvalue 引用外部实例支付数据局部性罚款吗?

【问题讨论】:

  • struct E 具有未定义的行为(悬空引用。)和struct F“有效”,但与其他所有行为完全不同(除了 UB E)。那么你想比较什么?
  • @juanchopanza: 是的,我学会了struct E 的行为。
  • 最好的选择是两个重载:T1(const T2&amp; t) : t_(t) {}T1(T2&amp;&amp; t) : t_(std::move(t)) {}
  • @juanchopanza:所有版本,包括struct F,都可以完全访问实例x,这是我需要的。
  • 不,F 引用了外部实体。它不拥有它。其他人(忽略E)可以。这是一个至关重要的区别。

标签: c++ c++11 rvalue-reference move-constructor copy-elision


【解决方案1】:

在不涉及您问题的太多细节的情况下,基本上尽可能使用复制省略。这是一个快速演示:

#include <iostream>
#include <utility>

struct X
{
    int n_;

    explicit X(int n) : n_(n) { std::cout << "Construct: " << n_ << "\n"; }
    X(X const & rhs) : n_(rhs.n_) { std::cout << "X copy:" << n_ << "\n"; }
    X(X && rhs) : n_(rhs.n_) { rhs.n_ = -1; std::cout << "X move:" << n_ << "\n"; }
   ~X() { std::cout << "Destroy: " << n_ << "\n"; }
};

struct A
{
    explicit A(X x) : x_(std::move(x)) {};
    X x_;
};

struct B
{
    X x;
};

int main()
{
    A a(X(12));
    B b { X(24) };
}

这个produces

Construct: 12
X move:12
Destroy: -1
Construct: 24
Destroy: 24
Destroy: 12

x_(std::move(x)) 中的一招不可省略,因为它不涉及函数返回。但这无论如何都很好。并注意聚合 b 确实是“就地”初始化的。


您的示例F 表明您愿意将X 的耦合暴露给它的环境类。在这种情况下,您可以为 A 创建一个特殊的构造函数,直接构造 X

struct A
{
    explicit A(X x) : x_(std::move(x)) {};
    X x_;

    // Better approach
    struct direct_x_t {};
    static direct_x_t direct_x;

    // In our case, this suffices:
    A(direct_x_t, int n) : x_(n) {}

    // Generally, a template may be better: (TODO: add SFINAE control)
    template <typename ...Args> A(direct_x_t, Args &&... args)
    : x_(std::forward<Args>(args)...) {}
};

用法:

A a(A::direct_x, 36);

【讨论】:

  • 谢谢!那么你认为struct B 是最好的吗?我最喜欢的struct F 有什么问题? :)
  • @Stefan:大多数时候你不能拥有聚合类,所以这可能不是一个选择。就个人而言,我对struct A 非常满意,并将其用作通用模型。 F 疯了;除非您真的知道自己在做什么并准备在凌晨 4 点为您的用户提供支持,否则您基本上不应该有参考成员。
  • @Stefan:如果您愿意将耦合公开给X,您可以尝试我添加的替代方法。类似于各种标准库容器中的emplace
  • 在我的用例中,如果我愿意,我可以随意使用struct F。我的评估表明,这将为我节省一次移动操作。 xf 都将生活在同一个小范围内。
  • @KerrekSB 为什么带有转发机制的结构 A 允许编译器就地构造对象,而构造函数移动临时对象却不允许?可变参数模板是否起到了允许的作用?
猜你喜欢
  • 1970-01-01
  • 2013-10-14
  • 1970-01-01
  • 1970-01-01
  • 2023-03-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多