最好的形式为构造函数?通过值或引用? [英] Best form for constructors? Pass by value or reference?

查看:172
本文介绍了最好的形式为构造函数?通过值或引用?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我想知道我的构造函数的最佳形式。下面是一些示例代码:

  class Y {...} 

class X
{
public:
X(const Y& y):m_y(y){} //(a)
X(Y y):m_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)可以在每种情况下执行



< (1b)y被复制到X的构造函数的参数中,然后复制到x1.m_y(2个副本)



(1c)y被移动到x1。 m_y(1 move)



(2a)将f()的结果复制到x2.m_y(1个副本)



(2b)f()构造到构造函数的参数中,然后复制到x2.m_y(1 copy)



在堆栈上创建,然后移动到x2.m_y(1 move)



现在有几个问题:


  1. 在这两个计数中,传递const引用不会更糟,有时比传递值更好。这似乎违反了有关想要速度?按值传递的讨论。。对于C ++(不是C ++ 0x),我应该坚持通过const引用这些构造函数,还是应该传递值?对于C ++ 0x,我应该通过值传递的右值引用吗?


  2. 对于(2)直接构造到x.m_y中。即使是右值版本,我认为需要一个移动,除非对象分配动态内存,是一样多的工作作为副本。有没有办法编码这样的编译器是允许避免这些副本和移动?


  3. 我做了很多假设在我认为编译器可以做最好的和在我的问题自己。



解决方案

扔在一起一些例子。我在所有这些中使用了GCC 4.4.4。



简单的情况,没有 -std = c ++ 0x



首先,我提出了一个简单的例子,两个类接受 std :: string / p>

  #include< string> 
#include< iostream>

struct A / *通过引用构造* /
{
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 / *按值构造* /
{
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>



结论



能够优化每个临时 A B
这与 C ++常见问题。基本上,即使调用函数,GCC也可以(并愿意)生成构建 a,a2,b,b2 看起来通过价值返回。因此,GCC可以通过查看代码避免许多临时性的存在可能推断。



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



现实情况, $ c> -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 / *通过引用构造* /
{
S s;

A(S 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 / *按值构造* /
{
S s_;

B(S 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;
}
};

/ *期望这里总共有一个S副本* /
static A f(){S s(string); return A(s); }

/ *期望这里总共有一个S副本* /
static A f2(){S s(string); s.s_ =abc; A a(s); a.s_.s_ =a; return a; }

/ *期望这里总共有两个S副本* /
static B g(){S s(string);返回B(s); }

/ *期望S的总计两个副本* /
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 ::



结论



不是能够优化由 B 的构造函数创建的临时 S 。使用 S 的默认拷贝构造函数并没有改变。将 f,g 更改为

  static A f A(S(string)); } //仍然一个副本
static B g(){return B(S(string)); } //简化为一个副本!

确实有效果。看起来GCC愿意构造 B 的构造函数的参数,但犹豫要构造 B 的参数成员到位。
请注意,仍然没有创建临时 A B 。这意味着 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有多少份? * / 
{
std :: cout< B ::< constructor> << std :: endl;
}

我们得到这个输出

  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 :: A ::< destructor>
S ::< destructor>

因此,我们可以用



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

召回 g,g2

  (){S s(string);返回B(s); } 
static B g2(){S s(string); s.s_ =abc; B b(s); / * s is zombie now * / b.s_.s_ =b; return b; }

标记的位置显示问题。对不是临时的对象执行了移动。这是因为右值引用的行为像左值引用,除非它们也可以绑定到临时。因此,我们不能忘记重载 B 的构造函数,并使用一个带有常量lvalue引用的构造函数。

  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 & result){A tmp; / * ... * / result = tmp; } / *或* / 
static void f(A& result){/ * ... * / result = A(S(string)); }

这将满足强保证,只有 A 的分配提供它。不能跳过到 result 中的副本,也不能构造 tmp 来代替 result ,因为 result 没有被构造。因此,它比以前更慢,其中不需要复制。 C ++ 0x编译器和移动赋值运算符将减少开销,但它仍然慢于按值返回。



返回值提供了强大的保证更容易。对象被构造就位。如果失败的一部分和其他部分已经构建,正常的解开将清理,并且只要 S 的构造函数满足其自身的基本保证成员和对全局项的强力保证,整个按值返回过程实际上提供了有力的保证。



如果你要去



想要速度?按值传递。。编译器可以生成代码,如果可能的话,构造调用者的参数,消除副本,它不能做,当你通过引用,然后手动复制。主要示例:
执行 NOT 写入(从引用的文章中获取)

  T :: operator =(T const& x)// x是对源的引用
{
T tmp(x); // copy construction of tmp does the hard work
swap(* this,tmp); //交易我们的资源tmp的
return * this; //我们的(旧)资源被销毁与tmp
}

/ p>

  T :: operator =(T x)// x是源的副本;辛苦工作已经做了
{
swap(* this,x); //交易我们的资源for x's
return * this; //我们的(旧的)资源被销毁与x
}



到非栈帧位置通过const引用pre C ++ 0x和另外通过rvalue引用后C ++ 0x



我们已经看到了这一点。通过引用引起在现场构造不可能比通过值更少时发生更少的副本。而C ++ 0x的移动语义可以用更少和更便宜的移动来替换许多副本。但请记住,移动会使僵尸从已经移动的对象。移动不是复制。只要提供一个接受右值引用的构造函数可能会破坏事情,如上所示。



如果要复制到非堆栈框架位置并且具有 swap ,考虑通过值传递(pre C ++ 0x)



如果你有廉价的默认构造, c $ c> swap 比复制内容更有效率。考虑 S 的构造函数为

  S s):s_(/ *对于你的std :: string是便宜的* /)
{
s_.swap(s); / *然后这可能比复制更快* /
std :: cout<< S ::< constructor> << std :: endl;
}


I'm wondering the best form for my constructors. Here is some sample code:

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)
}

