Durata garantita temporanea in C ++?


103

Il C ++ fornisce una garanzia per la durata di una variabile temporanea creata all'interno di una chiamata di funzione ma non utilizzata come parametro? Ecco un esempio di classe:

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

Ed ecco come lo useresti:

// 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);

Quando verrà chiamato il distruttore per l'oggetto StringBuffer temporaneo? È:

  • Prima della chiamata a GetString?
  • Dopo che GetString è tornato?
  • Dipendente dal compilatore?

So che il C ++ garantisce che una variabile temporanea locale sarà valida fintanto che c'è un riferimento ad essa: questo si applica agli oggetti padre quando c'è un riferimento a una variabile membro?

Grazie.


perché non ereditare e sovraccaricare o creare una funzione globale? Sarei più pulito e non dovresti creare una classe solo per chiamare un membro.
Jacek Ławrynowicz

1
Se avete intenzione di utilizzare questo, si dovrebbe chiamare m_str.reserve(maxlength)in char * Size(int maxlength), in caso contrario il distruttore potrebbe gettare.
Mankarse

Risposte:


109

Il distruttore per quel tipo di provvisori viene chiamato alla fine dell'espressione completa. Questa è l'espressione più esterna che non fa parte di nessun'altra espressione. Questo è nel tuo caso dopo che la funzione ritorna e il valore viene valutato. Quindi, funzionerà tutto bene.

È infatti ciò che fa funzionare i modelli di espressione: possono mantenere i riferimenti a quel tipo di temporanei in un'espressione come

e = a + b * c / d

Perché ogni temporaneo durerà fino all'espressione

x = y

Viene valutato completamente. È descritto in modo abbastanza conciso 12.2 Temporary objectsnello Standard.


3
Non sono mai riuscito a ottenere una copia dello standard. Dovrei renderlo una priorità.
Mark Ransom

2
@JohannesSchaub: Che cos'è una "espressione completa" in questo caso:? printf("%s", strdup(std::string("$$$").c_str()) );Voglio dire, se strdup(std::string("$$$").c_str())viene presa come espressione completa, allora il puntatore che strdupvede è valido . Se std::string("$$$").c_str()è un'espressione completa, il puntatore che strdupvede non è valido ! Potresti spiegare un po 'di più sulla base di questo esempio?
Grim Fandango

2
@GrimFandango AIUI il tuo intero printfè la piena espressione. Quindi strdupè una perdita di memoria non necessaria: puoi semplicemente lasciare che stampi c_str()direttamente.
Josh Stone

1
"Quel tipo di provvisori" - Che tipo è? Come posso sapere se il mio temporaneo è "quel tipo" di temporaneo?
RM

@ RM abbastanza giusto. Intendevo "quelli che crei all'interno di un argomento di funzione", come si fa nella domanda.
Johannes Schaub - litb

18

la risposta di litb è accurata. La durata dell'oggetto temporaneo (noto anche come valore r) è legata all'espressione e il distruttore per l'oggetto temporaneo viene chiamato alla fine dell'espressione completa e quando viene chiamato il distruttore su StringBuffer, sarà anche il distruttore su m_buffer chiamato, ma non il distruttore su m_str poiché è un riferimento.

Nota che C ++ 0x cambia le cose solo un po 'perché aggiunge riferimenti rvalue e sposta la semantica. Essenzialmente utilizzando un parametro di riferimento rvalue (annotato con &&) posso "spostare" il valore rvalue nella funzione (invece di copiarlo) e la durata di rvalue può essere vincolata all'oggetto in cui si muove, non all'espressione. C'è un ottimo post del blog del team di MSVC su questo argomento in grande dettaglio e incoraggio le persone a leggerlo.

L'esempio pedagogico per spostare i rvalue sono le stringhe temporanee e mostrerò l'assegnazione in un costruttore. Se ho una classe MyType che contiene una variabile membro di stringa, può essere inizializzata con un rvalue nel costruttore in questo modo:

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

Questo è bello perché quando dichiaro un'istanza di questa classe con un oggetto temporaneo:

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

quello che succede è che evitiamo di copiare e distruggere l'oggetto temporaneo e "hello" viene posizionato direttamente all'interno della variabile membro dell'istanza della classe proprietaria. Se l'oggetto ha un peso maggiore di una "stringa", la chiamata aggiuntiva alla copia e al distruttore può essere significativa.


1
Per far funzionare lo spostamento, penso che sia necessario rilasciare const e utilizzare std :: move come MyType (std :: string && name): m_name (std :: move (name)) {}
gast128


3

StringBuffer rientra nell'ambito di GetString. Dovrebbe essere distrutto alla fine dell'ambito di GetString (cioè quando ritorna). Inoltre, non credo che C ++ garantirà che una variabile esisterà finché c'è un riferimento.

Il seguente dovrebbe essere compilato:

Object* obj = new Object;
Object& ref = &(*obj);
delete obj;

Penso di aver esagerato con la garanzia: è solo per i provvisori locali. Ma esiste.
Mark Ransom

Ho modificato la domanda. Sulla base delle risposte fornite finora, sembra comunque essere un punto controverso.
Mark Ransom

Continuo a non pensare che la tua modifica sia corretta: Object & obj = GetObj (); Oggetto & GetObj () {return & Object (); } // bad - lascerà un riferimento penzolante.
BigSandwich

1
Ovviamente sto facendo un pessimo lavoro nello spiegarmi, e potrei anche non capire al 100%. Guarda informit.com/guides/content.aspx?g=cplusplus&seqNum=198 - spiega e risponde anche alla mia domanda originale.
Mark Ransom

1
Grazie per il collegamento, ora ha senso.
BigSandwich,

3

Ho scritto quasi esattamente la stessa classe:

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

Prima dello standard, ogni compilatore lo faceva in modo diverso. Credo che il vecchio Annotated Reference Manual per C ++ specificasse che i provvisori dovevano essere ripuliti alla fine dell'ambito, quindi alcuni compilatori lo hanno fatto. Fino al 2003, ho scoperto che il comportamento esisteva ancora per impostazione predefinita sul compilatore Forte C ++ di Sun, quindi StringBuffer non funzionava. Ma sarei stupito se qualche compilatore attuale fosse ancora così rotto.


Spettrale quanto sono simili! Grazie per l'avvertimento: il primo posto in cui proverò è VC ++ 6, che non è noto per la sua conformità agli standard. Starò a guardare attentamente.
Mark Ransom

Avrei scritto la classe originariamente su VC ++ 6 quindi non dovrebbe essere un problema.
Daniel Earwicker,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.