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.


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.


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.


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


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;

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.


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,
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.