Garantierte Lebensdauer der temporären in C ++?

103

Bietet C ++ eine Garantie für die Lebensdauer einer temporären Variablen, die innerhalb eines Funktionsaufrufs erstellt, aber nicht als Parameter verwendet wird? Hier ist eine Beispielklasse:

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

Und so würden Sie es verwenden:

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

Wann wird der Destruktor für das temporäre StringBuffer-Objekt aufgerufen? Ist es:

  • Vor dem Aufruf von GetString?
  • Nachdem GetString zurückgekehrt ist?
  • Compiler abhängig?

Ich weiß, dass C ++ garantiert, dass eine lokale temporäre Variable gültig ist, solange ein Verweis darauf vorhanden ist. Gilt dies für übergeordnete Objekte, wenn ein Verweis auf eine Mitgliedsvariable vorhanden ist?

Vielen Dank.

Mark Ransom
quelle
Warum nicht erben und überladen oder eine globale Funktion erstellen? Ich wäre sauberer und Sie müssten keine Klasse erstellen, um ein Mitglied anzurufen.
Jacek Ławrynowicz
1
Wenn Sie diese verwenden wollen, sollten Sie rufen m_str.reserve(maxlength)in char * Size(int maxlength), sonst könnte der destructor werfen.
Mankarse

Antworten:

109

Der Destruktor für diese Art von Temporären wird am Ende des vollständigen Ausdrucks aufgerufen. Das ist der äußerste Ausdruck, der nicht Teil eines anderen Ausdrucks ist. Dies ist in Ihrem Fall der Fall, nachdem die Funktion zurückgegeben und der Wert ausgewertet wurde. Also wird alles gut funktionieren.

Es ist in der Tat das, was Ausdrucksvorlagen zum Funktionieren bringt: Sie können Verweise auf diese Art von Temporären in einem Ausdruck wie enthalten

e = a + b * c / d

Weil jedes temporäre bis zum Ausdruck dauern wird

x = y

Wird komplett ausgewertet. Es ist 12.2 Temporary objectsim Standard ziemlich kurz beschrieben .

Johannes Schaub - litb
quelle
3
Ich bin nie ganz dazu gekommen, eine Kopie des Standards zu bekommen. Ich sollte es zu einer Priorität machen.
Mark Ransom
2
@JohannesSchaub: Was ist ein „full-Ausdruck“ in diesem Fall: printf("%s", strdup(std::string("$$$").c_str()) );? Ich meine , wenn strdup(std::string("$$$").c_str())als den vollen Ausdruck genommen wird, dann der Zeiger, strdupist sieht gültig . Wenn std::string("$$$").c_str()ein voller Ausdruck ist, dass der Zeiger strdupsieht , ist ungültig ! Könnten Sie bitte anhand dieses Beispiels etwas mehr erklären?
Grim Fandango
2
@GrimFandango AIUI Ihr gesamtes printfist der vollständige Ausdruck. Somit strdupist das ein unnötiger Speicherverlust - Sie können es einfach c_str()direkt drucken lassen .
Josh Stone
1
"Diese Art von Provisorien" - Was ist das? Wie kann ich feststellen, ob mein Temporär "diese Art" von Temporär ist?
RM
@ RM fair genug. Ich meinte "diejenigen, die Sie innerhalb eines Funktionsarguments erstellen", wie es in der Frage getan wird.
Johannes Schaub - litb
18

Die Antwort von litb ist richtig. Die Lebensdauer des temporären Objekts (auch als r-Wert bezeichnet) ist an den Ausdruck gebunden, und der Destruktor für das temporäre Objekt wird am Ende des vollständigen Ausdrucks aufgerufen. Wenn der Destruktor in StringBuffer aufgerufen wird, wird auch der Destruktor in m_buffer aufgerufen aufgerufen, aber nicht der Destruktor auf m_str, da es sich um eine Referenz handelt.

