我在我们的项目中看到了以下代码,并问自己有什么技术和心理影响:
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
你显然保存了一个临时对象!但直观上我仍然更喜欢加号运算符,并且希望编译器能够通过就地构造这些对象来优化临时对象,这样就不必破坏第二个临时对象。
您对这种“优化”有何看法?还有其他赞成和反对的论点吗?有没有可能的方法来保存加号运算符方法?我一直认为移动语义会处理这些事情,它将阻止所有这些临时对象,将我们从过去的代理对象中拯救出来,让我们编写更直接和功能性的代码等等......
编译器不知道
A
是如何实现的,因此它不知道任何构造函数、析构函数或运算符是否有副作用,因此它不能证明优化它们满足“好像”规则,它因此必须完全按照所写的方式保留您的代码。
您可以在命名空间范围内引入以下 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
}