想象一下管理资源的以下类(我的问题只是关于移动赋值运算符):
struct A
{
std::size_t s;
int* p;
A(std::size_t s) : s(s), p(new int[s]){}
~A(){delete [] p;}
A(A const& other) : s(other.s), p(new int[other.s])
{std::copy(other.p, other.p + s, this->p);}
A(A&& other) : s(other.s), p(other.p)
{other.s = 0; other.p = nullptr;}
A& operator=(A const& other)
{A temp = other; std::swap(*this, temp); return *this;}
// Move assignment operator #1
A& operator=(A&& other)
{
std::swap(this->s, other.s);
std::swap(this->p, other.p);
return *this;
}
// Move assignment operator #2
A& operator=(A&& other)
{
delete [] p;
s = other.s;
p = other.p;
other.s = 0;
other.p = nullptr;
return *this;
}
};
问题:
上述两个移动分配操作符#1和#2有哪些优点和缺点?我相信我能看到的唯一区别是std::swap
保留了lhs的存储空间,但是,我看不出它会如何有用,因为rvalues无论如何都会被破坏。也许唯一的时间是像a1 = std::move(a2);
这样的东西,但即使在这种情况下,我也没有看到任何理由使用#1。
答案 0 :(得分:8)
这是你应该真正衡量的情况。
我正在查看OP的复制赋值运算符并发现效率低下:
A& operator=(A const& other)
{A temp = other; std::swap(*this, temp); return *this;}
如果*this
和other
具有相同的s
在我看来,如果s == other.s
更智能的复制分配可能会避免访问堆。所有它必须做的是副本:
A& operator=(A const& other)
{
if (this != &other)
{
if (s != other.s)
{
delete [] p;
p = nullptr;
s = 0;
p = new int[other.s];
s = other.s;
}
std::copy(other.p, other.p + s, this->p);
}
return *this;
}
如果您不需要强大的异常安全性,只有复制分配的基本异常安全性(如std::string
,std::vector
等),那么就有潜力与上述相关的性能提升。多少?测量
我用这三种方式编写了这门课程:
设计1:
使用上面的复制赋值运算符和OP的移动赋值运算符#1。
设计2:
使用上面的复制赋值运算符和OP的移动赋值运算符#2。
设计3:
DeadMG的复制和移动分配的复制赋值运算符。
以下是我用来测试的代码:
#include <cstddef>
#include <algorithm>
#include <chrono>
#include <iostream>
struct A
{
std::size_t s;
int* p;
A(std::size_t s) : s(s), p(new int[s]){}
~A(){delete [] p;}
A(A const& other) : s(other.s), p(new int[other.s])
{std::copy(other.p, other.p + s, this->p);}
A(A&& other) : s(other.s), p(other.p)
{other.s = 0; other.p = nullptr;}
void swap(A& other)
{std::swap(s, other.s); std::swap(p, other.p);}
#if DESIGN != 3
A& operator=(A const& other)
{
if (this != &other)
{
if (s != other.s)
{
delete [] p;
p = nullptr;
s = 0;
p = new int[other.s];
s = other.s;
}
std::copy(other.p, other.p + s, this->p);
}
return *this;
}
#endif
#if DESIGN == 1
// Move assignment operator #1
A& operator=(A&& other)
{
swap(other);
return *this;
}
#elif DESIGN == 2
// Move assignment operator #2
A& operator=(A&& other)
{
delete [] p;
s = other.s;
p = other.p;
other.s = 0;
other.p = nullptr;
return *this;
}
#elif DESIGN == 3
A& operator=(A other)
{
swap(other);
return *this;
}
#endif
};
int main()
{
typedef std::chrono::high_resolution_clock Clock;
typedef std::chrono::duration<float, std::nano> NS;
A a1(10);
A a2(10);
auto t0 = Clock::now();
a2 = a1;
auto t1 = Clock::now();
std::cout << "copy takes " << NS(t1-t0).count() << "ns\n";
t0 = Clock::now();
a2 = std::move(a1);
t1 = Clock::now();
std::cout << "move takes " << NS(t1-t0).count() << "ns\n";
}
这是我得到的输出:
$ clang++ -std=c++11 -stdlib=libc++ -O3 -DDESIGN=1 test.cpp
$ a.out
copy takes 55ns
move takes 44ns
$ a.out
copy takes 56ns
move takes 24ns
$ a.out
copy takes 53ns
move takes 25ns
$ clang++ -std=c++11 -stdlib=libc++ -O3 -DDESIGN=2 test.cpp
$ a.out
copy takes 74ns
move takes 538ns
$ a.out
copy takes 59ns
move takes 491ns
$ a.out
copy takes 61ns
move takes 510ns
$ clang++ -std=c++11 -stdlib=libc++ -O3 -DDESIGN=3 test.cpp
$ a.out
copy takes 666ns
move takes 304ns
$ a.out
copy takes 603ns
move takes 446ns
$ a.out
copy takes 619ns
move takes 317ns
DESIGN 1
对我来说非常好。
警告:如果类具有需要“快速”解除分配的资源,例如互斥锁所有权或文件开放状态所有权,则从正确的角度来看,设计2移动赋值运算符可能更好。但是当资源只是内存时,通常有利的是尽可能延迟解除分配(如OP的用例)。
警告2:如果您知道其他用例很重要,请测量它们。你可能会得出与我在这里不同的结论。
注意:我重视“干”的表现。这里的所有代码都将封装在一个类(struct A
)中。让struct A
尽可能好。如果你的工作质量足够高,那么struct A
(可能是你自己)的客户就不会想要“RIA”(再次重塑它)。我更喜欢在一个类中重复一些代码,而不是一遍又一遍地重复整个类的实现。
答案 1 :(得分:7)
使用#1比使用#2更有效,因为如果你使用#2,你就违反了DRY并重复了你的析构函数逻辑。其次,考虑以下赋值运算符:
A& operator=(A other) {
swap(*this, other);
return *this;
}
这是复制和移动赋值运算符,没有重复的代码 - 一种很好的形式。
答案 2 :(得分:3)
如果swap()
涉及的对象无法抛出,DeadMG发布的赋值运算符正在做正确的事情。不幸的是,这不能保证!特别是,如果你有有状态的分配器,这将无法工作。如果分配器可能不同,那么您似乎需要单独的复制和移动分配:复制构造函数将无条件地创建传递给分配器的副本:
T& T::operator=(T const& other) {
T(other, this->get_allocator()).swap(*this);
return * this;
}
移动分配将测试分配器是否相同,如果是,只需swap()
两个对象,否则只需调用副本分配:
T& operator= (T&& other) {
if (this->get_allocator() == other.get_allocator()) {
this->swap(other);
}
else {
*this = other;
}
return *this;
}
采用值的版本是一个更简单的替代方案,如果noexcept(v.swap(*this))
为true
,则应首选。
这隐含地也回答了原始的qurstion:在抛出swap()
和移动赋值的情况下,两个实现都是错误的,因为它们不是基本的异常安全。假设swap()
中唯一的异常来源是不匹配的分配器,上面的实现是强安全的异常。