应该使用 unique_ptr 来更轻松地实现 "move" 语义吗?
Should use unique_ptr to more easily implement "move" semantics?
编辑: 使 Foo
和 Bar
变得不那么琐碎,直接替换为 shared_ptr<>
更加困难。
是否应该 unique_ptr<>
用作实现移动语义的更简单方法?
一个class赞
class Foo
{
int* m_pInts;
bool usedNew;
// other members ...
public:
Foo(size_t num, bool useNew=true) : usedNew(useNew) {
if (usedNew)
m_pInts = new int[num];
else
m_pInts = static_cast<int*>(calloc(num, sizeof(int)));
}
~Foo() {
if (usedNew)
delete[] m_pInts;
else
free(m_pInts);
}
// no copy, but move
Foo(const Foo&) = delete;
Foo& operator=(const Foo&) = delete;
Foo(Foo&& other) {
*this = std::move(other);
}
Foo& operator=(Foo&& other) {
m_pInts = other.m_pInts;
other.m_pInts = nullptr;
usedNew = other.usedNew;
return *this;
}
};
随着数据成员的增加,实现移动变得更加乏味。但是,可移动数据可以放置在单独的 struct
中,其实例由 unique_ptr<>
管理。这允许 =default
用于 move:
class Bar
{
struct Data
{
int* m_pInts;
bool usedNew;
// other members ...
};
std::unique_ptr<Data> m_pData = std::make_unique<Data>();
public:
Bar(size_t num, bool useNew = true) {
m_pData->usedNew = useNew;
if (m_pData->usedNew)
m_pData->usedNew = new int[num];
else
m_pData->m_pInts = static_cast<int*>(calloc(num, sizeof(int)));
}
~Bar() {
if (m_pData->usedNew)
delete[] m_pData->m_pInts;
else
free(m_pData->m_pInts);
}
// no copy, but move
Bar(const Bar&) = delete;
Bar& operator=(const Bar&) = delete;
Bar(Bar&& other) = default;
Bar& operator=(Bar&& other) = default;
};
除了 unique_ptr<>
实例的内存总是在堆上之外,这样的实现还存在什么其他问题?
这就是所谓的零规则。
零规则表明大多数 类 不实施 copy/move assignment/construction 或破坏。相反,您将其委托给资源处理 类.
规则 5 指出,如果您实现了 5 个 copy/move assign/ctor 或 dtor 中的任何一个,您应该实现或删除所有这 5 个(或者,经过适当考虑,默认它们).
在你的例子中,m_pInts
应该是一个唯一的指针,而不是原始内存处理的缓冲区。如果它与某些东西(比如大小)相关联,那么您应该编写一个实现 5 规则的指针和大小结构。或者如果 3 个指针而不是 2 个指针的开销是,您只需使用 std::vector<int>
可以接受。
部分原因是您停止直接调用 new
。 new
是直接管理资源的 5 规则类型中的实现细节。业务逻辑类不要乱用new
。他们既不新建也不删除。
unique_ptr
只是资源管理类型中的一种。 std::string
、std::vector
、std::set
、shared_ptr
、std::future
、std::function
-- 大多数 C++ std
类型都符合条件。自己编写也是一个好主意。但是当你这样做时,你应该从 "business logic".
中删除资源代码
因此,如果您编写了一个 std::function<R(Args...)>
克隆,您将使用 unique_ptr
或 boost::value_ptr
来存储函数对象的内部内容。也许你甚至会写一个 sbo_value_ptr
有时存在于堆上,有时存在于本地。
然后你用 std::function
的 "business logic" 包装它,它理解被指向的东西是可调用的等等。
"business logic" std::function
不会实现 copy/move assign/ctor,也不会实现析构函数。它可能会 =default
他们明确。
是的。您正在寻找的是零规则(作为 Three/Five 规则的 C++11 扩展)。通过让你的数据都知道如何复制和移动自己,外部 class 不需要编写 any 的特殊成员函数。编写那些特殊成员可能容易出错,因此不必编写它们可以解决很多问题。
所以 Foo
会变成:
class Foo
{
std::unique_ptr<size_t[]> data;
public:
Foo(size_t size): data(new size_t[size]) { }
};
而且这很容易证明其正确性。
我的建议是分开关注点和使用组合。
管理分配内存的生命周期是智能指针的工作。如何return那块内存(或其他资源)到运行时是智能指针删除器关心的问题。
一般来说,如果您发现自己在编写移动运算符和移动构造函数,那是因为您没有充分分解问题。
示例:
#include <cstring>
#include <memory>
// a deleter
//
struct delete_or_free
{
void operator()(int* p) const
{
if (free_) {
std::free(p);
}
else {
delete [] p;
}
}
bool free_;
};
class Foo
{
//
// express our memory ownership in terms of a smart pointer.
//
using ptr_type = std::unique_ptr<int[], delete_or_free>;
ptr_type ptr_;
// other members ...
//
// some static helpers (reduces clutter in the constructor)
//
static auto generate_new(int size) {
return ptr_type { new int[size], delete_or_free { false } };
}
static auto generate_calloc(int size) {
return ptr_type {
static_cast<int*>(calloc(size, sizeof(int))),
delete_or_free { true }
};
}
public:
//
// our one and only constructor
//
Foo(size_t num, bool useNew=true)
: ptr_ { useNew ? generate_new(num) : generate_calloc(num) }
{
}
// it's good manners to provide a swap, but not necessary.
void swap(Foo& other) noexcept {
ptr_.swap(other.ptr_);
}
};
//
// test
//
int main()
{
auto a = Foo(100, true);
auto b = Foo(200, false);
auto c = std::move(a);
a = std::move(b);
b = std::move(c);
std::swap(a, b);
}
编辑: 使 Foo
和 Bar
变得不那么琐碎,直接替换为 shared_ptr<>
更加困难。
是否应该 unique_ptr<>
用作实现移动语义的更简单方法?
一个class赞
class Foo
{
int* m_pInts;
bool usedNew;
// other members ...
public:
Foo(size_t num, bool useNew=true) : usedNew(useNew) {
if (usedNew)
m_pInts = new int[num];
else
m_pInts = static_cast<int*>(calloc(num, sizeof(int)));
}
~Foo() {
if (usedNew)
delete[] m_pInts;
else
free(m_pInts);
}
// no copy, but move
Foo(const Foo&) = delete;
Foo& operator=(const Foo&) = delete;
Foo(Foo&& other) {
*this = std::move(other);
}
Foo& operator=(Foo&& other) {
m_pInts = other.m_pInts;
other.m_pInts = nullptr;
usedNew = other.usedNew;
return *this;
}
};
随着数据成员的增加,实现移动变得更加乏味。但是,可移动数据可以放置在单独的 struct
中,其实例由 unique_ptr<>
管理。这允许 =default
用于 move:
class Bar
{
struct Data
{
int* m_pInts;
bool usedNew;
// other members ...
};
std::unique_ptr<Data> m_pData = std::make_unique<Data>();
public:
Bar(size_t num, bool useNew = true) {
m_pData->usedNew = useNew;
if (m_pData->usedNew)
m_pData->usedNew = new int[num];
else
m_pData->m_pInts = static_cast<int*>(calloc(num, sizeof(int)));
}
~Bar() {
if (m_pData->usedNew)
delete[] m_pData->m_pInts;
else
free(m_pData->m_pInts);
}
// no copy, but move
Bar(const Bar&) = delete;
Bar& operator=(const Bar&) = delete;
Bar(Bar&& other) = default;
Bar& operator=(Bar&& other) = default;
};
除了 unique_ptr<>
实例的内存总是在堆上之外,这样的实现还存在什么其他问题?
这就是所谓的零规则。
零规则表明大多数 类 不实施 copy/move assignment/construction 或破坏。相反,您将其委托给资源处理 类.
规则 5 指出,如果您实现了 5 个 copy/move assign/ctor 或 dtor 中的任何一个,您应该实现或删除所有这 5 个(或者,经过适当考虑,默认它们).
在你的例子中,m_pInts
应该是一个唯一的指针,而不是原始内存处理的缓冲区。如果它与某些东西(比如大小)相关联,那么您应该编写一个实现 5 规则的指针和大小结构。或者如果 3 个指针而不是 2 个指针的开销是,您只需使用 std::vector<int>
可以接受。
部分原因是您停止直接调用 new
。 new
是直接管理资源的 5 规则类型中的实现细节。业务逻辑类不要乱用new
。他们既不新建也不删除。
unique_ptr
只是资源管理类型中的一种。 std::string
、std::vector
、std::set
、shared_ptr
、std::future
、std::function
-- 大多数 C++ std
类型都符合条件。自己编写也是一个好主意。但是当你这样做时,你应该从 "business logic".
因此,如果您编写了一个 std::function<R(Args...)>
克隆,您将使用 unique_ptr
或 boost::value_ptr
来存储函数对象的内部内容。也许你甚至会写一个 sbo_value_ptr
有时存在于堆上,有时存在于本地。
然后你用 std::function
的 "business logic" 包装它,它理解被指向的东西是可调用的等等。
"business logic" std::function
不会实现 copy/move assign/ctor,也不会实现析构函数。它可能会 =default
他们明确。
是的。您正在寻找的是零规则(作为 Three/Five 规则的 C++11 扩展)。通过让你的数据都知道如何复制和移动自己,外部 class 不需要编写 any 的特殊成员函数。编写那些特殊成员可能容易出错,因此不必编写它们可以解决很多问题。
所以 Foo
会变成:
class Foo
{
std::unique_ptr<size_t[]> data;
public:
Foo(size_t size): data(new size_t[size]) { }
};
而且这很容易证明其正确性。
我的建议是分开关注点和使用组合。
管理分配内存的生命周期是智能指针的工作。如何return那块内存(或其他资源)到运行时是智能指针删除器关心的问题。
一般来说,如果您发现自己在编写移动运算符和移动构造函数,那是因为您没有充分分解问题。
示例:
#include <cstring>
#include <memory>
// a deleter
//
struct delete_or_free
{
void operator()(int* p) const
{
if (free_) {
std::free(p);
}
else {
delete [] p;
}
}
bool free_;
};
class Foo
{
//
// express our memory ownership in terms of a smart pointer.
//
using ptr_type = std::unique_ptr<int[], delete_or_free>;
ptr_type ptr_;
// other members ...
//
// some static helpers (reduces clutter in the constructor)
//
static auto generate_new(int size) {
return ptr_type { new int[size], delete_or_free { false } };
}
static auto generate_calloc(int size) {
return ptr_type {
static_cast<int*>(calloc(size, sizeof(int))),
delete_or_free { true }
};
}
public:
//
// our one and only constructor
//
Foo(size_t num, bool useNew=true)
: ptr_ { useNew ? generate_new(num) : generate_calloc(num) }
{
}
// it's good manners to provide a swap, but not necessary.
void swap(Foo& other) noexcept {
ptr_.swap(other.ptr_);
}
};
//
// test
//
int main()
{
auto a = Foo(100, true);
auto b = Foo(200, false);
auto c = std::move(a);
a = std::move(b);
b = std::move(c);
std::swap(a, b);
}