运算符重载:修改临时对象或创建新对象

问题描述 投票:0回答:2

我在我们的项目中看到了以下代码,并问自己有什么技术和心理影响:

class A {
public:
    A(const A&);
    A(A &&);
    ~A();

    A &operator += (const A &);
    A operator + (const A &);
private:
    class B;
    B *b;
};

A factory();
void sink(const A &);

void foo(const A &x) {
    sink(factory() += x); // <--
}

在突出显示的行中,我期望

sink(factory() + x)
。我在审查过程中指出,并得到答复说,由于没有创建第二个临时对象,因此当前的实现效率更高。作为一个有自尊心的书呆子,我立即在here检查了此声明,并得到了以下
x86_64
汇编输出。

foo(A const&):
   push    rbx
    mov     rbx, rdi
    sub     rsp, 16
    lea     rdi, [rsp+8]
    call    factory()
    mov     rsi, rbx
    lea     rdi, [rsp+8]
    call    A::operator+=(A const&)
    mov     rdi, rax
    call    sink(A const&)
    lea     rdi, [rsp+8]
    call    A::~A()
    add     rsp, 16
    pop     rbx
    ret

对战

bar(A const&):
    push    rbx
    mov     rbx, rdi
    sub     rsp, 16
    mov     rdi, rsp
    call    factory()
    mov     rdx, rbx
    mov     rsi, rsp
    lea     rdi, [rsp+8]
    call    A::operator+(A const&)
    lea     rdi, [rsp+8]
    call    sink(A const&)
    lea     rdi, [rsp+8]
    call    A::~A() [complete object destructor]
    mov     rdi, rsp
    call    A::~A() [complete object destructor]
    add     rsp, 16
    pop     rbx
    ret

你显然保存了一个临时对象!但直观上我仍然更喜欢加号运算符,并且希望编译器能够通过就地构造这些对象来优化临时对象,这样就不必破坏第二个临时对象。

您对这种“优化”有何看法?还有其他赞成和反对的论点吗?有没有可能的方法来保存加号运算符方法?我一直认为移动语义会处理这些事情,它将阻止所有这些临时对象,将我们从过去的代理对象中拯救出来,让我们编写更直接和功能性的代码等等......

c++ operator-overloading move-semantics return-value-optimization
2个回答
1
投票

编译器不知道

A
是如何实现的,因此它不知道任何构造函数、析构函数或运算符是否有副作用,因此它不能证明优化它们满足“好像”规则,它因此必须完全按照所写的方式保留您的代码。


0
投票

您可以在命名空间范围内引入以下 4 个重载(或为成员运算符添加

&
&&
修饰符),并利用表达式
factory()
导致重载优先考虑
A&&
的事实到一个采取
A const&
。至少在其中一个操作数是右值引用的情况下,这允许您返回该引用。请注意,如果您尝试通过将
+
操作的结果绑定到引用来延长它的生命周期,这可能会导致令人讨厌的意外。

struct A
{
    int m_dummy{ 1 };

    friend constexpr A operator+(A const& s1, A const& s2)
    {
        return {};
    }

    friend constexpr A&& operator+(A&& s1, A const& s2)
    {
        return std::move(s1);
    }

    friend constexpr A&& operator+(A&& s1, A&& s2)
    {
        return std::move(s1);
    }

    friend constexpr A&& operator+(A const& s1, A&& s2)
    {
        return std::move(s2);
    }
};

A const* address(A const& a)
{
    return &a;
}

int main() {
    std::cout << std::boolalpha;

    A s1;
    A s2;
    std::cout << (address(s1 + s2) == &s1) << '\n';
    std::cout << (address(s1 + s2) == &s2) << '\n';

    // same scenario as sink(factory() + x); here...
    std::cout << (address(std::move(s1) + s2) == &s1) << '\n';
    std::cout << (address(s1 + std::move(s2)) == &s2) << '\n';
    std::cout << (address(std::move(s1) + std::move(s2)) == &s1) << '\n';
}

通常您不会期望以下代码会导致未定义的行为,这就是为什么我不会像这样重载运算符。 (您可以通过实现

A
的移动语义并按值返回来避免此问题;但这会导致
A
的新实例的移动构造。)

int main() {
    A const& value = A{} + A{};
    std::cout << value.m_dummy << '\n'; // reading from a dangling reference here -> UB
}
© www.soinside.com 2019 - 2024. All rights reserved.