EffectiveModernCppChinese/4.SmartPointers/item19.html
2022-11-25 05:43:31 +00:00

307 lines
40 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE HTML>
<html lang="zh" class="sidebar-visible no-js light">
<head>
<!-- Book generated using mdBook -->
<meta charset="UTF-8">
<title>Item 19:对于共享资源使用std::shared_ptr - Effective Modern C++</title>
<!-- Custom HTML head -->
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#ffffff" />
<link rel="icon" href="../favicon.svg">
<link rel="shortcut icon" href="../favicon.png">
<link rel="stylesheet" href="../css/variables.css">
<link rel="stylesheet" href="../css/general.css">
<link rel="stylesheet" href="../css/chrome.css">
<link rel="stylesheet" href="../css/print.css" media="print">
<!-- Fonts -->
<link rel="stylesheet" href="../FontAwesome/css/font-awesome.css">
<link rel="stylesheet" href="../fonts/fonts.css">
<!-- Highlight.js Stylesheets -->
<link rel="stylesheet" href="../highlight.css">
<link rel="stylesheet" href="../tomorrow-night.css">
<link rel="stylesheet" href="../ayu-highlight.css">
<!-- Custom theme stylesheets -->
<!-- MathJax -->
<script async type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
</head>
<body>
<!-- Provide site root to javascript -->
<script type="text/javascript">
var path_to_root = "../";
var default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "navy" : "light";
</script>
<!-- Work around some values being stored in localStorage wrapped in quotes -->
<script type="text/javascript">
try {
var theme = localStorage.getItem('mdbook-theme');
var sidebar = localStorage.getItem('mdbook-sidebar');
if (theme.startsWith('"') && theme.endsWith('"')) {
localStorage.setItem('mdbook-theme', theme.slice(1, theme.length - 1));
}
if (sidebar.startsWith('"') && sidebar.endsWith('"')) {
localStorage.setItem('mdbook-sidebar', sidebar.slice(1, sidebar.length - 1));
}
} catch (e) { }
</script>
<!-- Set the theme before any content is loaded, prevents flash -->
<script type="text/javascript">
var theme;
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
if (theme === null || theme === undefined) { theme = default_theme; }
var html = document.querySelector('html');
html.classList.remove('no-js')
html.classList.remove('light')
html.classList.add(theme);
html.classList.add('js');
</script>
<!-- Hide / unhide sidebar before it is displayed -->
<script type="text/javascript">
var html = document.querySelector('html');
var sidebar = 'hidden';
if (document.body.clientWidth >= 1080) {
try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
sidebar = sidebar || 'visible';
}
html.classList.remove('sidebar-visible');
html.classList.add("sidebar-" + sidebar);
</script>
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
<div class="sidebar-scrollbox">
<ol class="chapter"><li class="chapter-item expanded "><a href="../Introduction.html">简介</a></li><li class="chapter-item expanded "><div>第一章 类型推导</div></li><li><ol class="section"><li class="chapter-item expanded "><a href="../1.DeducingTypes/item1.html">Item 1:理解模板类型推导</a></li><li class="chapter-item expanded "><a href="../1.DeducingTypes/item2.html">Item 2:理解auto类型推导</a></li><li class="chapter-item expanded "><a href="../1.DeducingTypes/item3.html">Item 3:理解decltype</a></li><li class="chapter-item expanded "><a href="../1.DeducingTypes/item4.html">Item 4:学会查看类型推导结果</a></li></ol></li><li class="chapter-item expanded "><div>第二章 auto</div></li><li><ol class="section"><li class="chapter-item expanded "><a href="../2.Auto/item5.html">Item 5:优先考虑auto而非显式类型声明</a></li><li class="chapter-item expanded "><a href="../2.Auto/item6.html">Item 6:auto推导若非己愿使用显式类型初始化惯用法</a></li></ol></li><li class="chapter-item expanded "><div>第三章 移步现代C++</div></li><li><ol class="section"><li class="chapter-item expanded "><a href="../3.MovingToModernCpp/item7.html">Item 7:区别使用()和{}创建对象</a></li><li class="chapter-item expanded "><a href="../3.MovingToModernCpp/item8.html">Item 8:优先考虑nullptr而非0和NULL</a></li><li class="chapter-item expanded "><a href="../3.MovingToModernCpp/item9.html">Item 9:优先考虑别名声明而非typedefs</a></li><li class="chapter-item expanded "><a href="../3.MovingToModernCpp/item10.html">Item 10:优先考虑限域枚举而非未限域枚举</a></li><li class="chapter-item expanded "><a href="../3.MovingToModernCpp/item11.html">Item 11:优先考虑使用deleted函数而非使用未定义的私有声明</a></li><li class="chapter-item expanded "><a href="../3.MovingToModernCpp/item12.html">Item 12:使用override声明重载函数</a></li><li class="chapter-item expanded "><a href="../3.MovingToModernCpp/item13.html">Item 13:优先考虑const_iterator而非iterator</a></li><li class="chapter-item expanded "><a href="../3.MovingToModernCpp/item14.html">Item 14:如果函数不抛出异常请使用noexcept</a></li><li class="chapter-item expanded "><a href="../3.MovingToModernCpp/item15.html">Item 15:尽可能的使用constexpr</a></li><li class="chapter-item expanded "><a href="../3.MovingToModernCpp/item16.html">Item 16:让const成员函数线程安全</a></li><li class="chapter-item expanded "><a href="../3.MovingToModernCpp/item17.html">Item 17:理解特殊成员函数函数的生成</a></li></ol></li><li class="chapter-item expanded "><div>第四章 智能指针</div></li><li><ol class="section"><li class="chapter-item expanded "><a href="../4.SmartPointers/item18.html">Item 18:对于独占资源使用std::unique_ptr</a></li><li class="chapter-item expanded "><a href="../4.SmartPointers/item19.html" class="active">Item 19:对于共享资源使用std::shared_ptr</a></li><li class="chapter-item expanded "><a href="../4.SmartPointers/item20.html">Item 20:当std::shared_ptr可能悬空时使用std::weak_ptr</a></li><li class="chapter-item expanded "><a href="../4.SmartPointers/item21.html">Item 21:优先考虑使用std::make_unique和std::make_shared而非new</a></li><li class="chapter-item expanded "><a href="../4.SmartPointers/item22.html">Item 22:当使用Pimpl惯用法请在实现文件中定义特殊成员函数</a></li></ol></li><li class="chapter-item expanded "><div>第五章 右值引用,移动语义,完美转发</div></li><li><ol class="section"><li class="chapter-item expanded "><a href="../5.RRefMovSemPerfForw/item23.html">Item 23:理解std::move和std::forward</a></li><li class="chapter-item expanded "><a href="../5.RRefMovSemPerfForw/item24.html">Item 24:区别通用引用和右值引用</a></li><li class="chapter-item expanded "><a href="../5.RRefMovSemPerfForw/item25.html">Item 25:对于右值引用使用std::move对于通用引用使用std::forward</a></li><li class="chapter-item expanded "><a href="../5.RRefMovSemPerfForw/item26.html">Item 26:避免重载通用引用</a></li><li class="chapter-item expanded "><a href="../5.RRefMovSemPerfForw/item27.html">Item 27:熟悉重载通用引用的替代品</a></li><li class="chapter-item expanded "><a href="../5.RRefMovSemPerfForw/item28.html">Item 28:理解引用折叠</a></li><li class="chapter-item expanded "><a href="../5.RRefMovSemPerfForw/item29.html">Item 29:认识移动操作的缺点</a></li><li class="chapter-item expanded "><a href="../5.RRefMovSemPerfForw/item30.html">Item 30:熟悉完美转发失败的情况</a></li></ol></li><li class="chapter-item expanded "><div>第六章 Lambda表达式</div></li><li><ol class="section"><li class="chapter-item expanded "><a href="../6.LambdaExpressions/item31.html">Item 31:避免使用默认捕获模式</a></li><li class="chapter-item expanded "><a href="../6.LambdaExpressions/item32.html">Item 32:使用初始化捕获来移动对象到闭包中</a></li><li class="chapter-item expanded "><a href="../6.LambdaExpressions/item33.html">Item 33:对于std::forward的auto&&形参使用decltype</a></li><li class="chapter-item expanded "><a href="../6.LambdaExpressions/item34.html">Item 34:优先考虑lambda表达式而非std::bind</a></li></ol></li><li class="chapter-item expanded "><div>第七章 并发API</div></li><li><ol class="section"><li class="chapter-item expanded "><a href="../7.TheConcurrencyAPI/Item35.html">Item 35:优先考虑基于任务的编程而非基于线程的编程</a></li><li class="chapter-item expanded "><a href="../7.TheConcurrencyAPI/item36.html">Item 36:如果有异步的必要请指定std::launch::threads</a></li><li class="chapter-item expanded "><a href="../7.TheConcurrencyAPI/item37.html">Item 37:从各个方面使得std::threads unjoinable</a></li><li class="chapter-item expanded "><a href="../7.TheConcurrencyAPI/item38.html">Item 38:关注不同线程句柄析构行为</a></li><li class="chapter-item expanded "><a href="../7.TheConcurrencyAPI/item39.html">Item 39:考虑对于单次事件通信使用void</a></li><li class="chapter-item expanded "><a href="../7.TheConcurrencyAPI/item40.html">Item 40:对于并发使用std::atomicvolatile用于特殊内存区</a></li></ol></li><li class="chapter-item expanded "><div>第八章 微调</div></li><li><ol class="section"><li class="chapter-item expanded "><a href="../8.Tweaks/item41.html">Item 41:对于那些可移动总是被拷贝的形参使用传值方式</a></li><li class="chapter-item expanded "><a href="../8.Tweaks/item42.html">Item 42:考虑就地创建而非插入</a></li></ol></li></ol>
</div>
<div id="sidebar-resize-handle" class="sidebar-resize-handle"></div>
</nav>
<div id="page-wrapper" class="page-wrapper">
<div class="page">
<div id="menu-bar-hover-placeholder"></div>
<div id="menu-bar" class="menu-bar sticky bordered">
<div class="left-buttons">
<button id="sidebar-toggle" class="icon-button" type="button" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
<i class="fa fa-bars"></i>
</button>
<button id="theme-toggle" class="icon-button" type="button" title="Change theme" aria-label="Change theme" aria-haspopup="true" aria-expanded="false" aria-controls="theme-list">
<i class="fa fa-paint-brush"></i>
</button>
<ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu">
<li role="none"><button role="menuitem" class="theme" id="light">Light (default)</button></li>
<li role="none"><button role="menuitem" class="theme" id="rust">Rust</button></li>
<li role="none"><button role="menuitem" class="theme" id="coal">Coal</button></li>
<li role="none"><button role="menuitem" class="theme" id="navy">Navy</button></li>
<li role="none"><button role="menuitem" class="theme" id="ayu">Ayu</button></li>
</ul>
<button id="search-toggle" class="icon-button" type="button" title="Search. (Shortkey: s)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="S" aria-controls="searchbar">
<i class="fa fa-search"></i>
</button>
</div>
<h1 class="menu-title">Effective Modern C++</h1>
<div class="right-buttons">
<a href="../print.html" title="Print this book" aria-label="Print this book">
<i id="print-button" class="fa fa-print"></i>
</a>
<a href="https://github.com/CnTransGroup/EffectiveModernCppChinese" title="Git repository" aria-label="Git repository">
<i id="git-repository-button" class="fa fa-github"></i>
</a>
</div>
</div>
<div id="search-wrapper" class="hidden">
<form id="searchbar-outer" class="searchbar-outer">
<input type="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
</form>
<div id="searchresults-outer" class="searchresults-outer hidden">
<div id="searchresults-header" class="searchresults-header"></div>
<ul id="searchresults">
</ul>
</div>
</div>
<!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
<script type="text/javascript">
document.getElementById('sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
document.getElementById('sidebar').setAttribute('aria-hidden', sidebar !== 'visible');
Array.from(document.querySelectorAll('#sidebar a')).forEach(function(link) {
link.setAttribute('tabIndex', sidebar === 'visible' ? 0 : -1);
});
</script>
<div id="content" class="content">
<main>
<h2 id="条款十九对于共享资源使用stdshared_ptr"><a class="header" href="#条款十九对于共享资源使用stdshared_ptr">条款十九:对于共享资源使用<code>std::shared_ptr</code></a></h2>
<p><strong>Item 19: Use <code>std::shared_ptr</code> for shared-ownership resource management</strong></p>
<p>使用带垃圾回收的语言的程序员指着C++程序员笑看他们如何防止资源泄露。“真是原始啊”他们嘲笑着说“你们没有从1960年的Lisp那里得到启发吗机器应该自己管理资源的生命周期而不应该依赖人类。”C++程序员翻白眼“你们得到的所谓启示就是只有内存算资源而且资源回收的时间点是不确定的我们更喜欢通用可预料的销毁谢谢你。”但我们的虚张声势可能底气不足。因为垃圾回收真的很方便而且手动管理生命周期真的就像是使用石头小刀和兽皮制作RAM电路。为什么我们不能同时有两个完美的世界一个自动工作的世界像是垃圾回收一个销毁可预测的世界像是析构</p>
<p>C++11中的<code>std::shared_ptr</code>将两者组合了起来。一个通过<code>std::shared_ptr</code>访问的对象其生命周期由指向它的有共享所有权(<em>shared ownership</em>)的指针们来管理。没有特定的<code>std::shared_ptr</code>拥有该对象。相反,所有指向它的<code>std::shared_ptr</code>都能相互合作确保在它不再使用的那个点进行析构。当最后一个指向某对象的<code>std::shared_ptr</code>不再指向那(比如因为<code>std::shared_ptr</code>被销毁或者指向另一个不同的对象),<code>std::shared_ptr</code>会销毁它所指向的对象。就垃圾回收来说,客户端不需要关心指向对象的生命周期,而对象的析构是确定性的。</p>
<p><code>std::shared_ptr</code>通过引用计数(<em>reference count</em>)来确保它是否是最后一个指向某种资源的指针,引用计数关联资源并跟踪有多少<code>std::shared_ptr</code>指向该资源。<code>std::shared_ptr</code>构造函数递增引用计数值(注意是<strong>通常</strong>——原因参见下面),析构函数递减值,拷贝赋值运算符做前面这两个工作。(如果<code>sp1</code><code>sp2</code><code>std::shared_ptr</code>并且指向不同对象,赋值“<code>sp1 = sp2;</code>”会使<code>sp1</code>指向<code>sp2</code>指向的对象。直接效果就是<code>sp1</code>引用计数减一,<code>sp2</code>引用计数加一。)如果<code>std::shared_ptr</code>在计数值递减后发现引用计数值为零,没有其他<code>std::shared_ptr</code>指向该资源,它就会销毁资源。</p>
<p>引用计数暗示着性能问题:</p>
<ul>
<li><strong><code>std::shared_ptr</code>大小是原始指针的两倍</strong>因为它内部包含一个指向资源的原始指针还包含一个指向资源的引用计数值的原始指针。这种实现法并不是标准要求的但是我指原书作者Scott Meyers熟悉的所有标准库都这样实现。</li>
<li><strong>引用计数的内存必须动态分配</strong>。 概念上,引用计数与所指对象关联起来,但是实际上被指向的对象不知道这件事情(译注:不知道有一个关联到自己的计数值)。因此它们没有办法存放一个引用计数值。(一个好消息是任何对象——甚至是内置类型的——都可以由<code>std::shared_ptr</code>管理。)<a href="../4.SmartPointers/item21.html">Item21</a>会解释使用<code>std::make_shared</code>创建<code>std::shared_ptr</code>可以避免引用计数的动态分配,但是还存在一些<code>std::make_shared</code>不能使用的场景,这时候引用计数就会动态分配。</li>
<li><strong>递增递减引用计数必须是原子性的</strong>因为多个reader、writer可能在不同的线程。比如指向某种资源的<code>std::shared_ptr</code>可能在一个线程执行析构(于是递减指向的对象的引用计数),在另一个不同的线程,<code>std::shared_ptr</code>指向相同的对象,但是执行的却是拷贝操作(因此递增了同一个引用计数)。原子操作通常比非原子操作要慢,所以即使引用计数通常只有一个<em>word</em>大小,你也应该假定读写它们是存在开销的。</li>
</ul>
<p>我写道<code>std::shared_ptr</code>构造函数只是“通常”递增指向对象的引用计数会不会让你有点好奇?创建一个指向对象的<code>std::shared_ptr</code>就产生了又一个指向那个对象的<code>std::shared_ptr</code>,为什么我没说<strong>总是</strong>增加引用计数值?</p>
<p>原因是移动构造函数的存在。从另一个<code>std::shared_ptr</code>移动构造新<code>std::shared_ptr</code>会将原来的<code>std::shared_ptr</code>设置为null那意味着老的<code>std::shared_ptr</code>不再指向资源,同时新的<code>std::shared_ptr</code>指向资源。这样的结果就是不需要修改引用计数值。因此移动<code>std::shared_ptr</code>会比拷贝它要快:拷贝要求递增引用计数值,移动不需要。移动赋值运算符同理,所以移动构造比拷贝构造快,移动赋值运算符也比拷贝赋值运算符快。</p>
<p>类似<code>std::unique_ptr</code>(参见<a href="../4.SmartPointers/item18.html">Item18</a><code>std::shared_ptr</code>使用<code>delete</code>作为资源的默认销毁机制,但是它也支持自定义的删除器。这种支持有别于<code>std::unique_ptr</code>。对于<code>std::unique_ptr</code>来说,删除器类型是智能指针类型的一部分。对于<code>std::shared_ptr</code>则不是:</p>
<pre><code class="language-CPP">auto loggingDel = [](Widget *pw) //自定义删除器
{ //和条款18一样
makeLogEntry(pw);
delete pw;
};
std::unique_ptr&lt; //删除器类型是
Widget, decltype(loggingDel) //指针类型的一部分
&gt; upw(new Widget, loggingDel);
std::shared_ptr&lt;Widget&gt; //删除器类型不是
spw(new Widget, loggingDel); //指针类型的一部分
</code></pre>
<p><code>std::shared_ptr</code>的设计更为灵活。考虑有两个<code>std::shared_ptr&lt;Widget&gt;</code>,每个自带不同的删除器(比如通过<em>lambda</em>表达式自定义删除器):</p>
<pre><code class="language-CPP">auto customDeleter1 = [](Widget *pw) { … }; //自定义删除器,
auto customDeleter2 = [](Widget *pw) { … }; //每种类型不同
std::shared_ptr&lt;Widget&gt; pw1(new Widget, customDeleter1);
std::shared_ptr&lt;Widget&gt; pw2(new Widget, customDeleter2);
</code></pre>
<p>因为<code>pw1</code><code>pw2</code>有相同的类型,所以它们都可以放到存放那个类型的对象的容器中:</p>
<pre><code class="language-CPP">std::vector&lt;std::shared_ptr&lt;Widget&gt;&gt; vpw{ pw1, pw2 };
</code></pre>
<p>它们也能相互赋值,也可以传入一个形参为<code>std::shared_ptr&lt;Widget&gt;</code>的函数。但是自定义删除器类型不同的<code>std::unique_ptr</code>就不行,因为<code>std::unique_ptr</code>把删除器视作类型的一部分。</p>
<p>另一个不同于<code>std::unique_ptr</code>的地方是,指定自定义删除器不会改变<code>std::shared_ptr</code>对象的大小。不管删除器是什么,一个<code>std::shared_ptr</code>对象都是两个指针大小。这是个好消息,但是它应该让你隐隐约约不安。自定义删除器可以是函数对象,函数对象可以包含任意多的数据。它意味着函数对象是任意大的。<code>std::shared_ptr</code>怎么能引用一个任意大的删除器而不使用更多的内存?</p>
<p>它不能。它必须使用更多的内存。然而,那部分内存不是<code>std::shared_ptr</code>对象的一部分。那部分在堆上面,或者<code>std::shared_ptr</code>创建者利用<code>std::shared_ptr</code>对自定义分配器的支持能力,那部分内存随便在哪都行。我前面提到了<code>std::shared_ptr</code>对象包含了所指对象的引用计数的指针。没错,但是有点误导人。因为引用计数是另一个更大的数据结构的一部分,那个数据结构通常叫做<strong>控制块</strong><em>control block</em>)。每个<code>std::shared_ptr</code>管理的对象都有个相应的控制块。控制块除了包含引用计数值外还有一个自定义删除器的拷贝,当然前提是存在自定义删除器。如果用户还指定了自定义分配器,控制块也会包含一个分配器的拷贝。控制块可能还包含一些额外的数据,正如<a href="../4.SmartPointers/item21.html">Item21</a>提到的,一个次级引用计数<em>weak count</em>,但是目前我们先忽略它。我们可以想象<code>std::shared_ptr</code>对象在内存中是这样:</p>
<p><img src="media/item19_fig1.png" alt="item19_fig1" /></p>
<p>当指向对象的<code>std::shared_ptr</code>一创建,对象的控制块就建立了。至少我们期望是如此。通常,对于一个创建指向对象的<code>std::shared_ptr</code>的函数来说不可能知道是否有其他<code>std::shared_ptr</code>早已指向那个对象,所以控制块的创建会遵循下面几条规则:</p>
<ul>
<li><strong><code>std::make_shared</code>(参见<a href="../4.SmartPointers/item21.html">Item21</a>)总是创建一个控制块</strong>。它创建一个要指向的新对象,所以可以肯定<code>std::make_shared</code>调用时对象不存在其他控制块。</li>
<li><strong>当从独占指针(即<code>std::unique_ptr</code>或者<code>std::auto_ptr</code>)上构造出<code>std::shared_ptr</code>时会创建控制块</strong>。独占指针没有使用控制块,所以指针指向的对象没有关联控制块。(作为构造的一部分,<code>std::shared_ptr</code>侵占独占指针所指向的对象的独占权所以独占指针被设置为null</li>
<li><strong>当从原始指针上构造出<code>std::shared_ptr</code>时会创建控制块</strong>。如果你想从一个早已存在控制块的对象上创建<code>std::shared_ptr</code>,你将假定传递一个<code>std::shared_ptr</code>或者<code>std::weak_ptr</code>(参见<a href="../4.SmartPointers/item20.html">Item20</a>)作为构造函数实参,而不是原始指针。用<code>std::shared_ptr</code>或者<code>std::weak_ptr</code>作为构造函数实参创建<code>std::shared_ptr</code>不会创建新控制块,因为它可以依赖传递来的智能指针指向控制块。</li>
</ul>
<p>这些规则造成的后果就是从原始指针上构造超过一个<code>std::shared_ptr</code>就会让你走上未定义行为的快车道,因为指向的对象有多个控制块关联。多个控制块意味着多个引用计数值,多个引用计数值意味着对象将会被销毁多次(每个引用计数一次)。那意味着像下面的代码是有问题的,很有问题,问题很大:</p>
<pre><code class="language-cpp">auto pw = new Widget; //pw是原始指针
std::shared_ptr&lt;Widget&gt; spw1(pw, loggingDel); //为*pw创建控制块
std::shared_ptr&lt;Widget&gt; spw2(pw, loggingDel); //为*pw创建第二个控制块
</code></pre>
<p>创建原始指针<code>pw</code>指向动态分配的对象很糟糕,因为它完全背离了这章的建议:倾向于使用智能指针而不是原始指针。(如果你忘记了该建议的动机,请翻到<a href="../4.SmartPointers/item18.html">本章开头</a>)。撇开那个不说,创建<code>pw</code>那一行代码虽然让人厌恶,但是至少不会造成未定义程序行为。</p>
<p>现在,传给<code>spw1</code>的构造函数一个原始指针,它会为指向的对象创建一个控制块(因此有个引用计数值)。这种情况下,指向的对象是<code>*pw</code>(即<code>pw</code>指向的对象)。就其本身而言没什么问题,但是将同样的原始指针传递给<code>spw2</code>的构造函数会再次为<code>*pw</code>创建一个控制块(所以也有个引用计数值)。因此<code>*pw</code>有两个引用计数值,每一个最后都会变成零,然后最终导致<code>*pw</code>销毁两次。第二个销毁会产生未定义行为。</p>
<p><code>std::shared_ptr</code>给我们上了两堂课。第一,避免传给<code>std::shared_ptr</code>构造函数原始指针。通常替代方案是使用<code>std::make_shared</code>(参见<a href="../4.SmartPointers/item21.html">Item21</a>),不过上面例子中,我们使用了自定义删除器,用<code>std::make_shared</code>就没办法做到。第二,如果你必须传给<code>std::shared_ptr</code>构造函数原始指针,直接传<code>new</code>出来的结果,不要传指针变量。如果上面代码第一部分这样重写:</p>
<pre><code class="language-cpp">std::shared_ptr&lt;Widget&gt; spw1(new Widget, //直接使用new的结果
loggingDel);
</code></pre>
<p>会少了很多从原始指针上构造第二个<code>std::shared_ptr</code>的诱惑。相应的,创建<code>spw2</code>也会很自然的用<code>spw1</code>作为初始化参数(即用<code>std::shared_ptr</code>拷贝构造函数),那就没什么问题了:</p>
<pre><code class="language-CPP">std::shared_ptr&lt;Widget&gt; spw2(spw1); //spw2使用spw1一样的控制块
</code></pre>
<p>一个尤其令人意外的地方是使用<code>this</code>指针作为<code>std::shared_ptr</code>构造函数实参的时候可能导致创建多个控制块。假设我们的程序使用<code>std::shared_ptr</code>管理<code>Widget</code>对象,我们有一个数据结构用于跟踪已经处理过的<code>Widget</code>对象:</p>
<pre><code class="language-cpp">std::vector&lt;std::shared_ptr&lt;Widget&gt;&gt; processedWidgets;
</code></pre>
<p>继续,假设<code>Widget</code>有一个用于处理的成员函数:</p>
<pre><code class="language-cpp">class Widget {
public:
void process();
};
</code></pre>
<p>对于<code>Widget::process</code>看起来合理的代码如下:</p>
<pre><code class="language-cpp">void Widget::process()
{
… //处理Widget
processedWidgets.emplace_back(this); //然后将它加到已处理过的Widget
} //的列表中,这是错的!
</code></pre>
<p>注释已经说了这是错的——或者至少大部分是错的。(错误的部分是传递<code>this</code>,而不是使用了<code>emplace_back</code>。如果你不熟悉<code>emplace_back</code>,参见<a href="../8.Tweaks/item42.html">Item42</a>)。上面的代码可以通过编译,但是向<code>std::shared_ptr</code>的容器传递一个原始指针(<code>this</code><code>std::shared_ptr</code>会由此为指向的<code>Widget</code><code>*this</code>)创建一个控制块。那看起来没什么问题,直到你意识到如果成员函数外面早已存在指向那个<code>Widget</code>对象的指针它是未定义行为的Game, Set, and Match译注一部关于网球的电影但是译者没看过。句子本意“压倒性胜利比赛结束”</p>
<p><code>std::shared_ptr</code>API已有处理这种情况的设施。它的名字可能是C++标准库中最奇怪的一个:<code>std::enable_shared_from_this</code>。如果你想创建一个用<code>std::shared_ptr</code>管理的类,这个类能够用<code>this</code>指针安全地创建一个<code>std::shared_ptr</code><code>std::enable_shared_from_this</code>就可作为基类的模板类。在我们的例子中,<code>Widget</code>将会继承自<code>std::enable_shared_from_this</code></p>
<pre><code class="language-cpp">class Widget: public std::enable_shared_from_this&lt;Widget&gt; {
public:
void process();
};
</code></pre>
<p>正如我所说,<code>std::enable_shared_from_this</code>是一个基类模板。它的模板参数总是某个继承自它的类,所以<code>Widget</code>继承自<code>std::enable_shared_from_this&lt;Widget&gt;</code>。如果某类型继承自一个由该类型(译注:作为模板类型参数)进行模板化得到的基类这个东西让你心脏有点遭不住,别去想它就好了。代码完全合法,而且它背后的设计模式也是没问题的,并且这种设计模式还有个标准名字,尽管该名字和<code>std::enable_shared_from_this</code>一样怪异。这个标准名字就是奇异递归模板模式(<em>The Curiously Recurring Template Pattern</em><em>CRTP</em>))。如果你想学更多关于它的内容,请搜索引擎一展身手,现在我们要回到<code>std::enable_shared_from_this</code>上。</p>
<p><code>std::enable_shared_from_this</code>定义了一个成员函数,成员函数会创建指向当前对象的<code>std::shared_ptr</code>却不创建多余控制块。这个成员函数就是<code>shared_from_this</code>,无论在哪当你想在成员函数中使用<code>std::shared_ptr</code>指向<code>this</code>所指对象时都请使用它。这里有个<code>Widget::process</code>的安全实现:</p>
<pre><code class="language-cpp">void Widget::process()
{
//和之前一样处理Widget
//把指向当前对象的std::shared_ptr加入processedWidgets
processedWidgets.emplace_back(shared_from_this());
}
</code></pre>
<p>从内部来说,<code>shared_from_this</code>查找当前对象控制块,然后创建一个新的<code>std::shared_ptr</code>关联这个控制块。设计的依据是当前对象已经存在一个关联的控制块。要想符合设计依据的情况,必须已经存在一个指向当前对象的<code>std::shared_ptr</code>(比如调用<code>shared_from_this</code>的成员函数外面已经存在一个<code>std::shared_ptr</code>)。如果没有<code>std::shared_ptr</code>指向当前对象(即当前对象没有关联控制块),行为是未定义的,<code>shared_from_this</code>通常抛出一个异常。</p>
<p>要想防止客户端在存在一个指向对象的<code>std::shared_ptr</code>前先调用含有<code>shared_from_this</code>的成员函数,继承自<code>std::enable_shared_from_this</code>的类通常将它们的构造函数声明为<code>private</code>,并且让客户端通过返回<code>std::shared_ptr</code>的工厂函数创建对象。以<code>Widget</code>为例,代码可以是这样:</p>
<pre><code class="language-cpp">class Widget: public std::enable_shared_from_this&lt;Widget&gt; {
public:
//完美转发参数给private构造函数的工厂函数
template&lt;typename... Ts&gt;
static std::shared_ptr&lt;Widget&gt; create(Ts&amp;&amp;... params);
void process(); //和前面一样
private:
… //构造函数
};
</code></pre>
<p>现在,你可能隐约记得我们讨论控制块的动机是想了解有关<code>std::shared_ptr</code>的成本。既然我们已经知道了怎么避免创建过多控制块,就让我们回到原来的主题。</p>
<p>控制块通常只占几个<em>word</em>大小,自定义删除器和分配器可能会让它变大一点。通常控制块的实现比你想的更复杂一些。它使用继承,甚至里面还有一个虚函数(用来确保指向的对象被正确销毁)。这意味着使用<code>std::shared_ptr</code>还会招致控制块使用虚函数带来的成本。</p>
<p>了解了动态分配控制块,任意大小的删除器和分配器,虚函数机制,原子性的引用计数修改,你对于<code>std::shared_ptr</code>的热情可能有点消退。可以理解,对每个资源管理问题来说都没有最佳的解决方案。但就它提供的功能来说,<code>std::shared_ptr</code>的开销是非常合理的。在通常情况下,使用默认删除器和默认分配器,使用<code>std::make_shared</code>创建<code>std::shared_ptr</code>产生的控制块只需三个word大小。它的分配基本上是无开销的。开销被并入了指向的对象的分配成本里。细节参见<a href="../4.SmartPointers/item21.html">Item21</a>)。对<code>std::shared_ptr</code>解引用的开销不会比原始指针高。执行需要原子引用计数修改的操作需要承担一两个原子操作开销,这些操作通常都会一一映射到机器指令上,所以即使对比非原子指令来说,原子指令开销较大,但是它们仍然只是单个指令上的。对于每个被<code>std::shared_ptr</code>指向的对象来说,控制块中的虚函数机制产生的开销通常只需要承受一次,即对象销毁的时候。</p>
<p>作为这些轻微开销的交换,你得到了动态分配的资源的生命周期自动管理的好处。大多数时候,比起手动管理,使用<code>std::shared_ptr</code>管理共享性资源都是非常合适的。如果你还在犹豫是否能承受<code>std::shared_ptr</code>带来的开销,那就再想想你是否需要共享所有权。如果独占资源可行或者<strong>可能</strong>可行,用<code>std::unique_ptr</code>是一个更好的选择。它的性能表现更接近于原始指针,并且从<code>std::unique_ptr</code>升级到<code>std::shared_ptr</code>也很容易,因为<code>std::shared_ptr</code>可以从<code>std::unique_ptr</code>上创建。</p>
<p>反之不行。当你的资源由<code>std::shared_ptr</code>管理,现在又想修改资源生命周期管理方式是没有办法的。即使引用计数为一,你也不能重新修改资源所有权,改用<code>std::unique_ptr</code>管理它。资源和指向它的<code>std::shared_ptr</code>的签订的所有权协议是“除非死亡否则永不分开”。不能分离,不能废除,没有特许。</p>
<p><code>std::shared_ptr</code>不能处理的另一个东西是数组。和<code>std::unique_ptr</code>不同的是,<code>std::shared_ptr</code>的API设计之初就是针对单个对象的没有办法<code>std::shared_ptr&lt;T[]&gt;</code>。一次又一次,“聪明”的程序员踌躇于是否该使用<code>std::shared_ptr&lt;T&gt;</code>指向数组,然后传入自定义删除器来删除数组(即<code>delete []</code>)。这可以通过编译,但是是一个糟糕的主意。一方面,<code>std::shared_ptr</code>没有提供<code>operator[]</code>,所以数组索引操作需要借助怪异的指针算术。另一方面,<code>std::shared_ptr</code>支持转换为指向基类的指针,这对于单个对象来说有效,但是当用于数组类型时相当于在类型系统上开洞。(出于这个原因,<code>std::unique_ptr&lt;T[]&gt;</code> API禁止这种转换。更重要的是C++11已经提供了很多内置数组的候选方案比如<code>std::array</code><code>std::vector</code><code>std::string</code>)。声明一个指向傻瓜数组的智能指针(译注:也是”聪明的指针“之意)几乎总是表示着糟糕的设计。</p>
<p><strong>请记住:</strong></p>
<ul>
<li><code>std::shared_ptr</code>为有共享所有权的任意资源提供一种自动垃圾回收的便捷方式。</li>
<li>较之于<code>std::unique_ptr</code><code>std::shared_ptr</code>对象通常大两倍,控制块会产生开销,需要原子性的引用计数修改操作。</li>
<li>默认资源销毁是通过<code>delete</code>,但是也支持自定义删除器。删除器的类型是什么对于<code>std::shared_ptr</code>的类型没有影响。</li>
<li>避免从原始指针变量上创建<code>std::shared_ptr</code></li>
</ul>
</main>
<nav class="nav-wrapper" aria-label="Page navigation">
<!-- Mobile navigation buttons -->
<a rel="prev" href="../4.SmartPointers/item18.html" class="mobile-nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
<i class="fa fa-angle-left"></i>
</a>
<a rel="next" href="../4.SmartPointers/item20.html" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
<i class="fa fa-angle-right"></i>
</a>
<div style="clear: both"></div>
</nav>
</div>
</div>
<nav class="nav-wide-wrapper" aria-label="Page navigation">
<a rel="prev" href="../4.SmartPointers/item18.html" class="nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
<i class="fa fa-angle-left"></i>
</a>
<a rel="next" href="../4.SmartPointers/item20.html" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
<i class="fa fa-angle-right"></i>
</a>
</nav>
</div>
<script type="text/javascript">
window.playground_copyable = true;
</script>
<script src="../elasticlunr.min.js" type="text/javascript" charset="utf-8"></script>
<script src="../mark.min.js" type="text/javascript" charset="utf-8"></script>
<script src="../searcher.js" type="text/javascript" charset="utf-8"></script>
<script src="../clipboard.min.js" type="text/javascript" charset="utf-8"></script>
<script src="../highlight.js" type="text/javascript" charset="utf-8"></script>
<script src="../book.js" type="text/javascript" charset="utf-8"></script>
<!-- Custom JS scripts -->
</body>
</html>