From what I understand, this is the best the compiler can do in each situation.

(1a) y is copied into x1.m_y (1 copy)

(1b) y is copied into the argument of the constructor of X, and then copied into x1.m_y (2 copies)

(1c) y is moved into x1.m_y (1 move)

(2a) result of f() is copied into x2.m_y (1 copy)

(2b) f() is constructed into the argument of the constructor, and then copied to x2.m_y (1 copy)

(2c) f() is created on the stack, and then moved into x2.m_y (1 move)

Now a few questions:

  1. On both counts, pass by const reference is no worse, and sometimes better than pass by value. This seems to go against the discussion on "Want Speed? Pass by Value.". For C++ (not C++0x), should I stick with pass by const reference for these constructors, or should I go pass by value? And for C++0x, should I do pass by rvalue reference over pass by value?

  2. For (2), I'd prefer if the temporary was constructed directly into x.m_y. Even the rvalue version I think requires a move which, unless the object allocates dynamic memory, is as much work as a copy. Is there any way to code this so the compiler is permitted to avoid these copies and moves?

  3. I've made a lot of assumptions in both what I think the compiler can do best and in my questions themselves. Please correct any of these if they are incorrect.

解决方案

I've thrown together some examples. I used GCC 4.4.4 in all of this.

Simple case, without -std=c++0x

First, I put together a very simple example with two classes that accept an std::string each.

#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;
  }

The output of that program on stdout is as follows:

A::<constructor>
A::<constructor>
B::<constructor>
B::<constructor>
B::<destructor>
B::<destructor>
A::<destructor>
A::<destructor>

Conclusion

GCC was able to optimize each and every temporary A or B away. This is consistent with the C++ FAQ. Basically, GCC may (and is willing to) generate code that constructs a, a2, b, b2 in place, even if a function is called that appearantly returns by value. Thereby GCC can avoid many of the temporaries whose existence one might have "inferred" by looking at the code.

The next thing we want to see is how often std::string is actually copied in the above example. Let's replace std::string with something we can observe better and see.

Realistic case, without -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;
  }

And the output, unfortunately, meets the expectation:

  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>

Conclusion

GCC was not able to optimize away the temporary S created by B's constructor. Using the default copy constructor of S did not change that. Changing f, g to be

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

did have the indicated effect. It appears that GCC is willing to construct the argument to B's constructor in place but hesitant to construct B's member in place. Do note that still no temporary A or B are created. That means a, a2, b, b2 are still being constructed in place. Cool.

Let's now investigate how the new move semantics may influence the second example.

Realistic case, with -std=c++0x

Consider adding the following constructor to S

    S (S &&s) : s_ ()
      {
        std::swap (s_, s.s_);
        std::cout << "  S::<move constructor>" << std::endl;
      }

And changing B's constructor to

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

We get this output

  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>

So, we were able to replace four copies with two moves by using pass by rvalue.

But we actually constructed a broken program.

Recall 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; }

The marked location shows the problem. A move was done on an object that is not a temporary. That's because rvalue references behave like lvalue references except they may also bind to temporaries. So we must not forget to overload B's constructor with one that takes a constant lvalue reference.

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

You will then notice that both g, g2 cause "constructor2" to be called, since the symbol s in either case is a better fit for a const reference than for an rvalue reference. We can persuade the compiler to do a move in g in either of two ways:

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

Conclusions

Do return-by-value. The code will be more readable than "fill a reference I give you" code and faster and maybe even more exception safe.

Consider changing f to

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

That will meet the strong guarantee only if A's assignment provides it. The copy into result cannot be skipped, neither can tmp be constructed in place of result, since result is not being constructed. Thus, it is slower than before, where no copying was necessary. C++0x compilers and move assignment operators would reduce the overhead, but it's still slower than to return-by-value.

Return-by-value provides the strong guarantee more easily. The object is constructed in place. If one part of that fails and other parts have already been constructed, normal unwinding will clean up and, as long as S's constructor fulfills the basic guarantee with regard to its own members and the strong guarantee with regard to global items, the whole return-by-value process actually provides the strong guarantee.

Always pass by value if you're going to copy (onto the stack) anyway

As discussed in Want speed? Pass by value.. The compiler may generate code that constructs, if possible, the caller's argument in place, eliminating the copy, which it cannot do when you take by reference and then copy manually. Principal example: Do NOT write this (taken from cited article)

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 
}

but always prefer this

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
}

If you want to copy to a non-stack frame location pass by const reference pre C++0x and additionally pass by rvalue reference post C++0x

We already saw this. Pass by reference causes less copies to take place when in place construction is impossible than pass by value. And C++0x's move semantics may replace many copies with fewer and cheaper moves. But do keep in mind that moving will make a zombie out of the object that has been moved from. Moving is not copying. Just providing a constructor that accepts rvalue references may break things, as shown above.

If you want to copy to a non-stack frame location and have swap, consider passing by value anyway (pre C++0x)

If you have cheap default construction, that combined with a swap may be more efficient than copying stuff around. Consider S's constructor to be

    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;
      }

这篇关于最好的形式为构造函数?通过值或引用?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