构造函数的最佳形式?通过价值或参考?

Cli*_*ton 26 c++ constructor pass-by-value rvalue rvalue-reference

我想知道我的构造者最好的形式.以下是一些示例代码:

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)
}
Run Code Online (Sandbox Code Playgroud)

据我所知,这是编译器在每种情况下都能做到的最好的.

(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 move)

现在几个问题:

  1. 在这两个方面,传递const引用并不差,有时候比传递值更好.这似乎违背了"想要速度?通过价值"的讨论..对于C++(不是C++ 0x),我应该坚持使用const引用作为这些构造函数,还是应该通过值传递?对于C++ 0x,我应该通过rvalue引用传递值吗?

  2. 对于(2),如果临时直接构造成x.m_y,我更喜欢.我认为即使是rvalue版本也需要一个移动,除非对象分配动态内存,否则移动与复制一样多.有没有办法对此进行编码,以便允许编译器避免这些副本和移动?

  3. 我已经在我认为编译器可以做得最好的事情和我自己的问题中做了很多假设.如果不正确,请更正其中任何一项.

den*_*ane 40

我把一些例子拼凑在一起.我在所有这些中都使用了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;
  }
Run Code Online (Sandbox Code Playgroud)

该计划的输出stdout如下:

A::<constructor>
A::<constructor>
B::<constructor>
B::<constructor>
B::<destructor>
B::<destructor>
A::<destructor>
A::<destructor>
Run Code Online (Sandbox Code Playgroud)

结论

海湾合作委员会能够优化每一个临时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;
  }
Run Code Online (Sandbox Code Playgroud)

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

  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>
Run Code Online (Sandbox Code Playgroud)

结论

GCC 无法优化SB构造函数创建的临时文件.使用默认的复制构造函数S没有改变.改变f, g

static A f () { return A (S ("string")); } // still one copy
static B g () { return B (S ("string")); } // reduced to one copy!
Run Code Online (Sandbox Code Playgroud)

确实有指示的效果.似乎海湾合作委员会愿意为构建者构建论证,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;
      }
Run Code Online (Sandbox Code Playgroud)

并将B构造函数更改为

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

我们得到这个输出

  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>
Run Code Online (Sandbox Code Playgroud)

所以,我们能够代替四个拷贝两个动作用路过右值.

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

召回 g, g2

static B g ()  { S s ("string"); return B (s); }
static B g2 () { S s ("string"); s.s_ = "abc"; B b (s); /* s is zombie now */ b.s_.s_ = "b"; return b; }
Run Code Online (Sandbox Code Playgroud)

标记的位置显示问题.对一个不是临时的对象进行了移动.这是因为右值引用的行为类似于左值引用,除了它们也可以绑定到临时值.所以我们不要忘记B使用一个带有常量左值引用的构造函数重载.

    B (S const &s) : s_ (s)
      {
        std::cout << "B::<constructor2>" << std::endl;
      }
Run Code Online (Sandbox Code Playgroud)

然后您会注意到两者都会g, g2导致调用"constructor2",因为s在任何一种情况下,符号都更适合const引用而不是rvalue引用.我们可以说服编译器以g两种方式之一进行移动:

static B g ()  { return B (S ("string")); }
static B g ()  { S s ("string"); return B (std::move (s)); }
Run Code Online (Sandbox Code Playgroud)

结论

按价值返回.该代码将更具可读性比"补参考我给你"代码更快的甚至更异常安全.

考虑f改为

static void f (A &result) { A tmp; /* ... */ result = tmp; } /* or */
static void f (A &result) { /* ... */ result = A (S ("string")); }
Run Code Online (Sandbox Code Playgroud)

只有在任务提供时,这才能满足强有力的保证A.复制到result不能被跳过,也不能tmp构造代替result,因为result没有被构造.因此,它比以前更慢,不需要复制.C++ 0x编译器和移动赋值运算符会减少开销,但它仍然比返回值慢.

按价值返回更容易提供强有力的保证.对象构造在适当的位置.如果其中一部分失效而其他部分已经建成,则正常的清理将被清理,并且只要S构造者履行其自身成员的基本保证和全球项目的强有力保证,整个回报实际价值过程实际上提供了有力的保障.

如果您要复制(到堆栈上),总是按值传递

想要速度?通过价值..编译器可以生成代码,如果可能的话,构造调用者的参数,消除副本,当您通过引用获取然后手动复制时,它不能执行.校长例如:请不要写这个(从引用的文章取)

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 
}
Run Code Online (Sandbox Code Playgroud)

但总是喜欢这个

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
}
Run Code Online (Sandbox Code Playgroud)

如果你想复制到非堆栈帧位置传递const引用前C++ 0x并另外传递rvalue引用后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;
      }
Run Code Online (Sandbox Code Playgroud)