Gwarantowany czas życia tymczasowego w C ++?

103

Czy C ++ zapewnia gwarancję na cały okres istnienia zmiennej tymczasowej, która jest tworzona w wywołaniu funkcji, ale nie jest używana jako parametr? Oto przykładowa klasa:

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

A oto jak byś tego użył:

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

Kiedy zostanie wywołany destruktor tymczasowego obiektu StringBuffer? Czy to:

  • Przed wywołaniem GetString?
  • Po zwrocie GetString?
  • Zależny od kompilatora?

Wiem, że C ++ gwarantuje, że lokalna zmienna tymczasowa będzie ważna, dopóki istnieje do niej odwołanie - czy dotyczy to obiektów nadrzędnych, gdy istnieje odwołanie do zmiennej składowej?

Dzięki.

Mark Okup
źródło
dlaczego nie dziedziczyć i nie przeciążać ani nie tworzyć funkcji globalnej? Byłbym czystszy i nie musiałbyś tylko tworzyć klasy, aby zadzwonić do członka.
Jacek Ławrynowicz
1
Jeśli masz zamiar tego użyć, powinieneś zadzwonić, m_str.reserve(maxlength)w char * Size(int maxlength)przeciwnym razie destruktor może rzucić.
Mankarse

Odpowiedzi:

109

Destruktor dla tego rodzaju tymczasowych jest wywoływany na końcu pełnego wyrażenia. To najbardziej zewnętrzne wyrażenie, które nie jest częścią żadnego innego wyrażenia. Dzieje się tak w twoim przypadku po zwróceniu funkcji i ocenie wartości. Więc wszystko będzie dobrze działać.

W rzeczywistości to właśnie sprawia, że ​​szablony wyrażeń działają: mogą przechowywać odniesienia do tego rodzaju tymczasowych w wyrażeniu takim jak

e = a + b * c / d

Ponieważ każdy tymczasowy będzie trwał do wyrażenia

x = y

Jest oceniany w całości. Jest to dość zwięźle opisane w 12.2 Temporary objectsnormie.

Johannes Schaub - litb
źródło
3
Nigdy nie udało mi się zdobyć kopii standardu. Powinienem uczynić to priorytetem.
Mark Ransom
2
@JohannesSchaub: Co to jest „pełne wyrażenie” w tym przypadku:? printf("%s", strdup(std::string("$$$").c_str()) );Mam na myśli, że jeśli strdup(std::string("$$$").c_str())jest traktowane jako pełne wyrażenie, to wskaźnik, który strdupwidzi, jest prawidłowy . Jeśli std::string("$$$").c_str()jest pełnym wyrażeniem, to wskaźnik, który strdupwidzi, jest nieprawidłowy ! Czy mógłbyś wyjaśnić trochę więcej na podstawie tego przykładu?
Grim Fandango
2
@GrimFandango AIUI cała twoja printfjest pełna ekspresja. W związku z tym strdupjest to niepotrzebny wyciek pamięci - możesz po prostu pozwolić mu c_str()bezpośrednio wydrukować .
Josh Stone
1
„Tego rodzaju tymczasowe” - co to za rodzaj? Jak mogę sprawdzić, czy moje tymczasowe jest „tego rodzaju” tymczasowe?
RM
@RM wystarczająco fair. Miałem na myśli „te, które tworzysz w ramach argumentu funkcji”, tak jak jest to zrobione w pytaniu.
Johannes Schaub - litb
18

odpowiedź litb jest trafna. Czas życia obiektu tymczasowego (znanego również jako wartość r) jest powiązany z wyrażeniem, a destruktor obiektu tymczasowego jest wywoływany na końcu pełnego wyrażenia, a gdy wywoływany jest destruktor na StringBuffer, destruktor na m_buffer również będzie wywoływana, ale nie destruktor na m_str, ponieważ jest referencją.

Zauważ, że C ++ 0x zmienia rzeczy tylko trochę, ponieważ dodaje odwołania do rvalue i semantykę przenoszenia. Zasadniczo, używając parametru referencyjnego rvalue (oznaczonego znakiem &&) mogę „przenieść” wartość r do funkcji (zamiast ją kopiować), a czas życia wartości r można powiązać z obiektem, do którego się przenosi, a nie z wyrażeniem. Jest naprawdę dobry post na blogu zespołu MSVC, który szczegółowo omawia ten temat i zachęcam do przeczytania go.

Pedagogicznym przykładem przenoszenia rwartości są tymczasowe ciągi znaków i pokażę przypisanie w konstruktorze. Jeśli mam klasę MyType, która zawiera zmienną składową typu string, można ją zainicjować za pomocą rvalue w konstruktorze w następujący sposób:

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

To fajne, ponieważ kiedy deklaruję wystąpienie tej klasy z tymczasowym obiektem:

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

dzieje się tak, że unikamy kopiowania i niszczenia tymczasowego obiektu, a słowo „hello” jest umieszczane bezpośrednio w zmiennej składowej instancji klasy będącej właścicielem. Jeśli obiekt ma większą wagę niż „string”, dodatkowe wywołanie copy i destructor może być znaczące.

Stóg
źródło
1
Myślę, że aby przenieść działanie, musisz porzucić stałą i użyć std :: move jak MyType (std :: string && name): m_name (std :: move (name)) {}
gast128
4

Po wywołaniu GetString zwraca.

David Segonds
źródło
3

StringBuffer znajduje się w zakresie GetString. Powinien zostać zniszczony na końcu zakresu GetString (tj. Po powrocie). Nie wierzę też, że C ++ gwarantuje, że zmienna będzie istnieć tak długo, jak długo będzie istniała referencja.

Należy skompilować:

Object* obj = new Object;
Object& ref = &(*obj);
delete obj;
BigSandwich
źródło
Myślę, że zawyżałem gwarancję - dotyczy tylko lokalnych prowizoriów. Ale ona istnieje.
Mark Ransom
Zmieniłem pytanie. Na podstawie dotychczas udzielonych odpowiedzi wydaje się jednak kwestią sporną.
Mark Ransom
Nadal nie sądzę, że twoja edycja jest poprawna: Object & obj = GetObj (); Object & GetObj () {return & Object (); } // źle - pozostawi wiszące odniesienie.
BigSandwich,
1
Najwyraźniej kiepsko mi się tłumaczę i mogę też nie rozumieć w 100%. Zajrzyj na informit.com/guides/content.aspx?g=cplusplus&seqNum=198 - wyjaśnia i odpowiada również na moje pierwotne pytanie.
Mark Ransom
1
Dzięki za link, to teraz ma sens.
BigSandwich
3

Napisałem prawie dokładnie tę samą klasę:

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

Przed standardem każdy kompilator robił to inaczej. Wydaje mi się, że stary podręcznik z adnotacjami dotyczący języka C ++ określał, że elementy tymczasowe powinny zostać wyczyszczone na końcu zakresu, więc niektóre kompilatory to zrobiły. Dopiero w 2003 roku odkryłem, że zachowanie nadal istnieje domyślnie w kompilatorze Sun's Forte C ++, więc StringBuffer nie działa. Ale byłbym zdziwiony, gdyby jakikolwiek aktualny kompilator nadal był tak zepsuty.

Daniel Earwicker
źródło
Straszne, jakie są podobne! Dzięki za ostrzeżenie - najpierw spróbuję to VC ++ 6, który nie jest znany ze swojej zgodności ze standardami. Będę uważnie obserwować.
Mark Ransom
Napisałbym tę klasę pierwotnie na VC ++ 6, więc nie powinno to stanowić problemu.
Daniel Earwicker,