Гарантированное время жизни временного в C ++?

103

Предоставляет ли C ++ гарантию на время жизни временной переменной, которая создается при вызове функции, но не используется в качестве параметра? Вот пример класса:

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

И вот как бы вы это использовали:

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

Когда будет вызван деструктор временного объекта StringBuffer? Это:

  • Перед вызовом GetString?
  • После возвращения GetString?
  • Зависит от компилятора?

Я знаю, что C ++ гарантирует, что локальная временная переменная будет действительна до тех пор, пока на нее есть ссылка - применимо ли это к родительским объектам, когда есть ссылка на переменную-член?

Спасибо.

Марк Рэнсом
источник
почему бы не унаследовать и не перегрузить или не сделать глобальную функцию? Я был бы чище, и вам не пришлось бы создавать класс только для вызова члена.
Яцек Лавринович
1
Если вы собираетесь использовать это, вы должны позвонить m_str.reserve(maxlength)в char * Size(int maxlength)противном случае деструктор может бросить.
Mankarse

Ответы:

109

Деструктор для такого рода временных файлов вызывается в конце полного выражения. Это самое внешнее выражение, которое не является частью любого другого выражения. Это в вашем случае после возврата функции и оценки значения. Так что все будет хорошо работать.

Фактически, это то, что заставляет шаблоны выражений работать: они могут содержать ссылки на такого рода временные объекты в выражении вроде

e = a + b * c / d

Потому что каждое временное будет длиться до выражения

x = y

Оценивается полностью. Это довольно лаконично описано в 12.2 Temporary objectsСтандарте.

Йоханнес Шауб - litb
источник
3
Мне так и не удалось получить копию стандарта. Я должен сделать это приоритетом.
Марк Рэнсом,
2
@JohannesSchaub: Что такое «полное выражение» в этом случае:? printf("%s", strdup(std::string("$$$").c_str()) );Я имею в виду, что если strdup(std::string("$$$").c_str())оно принимается как полное выражение, то указатель, который strdupвидит, действителен . Если std::string("$$$").c_str()это полное выражение, значит указатель, который strdupвидит, недействителен ! Не могли бы вы объяснить немного больше на этом примере?
Grim Fandango
2
@GrimFandango AIUI все ваше printf- полное выражение. Таким образом, strdupэто ненужная утечка памяти - вы можете просто позволить ей c_str()напрямую распечатать .
Джош Стоун
1
«Такие временные» - что это за? Как я могу определить, является ли мой временный «таким временным»?
RM
@RM достаточно честно. Я имел в виду «те, которые вы создаете в аргументе функции», как это сделано в вопросе.
Йоханнес Шауб - лит,
18

litb ответ точен. Время жизни временного объекта (также известного как rvalue) привязано к выражению, и деструктор для временного объекта вызывается в конце полного выражения, и когда вызывается деструктор в StringBuffer, деструктор в m_buffer также будет вызывается, но не деструктором на m_str, поскольку это ссылка.

Обратите внимание, что C ++ 0x немного меняет ситуацию, потому что он добавляет ссылки на rvalue и семантику перемещения. По сути, используя ссылочный параметр rvalue (обозначенный &&), я могу «переместить» rvalue в функцию (вместо того, чтобы копировать его), и время жизни rvalue можно привязать к объекту, в который он перемещается, а не к выражению. Есть действительно хороший пост в блоге от команды MSVC, в котором это подробно рассматривается, и я призываю людей прочитать его.

Педагогическим примером перемещения rvalue являются временные строки, и я покажу присваивание в конструкторе. Если у меня есть класс MyType, содержащий строковую переменную-член, его можно инициализировать с помощью rvalue в конструкторе следующим образом:

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

Это хорошо, потому что когда я объявляю экземпляр этого класса временным объектом:

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

происходит то, что мы избегаем копирования и уничтожения временного объекта, и «привет» помещается непосредственно внутри переменной-члена экземпляра класса-владельца. Если объект тяжелее, чем «строка», то дополнительная копия и вызов деструктора могут иметь значение.

Рик
источник
1
Чтобы заставить перемещение работать, я думаю, вам нужно отбросить константу и использовать std :: move, например MyType (std :: string && name): m_name (std :: move (name)) {}
gast128
4

После вызова GetString возвращается.

Дэвид Сегондс
источник
3

StringBuffer находится в сфере GetString. Он должен быть уничтожен в конце области действия GetString (т.е. когда он вернется). Кроме того, я не верю, что C ++ гарантирует, что переменная будет существовать до тех пор, пока есть ссылка.

Следующее должно компилироваться:

Object* obj = new Object;
Object& ref = &(*obj);
delete obj;
BigSandwich
источник
Думаю, я завысил гарантию - она ​​только для местных временных. Но он существует.
Марк Рэнсом,
Я редактировал вопрос. Однако, судя по полученным до сих пор ответам, это, похоже, спорный вопрос.
Марк Рэнсом,
Я все еще не думаю, что ваша редакция верна: Object & obj = GetObj (); Объект & GetObj () {возврат & Объект (); } // плохо - оставит висящую ссылку.
BigSandwich
1
Я, очевидно, плохо себя объясняю, и я тоже могу не понять на 100%. Посмотрите informit.com/guides/content.aspx?g=cplusplus&seqNum=198 - он также объясняет и отвечает на мой исходный вопрос.
Марк Рэнсом,
1
Спасибо за ссылку, теперь это имеет смысл.
BigSandwich
3

Я написал почти такой же класс:

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

До появления стандарта каждый компилятор делал это по-своему. Я считаю, что в старом аннотированном справочном руководстве для C ++ указано, что временные библиотеки должны очищаться в конце области видимости, поэтому некоторые компиляторы сделали это. Еще в 2003 году я обнаружил, что такое поведение по умолчанию все еще существует в компиляторе Sun Forte C ++, поэтому StringBuffer не работал. Но я был бы удивлен, если бы какой-нибудь текущий компилятор все еще был сломан.

Дэниел Эрвикер
источник
Жутко, как они похожи! Спасибо за предупреждение - в первую очередь я попробую, это VC ++ 6, который не известен своими стандартами. Я буду внимательно смотреть.
Марк Рэнсом,
Я бы изначально написал класс на VC ++ 6, так что проблем быть не должно.
Дэниел Эрвикер,