1

I have come across the situation where I really do need to execute non-trivial code in a copy-constructor/assignment-operator. The correctness of the algorithm depends on it.

While I could disable return value optimisation with a compiler switch, it seems a waste because it's only the one type I need it disabled for, so why should the performance of the whole application suffer? (Not to mention that my company would not allow me to add the switch, anyway).

struct A {
    explicit A(double val) : m_val(val) {}

    A(const A& other) : m_val(other.m_val) {
        // Do something really important here
    }
    A& operator=(const A& other) {
        if (&other != this) {
            m_val = other.m_val;
            // Do something really important here 
        }
        return *this;
    }
    double m_val;
};

A operator+(const A& a1, const A& a2) {
    A retVal(a1.m_val + a2.m_val);
    // Do something else important
    return retVal;
}
// Implement other operators like *,+,-,/ etc.

This class would be used as such:

A a1(3), a2(4), a3(5);
A a4 = (a1 + a2) * a3 / a1;

Return value optimisation means that a4 will not be created with the copy constructor, and the "really important thing to do" does not happen!

I know I could hack in a solution where operator+ returns a different type (B, say) and have an A constructor that takes a B as input. But then the number of operators needed to be implemented explodes:

B operator+(const A& a1, const A& a2);
B operator+(const B& a1, const A& a2);
B operator+(const A& a1, const B& a2);
B operator+(const B& a1, const B& a2);

There must be a better solution. How can I hack it so that RVO does not happen for my type? I can only change the A class code and the operators. I can't change the calling site code; i.e. I can't do this:

A a1(3), a2(4), a3(5);
A a4;
a4 = (a1 + a2) * a3 / a1;

One thing I've considered trying is to try and experiment with C++11 move constructors, but I'm not sure this would work, and I don't like it not being valid in C++03.

Any ideas?

EDIT: Please just accept that this is the only way I can do what I need to do. I cannot just 'change the design'. The calling code is fixed, and I must implement my strategy inside the mathematical operators and copy constructor & assignment operator. The idea is that the intermediate values calculated inside the "a4 = (a1+a2)*a3/a1" equation cannot be referenced anywhere else in the program - but a4 can. I know this is vague but you'll just have to live with it.

4

3 回答 3

2

在这里回答我自己的问题:我将硬着头皮使用中间类型:

struct B;

struct A
{
    A(int i) : m_i(i) {}
    A(const B& a);
    A(const A& a) : m_i(a.m_i)
    {
        std::cout << "A(const A&)" << std::endl;
    }
    int m_i;
};
struct B
{
    B(int i) : m_i(i) {}
    int m_i;
};

A::A(const B& a) : m_i(a.m_i)
{
    std::cout << "A(const B&)" << std::endl;
}

B operator+(const A& a0, const A& a1)
{
    B b(a0.m_i + a1.m_i);
    std::cout << "A+A" << std::endl;
    return b;
}
B operator+(const B& a0, const A& a1)
{
    B b(a0.m_i + a1.m_i);
    std::cout << "B+A" << std::endl;
    return b;
}
B operator+(const A& a0, const B& a1)
{
    B b(a0.m_i + a1.m_i);
    std::cout << "A+B" << std::endl;
    return b;
}
B operator+(const B& a0, const B& a1)
{
    B b(a0.m_i + a1.m_i);
    std::cout << "B+B" << std::endl;
    return b;
}

int main()
{
    A a(1);
    A b(2);
    A c(3);
    A d = (a+b) + (a + b + c);
}

GCC 4.2.1 上的输出:

A+A
B+A
A+A
B+B
A(const B&)

我可以在 A(const B&) 构造函数中做“非常重要的事情”。

于 2013-04-26T13:44:19.437 回答
1

正如 Angew 指出的,您可以使用中间类型。这是一个使用 move ctor 进行一些优化的示例。

#include <utility>
#include <iostream>

struct B;

struct A {
    explicit A(double val) : m_val(val)
    {
        std::cout << "A(double)" << std::endl;
    }
    A(A&& p) : m_val(p.m_val)
    { /* no output */ }

    A(const A& other) : m_val(other.m_val) {
        // Do something really important here
        std::cout << "A(A const&)" << std::endl;
    }
    A& operator=(const A& other) {
        if (&other != this) {
            m_val = other.m_val;
            // Do something really important here
            std::cout << "A::operator=(A const&)" << std::endl;
        }
        return *this;
    }
    double m_val;

