有关移动分配运算符的问题
Questions about the move assignment operator
想象一下以下管理资源的类(我的问题只是关于移动分配运算符):
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的存储,然而,我看不出这会有什么用处,因为rvalue无论如何都会被破坏。也许唯一的时间是使用a1 = std::move(a2);
之类的东西,但即使在这种情况下,我也看不出有任何理由使用#1。
在这种情况下,您应该真正进行测量。
我正在查看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() << "nsn";
t0 = Clock::now();
a2 = std::move(a1);
t1 = Clock::now();
std::cout << "move takes " << NS(t1-t0).count() << "nsn";
}
这是我得到的输出:
$ 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
在我看来相当不错。
注意:如果类有需要"快速"释放的资源,例如互斥锁所有权或文件打开状态所有权,那么从正确性的角度来看,design-2移动赋值操作符可能会更好。但是,当资源只是内存时,尽可能长时间地延迟释放它通常是有利的(就像在OP的用例中一样)。
注意事项2:如果你有其他你知道很重要的用例,测量它们。你可能会得出与我不同的结论。
注:我认为性能高于"DRY"。这里的所有代码都将封装在一个类(struct A
)中。尽可能地做好struct A
。如果你做了足够高质量的工作,那么你的struct A
客户(可能是你自己)就不会被"RIA"(再次创新)所诱惑。我更喜欢在一个类中重复一点代码,而不是一遍又一遍地重复整个类的实现。
使用#1比使用#2更有效,因为如果使用#2,则会违反DRY并复制析构函数逻辑。其次,考虑以下分配运算符:
A& operator=(A other) {
swap(*this, other);
return *this;
}
这是复制和移动赋值运算符,用于无重复代码-一种出色的形式。
如果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
,这应该是优选的。
这也隐含地回答了最初的问题:在抛出swap()
和移动赋值的情况下,这两种实现都是错误的,因为它们不是基本的异常安全的。假设swap()
中唯一的异常源是不匹配的分配器,那么上面的实现是强异常安全的。
- 一个关于在C++中重载布尔运算符的问题
- 使用运算符 [] 引用 std::vector 上最后一个元素时出现问题<>
- 运算符继承和 cpp 核心准则 c.128 的问题
- 在C++中使用重载提取运算符时出现问题
- 移动赋值运算符;尝试引用已删除的函数.我该如何解决这个问题?
- 关于条件块的问题与&&运算符有关
- 重载运算符与添加问题
- 加、乘、除、减复数的问题 C++ - 运算符重载
- 我需要这方面的帮助C++在使用逻辑运算符时是/否问题
- 0xC0000005:访问冲突读取位置 0x00000000. 重载 == 运算符的问题
- 如何解决不明确的运算符过载问题?
- 屏幕插入运算符<<的运算符过载问题
- 如何通过使用 2 位或更多数字的 XOR 运算符来执行此操作C++问题
- 在 C++ 的自定义运算符中删除与删除[](不同于常见的删除与删除[]问题)
- 用户定义的文本运算符(在原始模式下)存在问题
- 关于重载 -> 运算符中 const 关键字的特定位置的问题
- 字符 * 未从重载运算符或内存管理问题正确返回
- constexpr 运算符重载使用参数的问题
- c++中重载输入运算符的问题
- 有趣的问题运算符重载