Beachten Sie, dass C ++ 0x die Dinge nur ein wenig ändert, da es rWertreferenzen hinzufügt und die Semantik verschiebt. Im Wesentlichen kann ich mithilfe eines r-Wert-Referenzparameters (notiert mit &&) den r-Wert in die Funktion 'verschieben' (anstatt ihn zu kopieren), und die Lebensdauer des r-Werts kann an das Objekt gebunden werden, in das er verschoben wird, nicht an den Ausdruck. Es gibt einen wirklich guten Blog-Beitrag des MSVC-Teams, der dies ausführlich durchläuft, und ich ermutige die Leute, ihn zu lesen.

Das pädagogische Beispiel für das Verschieben von Werten sind temporäre Zeichenfolgen, und ich werde die Zuordnung in einem Konstruktor zeigen. Wenn ich eine Klasse MyType habe, die eine String-Member-Variable enthält, kann sie mit einem r-Wert im Konstruktor wie folgt initialisiert werden:

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

Das ist schön, denn wenn ich eine Instanz dieser Klasse mit einem temporären Objekt deklariere:

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

Was passiert ist, dass wir vermeiden, das temporäre Objekt zu kopieren und zu zerstören und "Hallo" direkt in der Mitgliedsvariablen der besitzenden Klasseninstanz platziert wird. Wenn das Objekt schwerer als eine 'Zeichenfolge' ist, kann der zusätzliche Aufruf von Kopie und Destruktor von Bedeutung sein.

Rick
quelle
1
Damit der Umzug funktioniert, müssen Sie die Konstante löschen und std :: move wie MyType (std :: string && name) verwenden: m_name (std :: move (name)) {}
gast128
4

Nach dem Aufruf von GetString kehrt zurück.

David Segonds
quelle
3

StringBuffer befindet sich im Bereich von GetString. Es sollte am Ende des Bereichs von GetString zerstört werden (dh wenn es zurückkehrt). Ich glaube auch nicht, dass C ++ garantiert, dass eine Variable existiert, solange es eine Referenz gibt.

Folgendes sollte kompiliert werden:

Object* obj = new Object;
Object& ref = &(*obj);
delete obj;
BigSandwich
quelle
Ich glaube, ich habe die Garantie überbewertet - sie gilt nur für lokale Provisorien. Aber es existiert.
Mark Ransom
Ich habe die Frage bearbeitet. Aufgrund der bisher gegebenen Antworten scheint dies jedoch ein strittiger Punkt zu sein.
Mark Ransom
Ich denke immer noch nicht, dass Ihre Bearbeitung korrekt ist: Object & obj = GetObj (); Object & GetObj () {return & Object (); } // schlecht - hinterlässt eine baumelnde Referenz.
BigSandwich
1
Ich erkläre mich offensichtlich schlecht und verstehe es möglicherweise auch nicht zu 100%. Schauen Sie sich informit.com/guides/content.aspx?g=cplusplus&seqNum=198 an - es erklärt und beantwortet auch meine ursprüngliche Frage.
Mark Ransom
1
Danke, für den Link, der jetzt Sinn macht.
BigSandwich
3

Ich habe fast genau die gleiche Klasse geschrieben:

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

Vor dem Standard hat jeder Compiler es anders gemacht. Ich glaube, das alte Annotated Reference Manual für C ++ hat festgelegt, dass Provisorien am Ende des Bereichs bereinigt werden sollen, also haben einige Compiler dies getan. Noch 2003 stellte ich fest, dass das Verhalten im Forte C ++ - Compiler von Sun standardmäßig noch vorhanden war, sodass StringBuffer nicht funktionierte. Aber ich wäre erstaunt, wenn ein aktueller Compiler noch so kaputt wäre.

Daniel Earwicker
quelle
Gruselig wie ähnlich sie sind! Vielen Dank für die Warnung - der erste Ort, an dem ich es versuchen werde, ist VC ++ 6, das nicht für seine Einhaltung von Standards bekannt ist. Ich werde genau hinschauen.
Mark Ransom
Ich hätte die Klasse ursprünglich auf VC ++ 6 geschrieben, also sollte es kein Problem sein.
Daniel Earwicker