c++ 最适合建筑师的形式?通过值还是引用传递?

yrdbyhpb  于 2023-06-25  发布在  其他
关注(0)|答案(1)|浏览(72)

我想知道我的建造者的最佳状态。下面是一些示例代码:

class Y { ... }

class X
{
public:
  X(const Y& y) : m_y(y) {} // (a)
  X(Y y) : m_y(y) {} // (b)
  X(Y&& y) : m_y(std::forward<Y>(y)) {} // (c)

  Y m_y;
}

Y f() { return ... }

int main()
{
  Y y = f();
  X x1(y); // (1)
  X x2(f()); // (2)
}

据我所知,这是编译器在每种情况下所能做的最好的。
(1a)y复制到x1.m_y(1个副本)
(1b)y复制到X的构造函数的参数中,然后复制到x1.m_y(2份)
(1c)y移动到x1.m_y(1次移动)
(2a)f()的结果被复制到x2.m_y(1个拷贝)
(2b)f()被构造到构造函数的参数中,然后复制到x2.m_y(1个副本)
(2c)f()在堆栈上创建,然后移动到x2.m_y(1次移动)
现在有几个问题:
1.在这两个方面,通过const引用传递并不比通过值传递更差,有时甚至比通过值传递更好。这似乎违背了关于"Want Speed? Pass by Value."的讨论。对于C++(不是C0x),我应该坚持使用const引用传递这些构造函数,还是应该通过值传递?对于C0x,我应该通过右值引用传递而不是通过值传递吗?
1.对于(2),我更希望临时直接构造成x.m_y。即使是右值版本,我认为也需要移动,除非对象分配动态内存,否则这与复制一样多。有没有什么方法可以编写这样的代码,以便允许编译器避免这些复制和移动?
1.我在我认为编译器能做得最好的方面和我的问题本身都做了很多假设。如果这些错误,请更正。

xxb16uws

xxb16uws1#

我随便举了几个例子。我使用了GCC 4.4.4。

    • 简单案例,不带-std=c++0x**

首先,我将一个非常简单的示例与两个分别接受std::string的类放在一起。

#include <string>
#include <iostream>

struct A /* construct by reference */
  {
    std::string s_;

    A (std::string const &s) : s_ (s)
      {
        std::cout << "A::<constructor>" << std::endl;
      }
    A (A const &a) : s_ (a.s_)
      {
        std::cout << "A::<copy constructor>" << std::endl;
      }
    ~A ()
      {
        std::cout << "A::<destructor>" << std::endl;
      }
  };

struct B /* construct by value */
  {
    std::string s_;

    B (std::string s) : s_ (s)
      {
        std::cout << "B::<constructor>" << std::endl;
      }
    B (B const &b) : s_ (b.s_)
      {
        std::cout << "B::<copy constructor>" << std::endl;
      }
    ~B ()
      {
        std::cout << "B::<destructor>" << std::endl;
      }
  };

static A f () { return A ("string"); }
static A f2 () { A a ("string"); a.s_ = "abc"; return a; }
static B g () { return B ("string"); }
static B g2 () { B b ("string"); b.s_ = "abc"; return b; }

int main ()
  {
    A a (f ());
    A a2 (f2 ());
    B b (g ());
    B b2 (g2 ());
    
    return 0;
  }

该程序在stdout上的输出如下:

A::<constructor>
A::<constructor>
B::<constructor>
B::<constructor>
B::<destructor>
B::<destructor>
A::<destructor>
A::<destructor>
    • 结论**
  • GCC能够优化每个临时AB。* 这与C++ FAQ一致。基本上,GCC可能(并且愿意)生成代码来构造a, a2, b, b2 * 就地 *,即使调用一个外观上是按值返回的函数。因此,GCC可以避免许多可能通过查看代码“推断”其存在的临时变量。

接下来我们要看的是在上面的例子中std::string实际上被复制的频率。让我们把std::string替换为我们可以更好地观察和看到的东西。

    • 真实情况,不含-std=c++0x *
#include <string>
#include <iostream>

