【问题标题】:How can I disable c++ return value optimization for one type only?如何仅针对一种类型禁用 c++ 返回值优化?
【发布时间】:2013-04-26 10:32:21
【问题描述】:

我遇到了一种情况,我确实需要在复制构造函数/赋值运算符中执行重要的代码。算法的正确性取决于它。

虽然我可以使用编译器开关禁用返回值优化,但这似乎是一种浪费,因为它只是我需要禁用它的一种类型,那么为什么整个应用程序的性能会受到影响呢? (更不用说我的公司不允许我添加开关)。

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.

这个类会被这样使用:

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

返回值优化意味着不会用复制构造函数创建a4,“真正重要的事情”不会发生!

我知道我可以破解一个解决方案,其中 operator+ 返回一个不同的类型(比如 B)并有一个将 B 作为输入的 A 构造函数。但随后需要实现的操作符数量激增:

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

必须有更好的解决方案。我怎样才能破解它,以便我的类型不会发生 RVO?我只能更改 A 类代码和运算符。我无法更改呼叫站点代码;即我不能这样做:

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

我考虑过尝试的一件事是尝试使用 C++11 移动构造函数,但我不确定这是否可行,而且我不喜欢它在 C++03 中无效。

有什么想法吗?

编辑:请接受这是我可以做我需要做的事情的唯一方法。我不能只是“改变设计”。调用代码是固定的,我必须在数学运算符和复制构造函数和赋值运算符中实现我的策略。这个想法是,在“a4 = (a1+a2)*a3/a1”等式中计算的中间值不能在程序的其他任何地方引用 - 但 a4 可以。我知道这很模糊,但你只能忍受它。

【问题讨论】:

  • 你能解释一下你为什么需要它吗?我认为我们会更好地说服您不要这样做。
  • 最好的办法似乎是改变算法,使其不依赖于对象被复制的次数。
  • Move 构造函数不会参与其中。如果省略了副本,则没有移动的余地。我会说你最好重新设计代码。
  • 这看起来像an XY problem,在这里您询问的是您尝试的解决方案,而不是真正的问题。为什么记录实际上从未执行过的复制操作“非常重要”?
  • @BoPersson +1 用于制定关键问题。

标签: c++ return-value-optimization copy-elision


【解决方案1】:

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

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&) 构造函数中做“非常重要的事情”。

【讨论】:

  • 也许你需要Expression Template 模式。其目的是消除大型临时对象的创建。例如,如果uv 是大小相同的长向量,而a 是双精度向量,则可以创建一个值为a*(u-v) 的向量,但无需为u-v 创建一个临时向量。
【解决方案2】:

正如 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)。 这就是我们可以从B 中返回A const&amp; 的原因,即使B 对象只是作为临时对象创建的。 (如果我错了,请参阅我之前对其他解决方案的编辑。:D)

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

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&amp;) -- A 对象的实际副本;这里不会发生
  • A(B&amp;&amp;) -- 根据运算符结果构造 A 对象
  • B(A&amp;&amp;) -- 为操作符的返回值调用
  • B::operator A const&amp;() const -- 调用以使用运算符的返回值

【讨论】:

  • 谢谢,但我现在宁愿将解决方案保留在 C++03 中。
  • @user2020792 那么下次请用“C++03”标记您的问题。 C++03 是一个已被 C++11 取代的弃用标准,尽管我承认大多数编译器还不完全支持后者。
  • 我确实在我的问题中明确说明了 c++03,只是没有在标题中。
  • @user2020792 我指的是标签,而不是标题。不幸的是,您对 move ctors 和 C++03 的评论对我来说不是很清楚,您是否想查看它是否仅适用于 move ctors 或 C++03。好吧,现在您有两个答案:D,但如果很明显您只想要与 C++03 兼容的答案,那就太好了。
【解决方案3】:

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

  • 在具有类返回类型的函数的 return 语句中,当表达式是非易失性自动对象的名称(其他 比函数或 catch-clause 参数)具有相同的 cv-unqualified type 作为函数返回类型,可以进行复制/移动操作 通过将自动对象直接构造到 函数的返回值

  • 当尚未绑定到引用 (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&amp;

【讨论】:

  • 我需要的是防止第二次省略。如果返回类型更改为 volatile 怎么办? 'volatile A 运算符+(const A&, const A&)'
  • @user2020792 这无济于事。第二点(关于从临时对象复制的一点)不会对volatile 造成例外。您唯一的希望是返回值有一个中间类型。
猜你喜欢
  • 1970-01-01
  • 2012-02-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-02-26
  • 2021-12-15
相关资源
最近更新 更多