¿Duración garantizada de temporal en C ++?

103

¿C ++ proporciona una garantía durante la vida útil de una variable temporal que se crea dentro de una llamada de función pero no se usa como parámetro? Aquí hay una clase de ejemplo:

class StringBuffer
{
public:
    StringBuffer(std::string & str) : m_str(str)
    {
        m_buffer.push_back(0);
    }
    ~StringBuffer()
    {
        m_str = &m_buffer[0];
    }
    char * Size(int maxlength)
    {
        m_buffer.resize(maxlength + 1, 0);
        return &m_buffer[0];
    }
private:
    std::string & m_str;
    std::vector<char> m_buffer;
};

Y así es como lo usaría:

// this is from a crusty old API that can't be changed
void GetString(char * str, int maxlength);

std::string mystring;
GetString(StringBuffer(mystring).Size(MAXLEN), MAXLEN);

¿Cuándo se llamará al destructor del objeto StringBuffer temporal? Lo es:

  • ¿Antes de la llamada a GetString?
  • ¿Después de que GetString regrese?
  • ¿Depende del compilador?

Sé que C ++ garantiza que una variable temporal local será válida siempre que haya una referencia a ella; ¿esto se aplica a los objetos principales cuando hay una referencia a una variable miembro?

Gracias.

Mark Ransom
fuente
¿Por qué no heredar y sobrecargar o hacer una función global? Sería más limpio y no tendrías que crear una clase solo para llamar a un miembro.
Jacek Ławrynowicz
1
Si se va a utilizar esto, usted debe llamar m_str.reserve(maxlength)en char * Size(int maxlength)caso contrario, el destructor podría lanzar.
Mankarse

Respuestas:

109

El destructor para ese tipo de temporales se llama al final de la expresión completa. Esa es la expresión más externa que no forma parte de ninguna otra expresión. Eso es en su caso después de que la función regrese y se evalúe el valor. Entonces, todo funcionará bien.

De hecho, es lo que hace que las plantillas de expresión funcionen: pueden mantener referencias a ese tipo de temporales en una expresión como

e = a + b * c / d

Porque cada temporal durará hasta la expresión

x = y

Se evalúa por completo. Se describe de manera bastante concisa 12.2 Temporary objectsen el Estándar.

Johannes Schaub - litb
fuente
3
Nunca llegué a conseguir una copia del estándar. Debería convertirlo en una prioridad.
Mark Ransom
2
@JohannesSchaub: ¿Qué es una "expresión completa" en este caso printf("%s", strdup(std::string("$$$").c_str()) );? Quiero decir, si strdup(std::string("$$$").c_str())se toma como la expresión completa, entonces el puntero que strdupve es válido . Si std::string("$$$").c_str()es una expresión completa, entonces el puntero que strdupve no es válido . ¿Podría explicar un poco más en base a este ejemplo?
Grim Fandango
2
@GrimFandango AIUI todo tu printfes la expresión completa. Por lo tanto, strdupes una pérdida de memoria innecesaria; puede dejar que imprima c_str()directamente.
Josh Stone
1
"Ese tipo de temporales" - ¿Qué tipo es ese? ¿Cómo puedo saber si mi temporal es "ese tipo" de temporal?
RM
@RM bastante justo. Me refiero a "los que creas dentro de un argumento de función", como se hace en la pregunta.
Johannes Schaub - litb
18

La respuesta de litb es precisa. El tiempo de vida del objeto temporal (también conocido como rvalue) está vinculado a la expresión y el destructor del objeto temporal se llama al final de la expresión completa y cuando se llama al destructor en StringBuffer, el destructor en m_buffer también será llamado, pero no el destructor en m_str ya que es una referencia.

Tenga en cuenta que C ++ 0x cambia las cosas solo un poco porque agrega referencias rvalue y mueve semántica. Esencialmente, al usar un parámetro de referencia rvalue (anotado con &&) puedo 'mover' el rvalue a la función (en lugar de copiarlo) y la vida útil del rvalue puede vincularse al objeto al que se mueve, no a la expresión. Hay una publicación de blog realmente buena del equipo de MSVC que explica esto con gran detalle y animo a la gente a leerlo.

