关于返回值优化的C ++问题

时间:2011-07-29 22:02:42

标签: c++

这个网站上有很多关于返回值优化的问题(我认为这是一个相当令人困惑的话题),但我似乎无法找到一个回答我特定问题的问题。如果我的代码如下所示:

returnType function() { stuff....}

void someOtherFunction()
{
    returnType var = function();
    more stuff...
}

我被告知编译器可能决定不在returnType中使用someOtherFunction()的两个实例。从逻辑上讲,我希望function()生成returnType类型的对象,someOtherFunction()将通过复制构造函数(重载或不重载)将该值接收到临时值。然后我希望通过赋值(可以重载并且在理论上可以具有任何类型的功能!)将临时值复制到var中,之前已经通过默认构造函数初始化了。{1}}。

我在这看到一个潜在的问题。如果returnType中没有someOtherFunction()的临时副本,会发生什么?不能通过复制构造函数使用var的返回值直接填充function()吗?如果是这样,不会调用赋值运算符吗?如果是这样,并且如果赋值运算符过载,那么这不能改变程序的功能吗?如果是这样,这是否意味着程序员有责任确保=始终与复制构造函数做同样的事情?我讨厌运行这一长串问题,但如果是这样,为什么C ++允许你定义复制构造函数来做除赋值之外的事情?

2 个答案:

答案 0 :(得分:8)

  

然后我希望通过赋值复制临时值   (这可能是超载的,理论上可以有任何一种   功能!)到var,它之前已被初始化   通过默认构造函数。

嗯,首先,这是非常错误的。

int x = 0;
int x(0);

这两行是相同的 - 构造函数调用。存在一些差异 - 我认为第一个不能调用显式构造函数 - 但它们是相同的函数调用。没有默认构造,也没有赋值操作符调用。它们都是直接构造。

基本上,标准说“如果你在复制构造函数中复制一个对象以外的其他东西,这是你自己的愚蠢错误,我笑了,因为你的程序在优化器消除调用时没有表现出预期的行为”。当然,这些是我自己的释义词,但标准对于优化器被允许消除副本非常明确。在C ++ 0x中,这也适用于移动。

上面的代码片段确实是

returnType function() { stuff....}

void someOtherFunction()
{
    returnType var(function());
    more stuff...
}

这不是优化版本,它就是它的真实含义。赋值运算符从不调用。使用NRVO,它看起来像

void function(void* mem) { // construct return value into mem
    new (mem) returnType;
    // Do shiz with returnType;
}
void someOtherFunction() {
    // This doesn't respect some other things like alignment
    // but it's the basic idea
    char someMemory[sizeof(returnType)];
    function(someMemory);
    // more stuff here
}

当然,即使在异常的情况下,编译器也必须处理破坏对象,并确保所有别名都是正确的类型,对齐,以及我在其中没有处理的其他一些事情样本,但希望你得到一般的要点。

答案 1 :(得分:2)

回答你的最后几个问题:

  

...如果赋值运算符过载,则无法改变   程序的功能?如果是这样,那是否意味着它是   程序员有责任确保=始终如一   作为复制构造函数的东西?而且我讨厌经营这条长链   问题,但如果是这样,为什么C ++允许您定义副本   构造函数除了赋值之外还要做什么?

是的,赋值运算符实际上可以更改程序的功能。但是,赋值运算符完全可以执行与复制构造函数不同的操作,但仍然具有相同的 observable 行为。例如,对于String类,我可以根据复制构造函数定义赋值运算符:

class String
{
public:
    String(const wchar_t* str) : buffer(str) {}
    String(const String& rhs) : buffer(rhs.buffer) {}

    String& operator=(String rhs) // copy-and-swap idiom
    {
         Swap(rhs);
         return *this;
    }

    // ...

    void Swap(String& rhs)
    {
        buffer.Swap(rhs.buffer);
    }

private:
    // StringBuffer is an RAII wrapper for a string character array
    // allocated on the free store
    StringBuffer buffer;
};

在上面的代码中,copy-constructor和赋值运算符基本上做同样的事情。但是如果接收String有足够的内存来保存源字符串,那么将其丢弃而不是简单地覆盖现有内容是相当浪费的。

String& operator=(const String& rhs)
{
    // We don't have enough room to hold the source string.
    // Copy over to a new, bigger buffer.
    if(rhs.buffer.Length() > buffer.Length())
    {
        String temp(rhs);
        Swap(temp);
        // temp holds our old buffer now, and will be destroyed
        // when we exit this scope thanks to RAII.
    }
    else
    {
        // Instead of throwing away our existing buffer and having
        // to allocate a new one, let's just overwrite what we
        // have since our buffer is big enough.
    }
}

显然,为<{1}}分配涉及与复制构建 a String完全不同的代码,但仍然具有相同的可观察的行为(现在我们有两个合法的字符串副本)。不同之处在于,这个更复杂的String赋值运算符可以避免不必要的内存分配。

如果C ++语言强迫你让复制构造函数和赋值运算符做同样的事情,我将无法进行这种优化。是的,我负责确保复制构造函数和赋值运算符按照您的想法执行,但无论如何,对所有其他运算符/特殊成员函数都是如此。