struct S
  {
    std::string s_;

    S (std::string const &s) : s_ (s)
      {
        std::cout << "  S::<constructor>" << std::endl;
      }
    S (S const &s) : s_ (s.s_)
      {
        std::cout << "  S::<copy constructor>" << std::endl;
      }
    ~S ()
      {
        std::cout << "  S::<destructor>" << std::endl;
      }
  };

struct A /* construct by reference */
  {
    S s_;

    A (S const &s) : s_ (s) /* expecting one copy here */
      {
        std::cout << "A::<constructor>" << std::endl;
      }
    A (A const &a) : s_ (a.s_)
      {
        std::cout << "A::<copy constructor>" << std::endl;
      }
    ~A ()
      {
        std::cout << "A::<destructor>" << std::endl;
      }
  };

struct B /* construct by value */
  {
    S s_;

    B (S s) : s_ (s) /* expecting two copies here */
      {
        std::cout << "B::<constructor>" << std::endl;
      }
    B (B const &b) : s_ (b.s_)
      {
        std::cout << "B::<copy constructor>" << std::endl;
      }
    ~B ()
      {
        std::cout << "B::<destructor>" << std::endl;
      }
  };

/* expecting a total of one copy of S here */
static A f () { S s ("string"); return A (s); }

/* expecting a total of one copy of S here */
static A f2 () { S s ("string"); s.s_ = "abc"; A a (s); a.s_.s_ = "a"; return a; }

/* expecting a total of two copies of S here */
static B g () { S s ("string"); return B (s); }

/* expecting a total of two copies of S here */
static B g2 () { S s ("string"); s.s_ = "abc"; B b (s); b.s_.s_ = "b"; return b; }

int main ()
  {
    A a (f ());
    std::cout << "" << std::endl;
    A a2 (f2 ());
    std::cout << "" << std::endl;
    B b (g ());
    std::cout << "" << std::endl;
    B b2 (g2 ());
    std::cout << "" << std::endl;
    
    return 0;
  }

不幸的是,输出符合预期:

S::<constructor>
  S::<copy constructor>
A::<constructor>
  S::<destructor>

  S::<constructor>
  S::<copy constructor>
A::<constructor>
  S::<destructor>

  S::<constructor>
  S::<copy constructor>
  S::<copy constructor>
B::<constructor>
  S::<destructor>
  S::<destructor>

  S::<constructor>
  S::<copy constructor>
  S::<copy constructor>
B::<constructor>
  S::<destructor>
  S::<destructor>

B::<destructor>
  S::<destructor>
B::<destructor>
  S::<destructor>
A::<destructor>
  S::<destructor>
A::<destructor>
  S::<destructor>
    • 结论**GCC无法 * 优化掉由B的构造函数创建的临时S。使用S的默认复制构造函数并没有改变这一点。将f, g更改为
static A f () { return A (S ("string")); } // still one copy
static B g () { return B (S ("string")); } // reduced to one copy!

确实具有预期效果。看起来GCC愿意在适当的位置构造B的构造函数的参数,但不愿意在适当的位置构造B的成员。请注意,仍然没有创建临时AB。这意味着a, a2, b, b2仍在建造中 * 在原地 *。很酷
现在让我们研究新的移动语义如何影响第二个例子。

    • 现实情况,-std=c++0x**考虑在S中添加以下构造函数:
S (S &&s) : s_ ()
      {
        std::swap (s_, s.s_);
        std::cout << "  S::<move constructor>" << std::endl;
      }

B的构造函数改为

B (S &&s) : s_ (std::move (s)) /* how many copies?? */
      {
        std::cout << "B::<constructor>" << std::endl;
      }

并将g()g2()更改为

static B g () { S s ("string"); return B (std::move(s)); }

static B g2 () { S s ("string"); s.s_ = "abc"; B b (std::move(s)); b.s_.s_ = "b"; return b; }

我们得到这个输出

S::<constructor>
  S::<copy constructor>
A::<constructor>
  S::<destructor>

  S::<constructor>
  S::<copy constructor>
A::<constructor>
  S::<destructor>

  S::<constructor>
  S::<move constructor>
B::<constructor>
  S::<destructor>

  S::<constructor>
  S::<move constructor>
