提问人:phlipsy 提问时间:8/25/2023 更新时间:8/25/2023 访问量:157
运算符重载:修改临时对象或创建新对象
Operator overloading: Modify temporary object or create new one
问:
我在我们的项目中看到了以下代码,并问自己技术和心理含义是什么:
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)
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
你显然保存了一个临时对象!但从直觉上讲,我仍然更喜欢加号运算符,并希望编译器能够通过就地构造这些对象来优化临时对象,这样就不必破坏第二个临时对象。
你对这种“优化”有什么看法?还有其他赞成和反对的论点吗?有没有可能的方法可以保存加号运算符方法?我一直认为移动语义会处理这样的事情,它会阻止所有这些临时对象,将我们从过去的代理对象中拯救出来,让我们编写更直接和实用的代码等等......
答:
编译器不知道如何实现,因此它无法知道任何构造函数、析构函数或运算符是否有副作用,因此它无法证明优化它们满足了“仿佛”规则,因此它必须完全按照编写的代码保留代码。A
评论
您可以在命名空间范围内引入以下 4 个重载(或成员运算符的 add 和修饰符),并利用表达式导致重载 take 优先于重载 take 这一事实。至少在其中一个操作数是右值引用的情况下,这允许您返回此引用。请注意,如果尝试通过将操作结果绑定到引用来延长操作结果的生存期,这可能会导致令人讨厌的意外。&
&&
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;
// none of the operands is an rvalue -> create a new temporary
std::cout << (address(s1 + s2) == &s1) << '\n'; // false
std::cout << (address(s1 + s2) == &s2) << '\n'; // false
// same scenario as sink(factory() + x); here...
// the first operand is an rvalue
std::cout << (address(std::move(s1) + s2) == &s1) << '\n'; // true
// second operand is an rvalue
std::cout << (address(s1 + std::move(s2)) == &s2) << '\n'; // true
// both operands are rvalues
std::cout << (address(std::move(s1) + std::move(s2)) == &s1) << '\n'; // true
}
不过,通常您不会期望以下代码会导致未定义的行为,这就是为什么我不会像这样重载运算符的原因。(您可以通过实现 for 的移动语义并按值返回来避免此问题;但是,这将导致 的 移动构造 的新实例。A
A
int main() {
A const& value = A{} + A{};
std::cout << value.m_dummy << '\n'; // reading from a dangling reference here -> UB
}
评论
+=
+
+=
+=
{ auto temp = factory(); temp += x; sink(temp);
+=
*this
评论
+=
+
x
x += a
x
y = x +a