    A(B&&);
};

struct B
{
    operator A const&() const
    {
        std::cout << "B::operator A const&()" << std::endl;
        return a;
    }

private:
    friend struct A;
    A a;

    // better: befriend a factory function
    friend B operator+(const A&, const A&);
    friend B operator*(const A&, const A&);
    friend B operator/(const A&, const A&);
    B(A&& p) : a( std::move(p) )
    { /* no output */ }
};

A::A(B&& p) : A( std::move(p.a) )
{
    std::cout << "A(B&&)" << std::endl;
}

B operator+(const A& a1, const A& a2) {
    std::cout << "A const& + A const&" << std::endl;
    A retVal(a1.m_val + a2.m_val);
    // Do something else important
    return std::move(retVal);
}

B operator*(const A& a1, const A& a2) {
    std::cout << "A const& * A const&" << std::endl;
    A retVal(a1.m_val * a2.m_val);
    // Do something else important
    return std::move(retVal);
}

B operator/(const A& a1, const A& a2) {
    std::cout << "A const& / A const&" << std::endl;
    A retVal(a1.m_val / a2.m_val);
    // Do something else important
    return std::move(retVal);
}

int main()
{
    A a1(3), a2(4), a3(5);
    A a4 = (a1 + a2) * a3 / a1;
}

IIRC,临时返回的,说a1 + a2持续整个复制初始化(更准确地说:整个完整表达式,包括 AFAIK 的构造a4)。这就是为什么我们可以A const&从内部返回一个的原因B,即使这些B对象只是作为临时对象创建的。(如果我错了,请参阅我之前的编辑以获取其他解决方案.. :D)

这个例子的本质是中间类型、移动 ctor 和引用返回的组合。

g++4.6.3 和 clang++3.2 的输出:

A(double)             <---- A a1(3);
A(double)             <---- A a2(4);
A(double)             <---- A a3(5);
A const& + A const&   <---- a1 + a2;
A(double)               <-- A retVal(a1.m_val + a2.m_val);
B::operator A const&()<---- __temp__ conversion B --> const A&
A const& * A const&   <---- __temp__ * a3;
A(double)               <-- A retVal(a1.m_val * a2.m_val);
B::operator A const&()<---- __temp__ conversion B --> const A&
A const& / A const&   <---- __temp__ / a1;
A(double)               <-- A retVal(a1.m_val / a2.m_val);
A(B&&)                <---- A a4 = __temp__;

既然复制和移动操作(未显示)被拆分,我认为您可以更准确地实现您的“重要事项”它所属的位置:

  • A(double)A--从数值创建一个新对象
  • A(A const&)-- 对象的实际副本A;不会在这里发生
  • A(B&&)--A根据运算符结果构造对象
  • B(A&&)-- 为操作符的返回值调用
  • B::operator A const&() const-- 调用以使用运算符的返回值
于 2013-04-26T12:42:04.123 回答
0

标准允许 RVO,在以下情况下([class.copy]§31,仅列出适用部分):

  • 在具有类返回类型的函数的 return 语句中,当表达式是具有与函数返回类型相同的 cv 非限定类型的非易失性自动对象(函数或 catch 子句参数除外)的名称时,通过将自动对象直接构造到函数的返回值中,可以省略复制/移动操作

  • 当尚未绑定到引用 (12.2) 的临时类对象将被复制/移动到具有相同 cv-unqualified 类型的类对象时,可以通过将临时对象直接构造到目标中来省略复制/移动操作省略的复制/移动

在您的代码中:

A operator+(const A& a1, const A& a2) {
    A retVal(a1.m_val + a2.m_val);
    // Do something else important
    return retVal;
}


A a4 = (a1 + a2) * a3 / a1;

涉及两个可删除的副本:复制revVal到存储返回值的临时对象中operator+,以及将此临时对象复制到a4.

我看不到防止省略第二个副本(从返回值到a4的那个)的方法,但是标准的“非易失性”部分让我相信这应该可以防止省略第一个副本:

A operator+(const A& a1, const A& a2) {
    A retVal(a1.m_val + a2.m_val);
    // Do something else important
    volatile A volRetVal(retVal);
    return volRetVal;
}

当然,这意味着您必须定义一个额外的复制构造函数来A获取const volatile A&.

于 2013-04-26T11:45:54.087 回答