我有几个基于PIMPL习惯用法的类(其中unique_ptr
指的是实际的实现结构)。
我尚未添加friend
swap
函数(如here所述),因为据我所知,标准std::swap
使用移动语义,可以很好地交换unique_ptr
。到目前为止,一切都很好。
但是,我阅读了(斯科特·迈耶斯(Scott Meyers)在第25项中说的有些过时的Effective C++
:
但是,默认的交换实现可能不会让您感到兴奋。它涉及到复制三个对象:a到temp,b到a和temp到b。 [...]对于某些类型,默认交换使您进入快速通道,进入慢速通道。这些类型中最重要的是那些主要由指向包含实际数据的其他类型的指针组成的类型。这种设计的一个常见体现是“ pimpl”惯用语。
[之后,他还建议也专门研究std::swap
。
我的问题是,这在C ++ 11中是否仍然有效。看来C ++ 11 swap
对于pimpl'd类来说工作得很好。我知道添加friend
swap
允许STL使用ADL等等,但是我更喜欢使类尽可能精简。
我的问题是,这在C ++ 11中是否仍然适用。
仅在较小程度上。
自从C ++ 11中引入移动语义以来,通用交换不再复制,而是移动。
移动通常与最佳交换实现非常接近,以至于通常无需费心编写一个自定义实现。尽管它可能接近最佳状态,但是在许多情况下,自定义实现可能会稍微更理想一些。可以通过测量性能来确定编写自定义代码是否有益。
这里的问题可能是,std::unique_ptr
实现的PIMPL,您基本上需要在头文件之外定义move构造函数/赋值运算符和析构函数(请参阅Meyers的Effective Modern C ++项目22)。然后,std::swap
不会“看到”这些定义,并且编译器无法优化掉不必要的操作,例如空指针的设置等。由于没有其他选择,它将仅生成4条call
指令。
考虑该课程的simple demo:
class X { public: X(X&&); X& operator=(X&&); ~X(); void swap(X& other) { std::swap(pimpl_, other.pimpl_); } private: class Impl; std::unique_ptr<Impl> pimpl_; };
由GCC使用
-O3
生成的程序集,该程序集使用X
交换两个std::swap
对象,如下所示:
f1(X&, X&): push r12 mov r12, rdi push rbp mov rbp, rsi mov rsi, rdi sub rsp, 24 lea rdi, [rsp+8] call X::X(X&&) mov rsi, rbp mov rdi, r12 call X::operator=(X&&) lea rsi, [rsp+8] mov rdi, rbp call X::operator=(X&&) lea rdi, [rsp+8] call X::~X() [complete object destructor] add rsp, 24 pop rbp pop r12 ret mov rbp, rax jmp .L2 f1(X&, X&) [clone .cold]: .L2: lea rdi, [rsp+8] call X::~X() [complete object destructor] mov rdi, rbp call _Unwind_Resume
虽然通过
X::swap
为同一操作生成的程序集为:
f2(X&, X&): mov rax, QWORD PTR [rdi] mov rdx, QWORD PTR [rsi] mov QWORD PTR [rdi], rdx mov QWORD PTR [rsi], rax ret
后者显然是最佳的,因为它只涉及交换两个常规指针所必需的指令(在我们的示例中隐藏在
std::unique_ptr
后面。