El ejemplo pedagógico para mover rvalue son cadenas temporales y mostraré la asignación en un constructor. Si tengo una clase MyType que contiene una variable miembro de cadena, se puede inicializar con un rvalue en el constructor así:

class MyType{
   const std::string m_name;
public:
   MyType(const std::string&& name):m_name(name){};
}

Esto es bueno porque cuando declaro una instancia de esta clase con un objeto temporal:

void foo(){
    MyType instance("hello");
}

lo que sucede es que evitamos copiar y destruir el objeto temporal y "hola" se coloca directamente dentro de la variable miembro de la instancia de la clase propietaria. Si el objeto pesa más que una 'cadena', la copia adicional y la llamada al destructor pueden ser importantes.

Almiar
fuente
1
Para que el movimiento funcione, creo que debe eliminar la constante y usar std :: move como MyType (std :: string && name): m_name (std :: move (name)) {}
gast128
4

Después de que regrese la llamada a GetString.

David Segonds
fuente
3

StringBuffer está en el ámbito de GetString. Debería destruirse al final del alcance de GetString (es decir, cuando regrese). Además, no creo que C ++ garantice que existirá una variable siempre que haya una referencia.

Se debe compilar lo siguiente:

Object* obj = new Object;
Object& ref = &(*obj);
delete obj;
BigSandwich
fuente
Creo que exageré la garantía, es solo para temporeros locales. Pero existe.
Mark Ransom
Edité la pregunta. Sin embargo, según las respuestas proporcionadas hasta ahora, parece ser un punto discutible.
Mark Ransom
Todavía no creo que su edición sea correcta: Object & obj = GetObj (); Object & GetObj () {return & Object (); } // malo - dejará una referencia pendiente.
BigSandwich
1
Obviamente estoy haciendo un mal trabajo explicándome, y puede que tampoco entienda al 100%. Mire informit.com/guides/content.aspx?g=cplusplus&seqNum=198 ; también explica y responde mi pregunta original.
Mark Ransom
1
Gracias, por el enlace, eso tiene sentido ahora.
BigSandwich
3

Escribí casi exactamente la misma clase:

template <class C>
class _StringBuffer
{
    typename std::basic_string<C> &m_str;
    typename std::vector<C> m_buffer;

public:
    _StringBuffer(std::basic_string<C> &str, size_t nSize)
        : m_str(str), m_buffer(nSize + 1) { get()[nSize] = (C)0; }

    ~_StringBuffer()
        { commit(); }

    C *get()
        { return &(m_buffer[0]); }

    operator C *()
        { return get(); }

    void commit()
    {
        if (m_buffer.size() != 0)
        {
            size_t l = std::char_traits<C>::length(get());
            m_str.assign(get(), l);    
            m_buffer.resize(0);
        }
    }

    void abort()
        { m_buffer.resize(0); }
};

template <class C>
inline _StringBuffer<C> StringBuffer(typename std::basic_string<C> &str, size_t nSize)
    { return _StringBuffer<C>(str, nSize); }

Antes del estándar, cada compilador lo hacía de manera diferente. Creo que el antiguo Manual de referencia anotado para C ++ especificaba que los temporales deberían limpiarse al final del alcance, por lo que algunos compiladores hicieron eso. Todavía en 2003, descubrí que el comportamiento todavía existía de forma predeterminada en el compilador Forte C ++ de Sun, por lo que StringBuffer no funcionó. Pero me sorprendería si algún compilador actual estuviera tan roto.

Daniel Earwicker
fuente
¡Qué parecidos son! Gracias por la advertencia: el primer lugar donde lo intentaré es VC ++ 6, que no es conocido por su cumplimiento de estándares. Estaré observando con atención.
Mark Ransom
Habría escrito la clase originalmente en VC ++ 6, por lo que no debería ser un problema.
Daniel Earwicker