共享的、唯一的和弱的——初始化——第2部分





0/5 (0投票)
更多关于 C++ 智能内存管理的内容
上次我们拜访了共享岛,现在只剩下两个岛屿要拜访,但幸运的是,它们离这里不远。所以深呼吸(再次),是时候去唯一的岛屿周围游泳了。=
std::move
()
上一篇文章:共享的、唯一的和弱的——初始化——第1部分
STD::UNIQUE_PTR
我非常喜欢我们第一次到那里旅行时这个岛屿。
构造函数
空/空指针
与std::shared_ptr
相同,我们可以使用空管理的 对象初始化unique_ptr
实例。
std::unique_ptr<int> iu;
std::unique_ptr<int> iu2 = nullptr;
从指针构造
一个接受原始指针的简单构造函数
std::unique_ptr<int> iu(new int(8));
从指针和删除器构造
删除器可以通过值、引用或const
引用传递。这里要记住的主要事情是,删除器直接存储在unique_ptr
中(与std::shared_ptr
不同,std::shared_ptr
中它存储在control_block
中,因此其大小始终为两个指针)。
但删除器直接存储在std::unique_ptr
内部的事实,由于其存储方式的依赖性,在将unique_ptr
从一个实例移动到另一个实例时会产生一些限制。
template <typename T>
struct CustomDeleter {
CustomDeleter() = default;
CustomDeleter(const CustomDeleter&) = default;
CustomDeleter(CustomDeleter&) = default;
CustomDeleter(CustomDeleter&&) = default;
void operator()(T* p) const { delete p; };
};
{
CustomDeleter<int> deleter;
std::cout << "Example 1\n";
std::unique_ptr<int, CustomDeleter<int>> foo_unique(new int(),
CustomDeleter<int>()); // move CustomDeleter
std::unique_ptr<int, CustomDeleter<int>> f2 =
std::move(foo_unique); // move CustomDeleter
std::cout << "Example 2\n";
std::unique_ptr<int, CustomDeleter<int>&>
f3(new int(), deleter); // reference CustomDeleter
std::unique_ptr<int, CustomDeleter<int>&> f4 =
std::move(f3); // reference CustomDeleter
// std::unique_ptr<int, CustomDeleter<int>&&> f41 =
std::move(f4); // Won't compile
std::unique_ptr<int, CustomDeleter<int>> f5 =
std::move(f4); // non-const copy CustomDeleter
// std::unique_ptr<Foo, CustomDeleter<int>&> f6 = std::move(f5); // Won't compile
std::cout << "Example 3\n";
std::unique_ptr<int,
const CustomDeleter<int>&> f7(new int(), deleter); // reference CustomDeleter
std::unique_ptr<int, CustomDeleter<int>> f8 = std::move(f7); // copy CustomDeleter
//std::unique_ptr<int, CustomDeleter<int>&> f9 = std::move(f8); // Won't compile
}
这种CustomDeleter
只是为唯一ptr
创建自定义删除器的一些方法之一。有一些权衡,如可读性、需求等,可能会影响您为每个std::unique_ptr
实例选择的特定类型。然而,除了所有这些权衡之外,还有一件事需要考虑:unique_ptr
实例的大小,因为删除器直接存储在实例内部。
函数指针
另一个要存储在std::unique_ptr
实例中的指针。在这种情况下,我们将得到sizeof(void*) * 2
的大小。一个指向实际数据的指针,一个指向delete
函数的指针。
std::unique_ptr<int, void(*)(int*)> my_unique(new int(), [](int*){/*...*/});
std::function
这个给我们带来了一点开销。std::function
的纯大小(在cpo.sh上测试,32位操作系统)是24字节。加上指针大小的对齐,我们得到总大小32字节。
但是,我们可以通过引用将其传递给unique_ptr
实例,然后,我们将得到两个指针的总大小(并且std::function
实例存储在外部)。
std::unique_ptr<int, std::function<void(int*)>> my_unique(new int(),
[](int*){/*...*/}); // By value, size: 32 bytes.
std::function<void(int*)> my_del = [](int*){/*...*/};
std::unique_ptr<int, std::function<void(int*)>&>
my_unique2(new int(), my_del); // By reference, size: 8 bytes.
Lambda
如果您不熟悉其背后的实际结构,Lambda可能有点棘手。因此,我建议阅读其背后的真相。
当谈到空捕获的lambda时,它本身会占用一个字节,因为它是对象的最小大小。但是,当将其存储在std::unique_ptr
中时,一些编译器(如llvm
)仍然可能使unique_ptr
实例的总大小达到4字节。如果它看起来像魔术,闻起来像魔术,并且可以像魔术一样消失,那么它真的必须是魔术吗?我将在下一篇文章中进一步讨论一些编译器中的这种魔术行为,但现在,让我们假设没有这种魔术,只使用预期的行为。
因此,如果lambda大小为1字节,指针大小为4字节,默认对齐,我们将得到一个总大小为8字节的std::unique_ptr
实例。当我们通过引用传递lambda时,使用的大小相同。
注意:如果lambda捕获不为空,则lambda大小可能与1不同,但引用大小将保持不变。
auto my_deleter = [](int*){};
std::unique_ptr<int, decltype(my_deleter)> my_unique(new int(), my_deleter);
空仿函数
与lambda(实际上是一个仿函数)的行为相同。
template <typename T>
class DeleteFunctor {
public:
void operator()(T* p) const {
delete p;
}
};
{
std::unique_ptr<int, DeleteFunctor<int>> mu(new int(), DeleteFunctor<int>());
}
从唯一构造
正如我们之前遇到的,我们可以将所有权从一个uniqe_ptr
实例移动到另一个。无法复制一个到另一个,因为它违反了唯一所有权的概念。一旦我们移动了所有权,原始的unique_ptr
将被重置为nullptr
。
std::unique_ptr<int> mu(new int());
auto mu1 = std::move(mu);
// auto mu2 = mu1; // Won't compile
外部唯一初始化器
与std::shared_ptr
类似,unique_ptr
可以使用外部初始化器std::make_unique
进行初始化。
std::make_unique
上次,我们看到了std::make_shared
的所有优点。然而,unique_ptr
除了管理的 对象之外不管理任何指针。那么为什么要使用这种方法呢?让我们以以下函数为例
void my_func(std::unique_ptr<int> iptr1, std::unique_ptr<int> iptr2);
// Possible call 1:
my_func(std::unique_ptr<int>(new int(1)), std::unique_ptr<int>(new int(2)));
// Possible call 2:
my_func(std::make_unique<int>(1), std::make_unique<int>(2));
第二次调用比第一次调用有三个主要好处。
- 第一个优点很容易发现——减少了冗余初始化。当使用
std::unique_ptr
构造函数时,我们必须重复具体类型两次。一次作为模板的一部分,另一次作为new
调用的一部分。 - 第二个优点是能够避免
new
关键字,作为代码维护的一部分,并使用良好的实践约定。 - 第三个优点是唯一可能防止实际和立即隐藏错误(bug)的优点。但为了理解它,并使错误(bug)更明显,我们应该知道一个规则。
评估顺序(直到C++17)
来自cppreference:“任何表达式的任何部分的评估顺序,包括函数参数的评估顺序都是未指定的(下面列出了一些例外)。编译器可以以任何顺序评估操作数和其他子表达式,并且在再次评估同一表达式时可能会选择不同的顺序。”
现在,让我们再看看第一个调用示例
my_func(std::unique_ptr<int>(new int(1)), std::unique_ptr<int>(new int(2)));
/* Possible order of evaluation:
1. new int(1)
2. new int(2) // May throw
3. std::unique_ptr<int>(new int(2))
4. std::unique_ptr<int>(new int(1))
*/
现在,假设第二个表达式(new int(2)
)抛出异常,那么new int(1)
将发生内存泄漏。使用std::make_unique
代替时,此问题不存在。
C++17 注意:从C++17开始,评估顺序增加了几条规则,其中一条正好解决了这个问题
来自cppreference:“15) 在函数调用中,每个参数的初始化的值计算和副作用与任何其他参数的值计算和副作用是无确定顺序的。”
这意味着,编译器必须在继续下一个参数之前完全处理每个参数。处理参数的顺序仍然未指定,但它不能部分处理一个参数然后继续处理另一个参数。(特别感谢Yehezkel Bernat提供此说明。)
std::make_unique 的缺点
然而,这个std::make_unique
函数有一个限制,可能不适用于所有情况:您不能用它传递自定义删除器。在std::shared_ptr
中,这个问题已经通过std::allocate_shared
解决,但对于唯一情况(unique case)还没有类似的东西(尚无)。有一个关于它的提案,但到目前为止还没有进入标准P0211。
STD::WEAK_PTR
std::weak_ptr
负责持有由shared_ptr
在外部管理的指针。除非用nullptr
初始化,否则不能在没有shared_ptr
实例或另一个weak_ptr
实例的情况下初始化弱指针。为了访问底层指针,我们必须从中创建一个shared_ptr
实例,并通过它访问它。您可以在系列的第一篇文章中进一步阅读。=
.lock().
构造函数
默认构造函数
默认构造函数使用nullptr control_block
初始化weak_ptr
实例。
从共享初始化
weak_ptr
的目的是从shared_ptr
实例创建,以便在不拥有所有权的情况下访问托管对象。常见的用例是解决循环指向问题。
auto is = std::make_shared<int>(); // shard_counter = 1, weak_counter = 0
std::weak_ptr<int> iu = is; // shard_counter = 1, weak_counter = 1
从弱指针复制
一个weak_ptr
可以获取另一个weak_ptr
实例的副本。如果另一个实例拥有一个已初始化的control_block
,此调用将增加共享control_block
内的弱计数。
auto is = std::make_shared<int>(); // shard_counter = 1, weak_counter = 0
std::weak_ptr<int> iu = is; // shard_counter = 1, weak_counter = 1
auto iu1 = iu; // shard_counter = 1, weak_counter = 2
从弱指针移动
我们可以将一个weak_ptr
实例移动到另一个,通过这种方式,原始weak_ptr
的control_block
将变为nullptr
。
auto is = std::make_shared<int>(); // shard_counter = 1, weak_counter = 0
std::weak_ptr<int> iu = is; // shard_counter = 1, weak_counter = 1
auto iu1 = std::move(iu); // shard_counter = 1, weak_counter = 1
// iu: shared_counter = 0, weak_counter = 0
结论
智能指针比通常看起来要多,而且有很多方法可以创建和使用它们。然而,我们还有一些尚未谈论的隐藏魔术,在下一篇文章中,我们将揭示一些编译器用来优化unique_ptr
实例所占内存空间的隐藏秘密。所以请继续关注,并随时在评论中分享(ptr)您的想法。