B::<constructor>
  S::<destructor>

B::<destructor>
  S::<destructor>
B::<destructor>
  S::<destructor>
A::<destructor>
  S::<destructor>
A::<destructor>
  S::<destructor>

因此,我们能够通过使用rvalue传递将 * 四个副本 * 替换为 * 两个移动 *。

    • 但我们实际上构建了一个破碎的程序**

回想一下更新后的g()g2()

static B g ()  { S s ("string"); return B (std::move(s)); /* s is zombie now */ }
static B g2 () { S s ("string"); s.s_ = "abc"; B b (std::move(s)); /* s is zombie now */ b.s_.s_ = "b"; return b; }

标记的位置显示问题。在非临时对象上执行了移动。s将处于有效但未指定的状态。因为右值引用的行为与左值引用类似,只是它们绑定到右值,所以我们不得不将s强制转换为右值,以便将其传递给B的构造函数,该构造函数现在接受右值引用。所以我们一定不要忘记用一个常量左值引用的构造函数重载B的构造函数。

B (S const &s) : s_ (s)
      {
        std::cout << "B::<constructor2>" << std::endl;
      }

然后你会注意到,这两个g, g2都会导致“constructor2”被调用,因为符号s在任何情况下都更适合const引用而不是右值引用。我们可以通过以下两种方式之一说服编译器在g中执行移动:

static B g ()  { return B (S ("string")); }
static B g ()  { S s ("string"); return B (std::move (s)); }
    • 结论**
    • 按值返回。该代码将比“填充我给你的引用”代码更具可读性 *,并且 * 更快 甚至可能更加异常安全。**考虑将f更改为
static void f (A &result) { A tmp; /* ... */ result = tmp; } /* or */
static void f (A &result) { /* ... */ result = A (S ("string")); }

只有当A的赋值提供时,它才能满足strong guarantee。无法跳过到result的复制,也无法构建tmp来代替result,因为result尚未构建。因此,它比以前慢,在那里不需要复制。C ++0x编译器和移动赋值操作符会减少开销,但它仍然比按值返回慢。
按值回报更容易提供强有力的保障。该对象在适当位置构造。如果其中的一部分失效,而其他部分已经构造完毕,则正常的展开将被清理干净,只要S的构造器满足了关于其自身成员的基本保证和关于全局项的强保证,整个按值返回过程实际上提供了强保证。

    • 无论如何,如果你要复制(到堆栈上),总是按值传递Want speed? Pass by value.中所讨论的。编译器可以生成代码,如果可能的话,在适当的位置构造调用者的参数,消除复制,当您通过引用获取然后手动复制时,它不能这样做。主要示例:不要*写这篇文章(摘自引用文章)
T& T::operator=(T const& x) // x is a reference to the source
{ 
    T tmp(x);          // copy construction of tmp does the hard work
    swap(*this, tmp);  // trade our resources for tmp's
    return *this;      // our (old) resources get destroyed with tmp 
}

但总是喜欢这个

T& T::operator=(T x)    // x is a copy of the source; hard work already done
{
    swap(*this, x);  // trade our resources for x's
    return *this;    // our (old) resources get destroyed with x
}
    • 如果你想复制到一个非栈帧位置,在C ++0x之前通过常量引用传递,在C ++0x之后通过右值引用传递**我们已经看到了这一点。当不可能进行就地构造时,按引用传递比按值传递产生更少的副本。C ++0x的移动语义可能会用更少更便宜的移动来取代许多副本。但是请记住,移动会使被移动的物体变成僵尸。移动不是复制。仅仅提供一个接受右值引用的构造函数可能会破坏一些东西,如上所示。
    • 如果你想复制到一个非栈帧位置,并且有swap,无论如何考虑通过值传递(C ++0x之前)**如果你有便宜的默认构造,与swap * 结合可能 * 比复制东西更有效。假设S的构造函数为
S (std::string s) : s_ (/* is this cheap for your std::string? */)
      {
        s_.swap (s); /* then this may be faster than copying */
        std::cout << "  S::<constructor>" << std::endl;
      }

相关问题