c++ - tipos - ¿Mejor forma para constructores? ¿Pasar por valor o por referencia?
parametros por valor y por referencia java (1)
He juntado algunos ejemplos. Utilicé GCC 4.4.4 en todo esto.
Caso simple, sin -std=c++0x
Primero, armé un ejemplo muy simple con dos clases que aceptan una std::string
cada una.
#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;
}
La salida de ese programa en la salida stdout
es la siguiente:
A::<constructor>
A::<constructor>
B::<constructor>
B::<constructor>
B::<destructor>
B::<destructor>
A::<destructor>
A::<destructor>
Conclusión
GCC fue capaz de optimizar todos y cada uno temporal A
o B
distancia. Esto es consistente con las preguntas frecuentes de C ++ . Básicamente, GCC puede (y está dispuesto a) generar código que construya a, a2, b, b2
en su lugar , incluso si se llama a una función que aparentemente devuelve por valor. De este modo, GCC puede evitar muchos de los temporales cuya existencia se podría haber "deducido" al mirar el código.
Lo siguiente que queremos ver es la frecuencia con la que std::string
se copia en el ejemplo anterior. Vamos a reemplazar std::string
por algo que podamos observar mejor y ver.
Caso realista, sin -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;
}
Y la salida, desafortunadamente, cumple con las expectativas:
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>
Conclusión
GCC no pudo optimizar la S
temporal creada por el constructor de B
Usar el constructor de copia por defecto de S
no cambió eso. Cambiando f, g
para ser
static A f () { return A (S ("string")); } // still one copy
static B g () { return B (S ("string")); } // reduced to one copy!
Tuvo el efecto indicado. Parece que GCC está dispuesto a construir el argumento para el constructor de B
en su lugar, pero se muestra reacio a construir el miembro de B
en su lugar. Tenga en cuenta que todavía no se crean A
o B
temporales. Eso significa que a, a2, b, b2
todavía se están construyendo en su lugar . Guay.
Ahora investiguemos cómo la nueva semántica del movimiento puede influir en el segundo ejemplo.
Caso realista, con -std=c++0x
Considere agregar el siguiente constructor a S
S (S &&s) : s_ ()
{
std::swap (s_, s.s_);
std::cout << " S::<move constructor>" << std::endl;
}
Y cambiando el constructor de B
por
B (S &&s) : s_ (std::move (s)) /* how many copies?? */
{
std::cout << "B::<constructor>" << std::endl;
}
Obtenemos esta salida.
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>
Por lo tanto, pudimos reemplazar cuatro copias con dos movimientos utilizando el paso por rvalor.
Pero en realidad construimos un programa roto.
Recuerdo 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; }
La ubicación marcada muestra el problema. Se realizó un movimiento en un objeto que no es temporal. Esto se debe a que las referencias de rvalue se comportan como referencias de lvalue, excepto que también pueden vincularse a temporales. Así que no debemos olvidar sobrecargar el constructor de B
con uno que toma una referencia de valor constante.
B (S const &s) : s_ (s)
{
std::cout << "B::<constructor2>" << std::endl;
}
Luego notará que tanto g, g2
hacen que se llame "constructor2", ya que el símbolo s
en cualquier caso es mejor para una referencia constante que para una referencia rvalue. Podemos persuadir al compilador para que haga un movimiento en g
de dos maneras:
static B g () { return B (S ("string")); }
static B g () { S s ("string"); return B (std::move (s)); }
Conclusiones
Hacer retorno por valor. El código será más legible que el código de "llenar una referencia que te doy" y más rápido y tal vez incluso más seguro de excepción.
Considere cambiar f
a
static void f (A &result) { A tmp; /* ... */ result = tmp; } /* or */
static void f (A &result) { /* ... */ result = A (S ("string")); }
Eso cumplirá con la fuerte garantía solo si la asignación de A lo proporciona. La copia en el result
no se puede omitir, ni tmp
puede construir en lugar del result
, ya que el result
no se está construyendo. Por lo tanto, es más lento que antes, donde no era necesario copiar. Los compiladores de C ++ 0x y los operadores de asignación de movimiento reducirían la sobrecarga, pero aún es más lento que el retorno por valor.
Devolución por valor proporciona la garantía fuerte más fácilmente. El objeto se construye en su lugar. Si una parte de eso falla y otras partes ya se han construido, el desenrollado normal se limpiará y, mientras el constructor de S
cumpla con la garantía básica con respecto a sus propios miembros y con la garantía sólida con respecto a los artículos globales, el conjunto El proceso de retorno por valor en realidad proporciona la garantía sólida.
Siempre pase por valor si va a copiar (en la pila) de todos modos
Como se discute en ¿Quieres velocidad? Pase por valor. . El compilador puede generar un código que construya, si es posible, el argumento de la persona que llama en su lugar, eliminando la copia, que no puede hacer cuando toma por referencia y luego la copia manualmente. Ejemplo principal: NO escriba esto (tomado del artículo citado)
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
}
pero siempre prefiero esto
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
}
Si desea copiar a una ubicación de trama no apilada, pase la referencia constante pre C ++ 0x y, además, pase la referencia de rvalue post C ++ 0x
Ya vimos esto. Pasar por referencia hace que se realicen menos copias cuando la construcción en el lugar es imposible que pasar por valor. Y la semántica de movimientos de C ++ 0x puede reemplazar muchas copias con movimientos menos costosos. Pero ten en cuenta que moverte hará que un zombi salga del objeto del que se ha movido. Mover no es copiar. El solo hecho de proporcionar un constructor que acepte referencias de valor de r, puede romper cosas, como se muestra arriba
Si desea copiar en una ubicación de trama no apilada y tener swap
, considere pasar por valor de todos modos (antes C ++ 0x)
Si tiene una construcción por defecto barata, eso combinado con un swap
puede ser más eficiente que copiar cosas. Considerar que el constructor de S
es
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;
}
Me pregunto cuál es la mejor forma para mis constructores. Aquí hay un código de ejemplo:
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)
}
Por lo que entiendo, esto es lo mejor que el compilador puede hacer en cada situación.
(1a) y se copia en x1.m_y (1 copia)
(1b) y se copia en el argumento del constructor de X, y luego se copia en x1.m_y (2 copias)
(1c) y se mueve a x1.m_y (1 movimiento)
(2a) el resultado de f () se copia en x2.m_y (1 copia)
(2b) f () se construye en el argumento del constructor y luego se copia en x2.m_y (1 copia)
(2c) f () se crea en la pila y luego se mueve a x2.m_y (1 movimiento)
Ahora algunas preguntas:
En ambos casos, la referencia de paso por constante no es peor, ya veces es mejor que pasar por valor. Esto parece ir en contra de la discusión sobre "¿Quieres velocidad? Pasar por valor". . Para C ++ (no C ++ 0x), ¿debería atenerme a la referencia paso a paso para estos constructores, o debo pasar por valor? Y para C ++ 0x, ¿debo pasar por referencia de valor sobre pasar por valor?
Para (2), preferiría que el temporal se construyera directamente en x.m_y. Incluso la versión rvalue creo que requiere un movimiento que, a menos que el objeto asigne memoria dinámica, es tanto trabajo como una copia. ¿Hay alguna forma de codificar esto para que el compilador tenga permiso para evitar estas copias y movimientos?
He hecho muchas suposiciones tanto en lo que creo que el compilador puede hacer mejor como en mis propias preguntas. Por favor corrija cualquiera de estos si son incorrectos.