C++中临时变量的寿命有保障吗?

109
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++保证本地临时变量只要存在引用就有效 - 当存在对成员变量的引用时,这是否适用于父对象?

谢谢。


为什么不继承和重载或者创建一个全局函数呢?这样会更简洁,而且您不必只是为了调用成员而创建一个类。 - Jacek Ławrynowicz
1
如果您要使用此功能,应在 char * Size(int maxlength) 中调用 m_str.reserve(maxlength),否则析构函数可能会抛出异常。 - Mankarse
6个回答

118

这种临时对象的析构函数会在完整表达式的末尾被调用。这是最外层的表达式,不是任何其他表达式的一部分。在您的情况下,在函数返回并计算值之后就会发生这种情况。所以,它会很好地工作。

实际上,这正是表达式模板的工作原理:它们可以保留对该类型临时对象的引用,例如:

e = a + b * c / d

因为每个临时变量只会持续到表达式

x = y

完全被求值。这在标准中的12.2 临时对象中已经被简洁地描述了。


3
我从来没有抽出时间去获取这个标准的副本。我应该把它作为当务之急。 - Mark Ransom
3
在这种情况下,“full-expression”是什么意思:printf("%s", strdup(std::string("$$$").c_str()) );?如果将strdup(std::string("$$$").c_str())视为完整表达式,则strdup看到的指针是有效的。如果将std::string("$$$").c_str()视为完整表达式,则strdup看到的指针是无效的!您能否根据此示例进行更详细的解释? - Grim Fandango
3
据我理解,你的整个printf表达式就是完整的表达式。因此,strdup是不必要的内存泄漏——你可以直接让它打印c_str() - Josh Stone
2
“那种临时变量” - 是什么类型的?我怎样才能知道我的临时变量是不是“那种类型”的临时变量? - R.M.
1
@R.M 好的,我是指“在函数参数中创建的那些变量”,就像问题中所做的那样。 - Johannes Schaub - litb

19

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

发生的情况是我们避免了复制和销毁临时对象,而是将“hello”直接放入拥有类实例的成员变量中。如果对象比“string”更重,则额外的复制和析构函数调用可能相当显著。


4
为了使这个移动操作生效,我认为你需要去掉const并使用std::move,像这样:MyType(std::string&& name) : m_name(std::move(name)) {}。 - gast128
这可能是博客文章:https://devblogs.microsoft.com/cppblog/rvalue-references-c0x-features-in-vc10-part-2/ - Hari

4

我写了一个几乎完全相同的类:

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++标准出现之前,每个编译器都有自己的方式进行操作。我相信旧版C++注释参考手册规定了临时对象应该在作用域结束时清理,所以一些编译器就按照这种方式进行操作。甚至到2003年,我发现Sun公司的Forte C++编译器仍默认采用这种行为,导致StringBuffer无法正常工作。但是,如果现代编译器还保留这种错误,我会感到非常惊讶。


它们的相似之处令人毛骨悚然!感谢警告 - 我将首先尝试VC++6,这并不以其符合标准而闻名。我会仔细观察。 - Mark Ransom
我原本会在VC++6上编写这个类,所以不应该有问题。 - Daniel Earwicker

4
调用GetString函数后。

3

StringBuffer在GetString的范围内,应该在GetString的范围结束时销毁(即返回时)。此外,我不相信C++会保证变量存在,只要有引用存在。

以下内容应该可以编译:

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

我想我夸大了保证 - 它仅适用于本地临时变量。但它确实存在。 - Mark Ransom
我已经编辑了这个问题。根据迄今为止提供的答案,似乎这已经成为了一个无用的点了。 - Mark Ransom
我仍然认为你的编辑不正确:Object& obj = GetObj(); Object& GetObj() { return &Object(); } //bad - will leave dangling reference. - BigSandwich
1
我显然没有很好地解释清楚自己,而且我自己也可能并没有完全理解。请看http://www.informit.com/guides/content.aspx?g=cplusplus&seqNum=198 - 它可以解释并回答我的原始问题。 - Mark Ransom
1
感谢提供链接,现在我明白了。 - BigSandwich
显示剩余2条评论

3

来自cppreference

所有临时对象都会在包含它们创建点的完整表达式被评估的最后一步中销毁,如果创建了多个临时对象,则按照创建顺序相反的顺序销毁它们。即使该评估以抛出异常结束,这也是正确的。


总是很好有一个比标准更易接近的参考,即使它不是决定性的。 - Mark Ransom

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接