EffectiveModernCppChinese/6.LambdaExpressions/item31.html
2022-11-18 14:12:20 +00:00

418 lines
35 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 31:避免使用默认捕获模式 - 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">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" class="active">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>
<h1 id="第6章-lambda表达式"><a class="header" href="#第6章-lambda表达式">第6章 <em>lambda</em>表达式</a></h1>
<p><strong>CHAPTER 6 Lambda Expressions</strong></p>
<p><em>lambda</em>表达式是C++编程中的游戏规则改变者。这有点令人惊讶,因为它没有给语言带来新的表达能力。<em>lambda</em>可以做的所有事情都可以通过其他方式完成。但是<em>lambda</em>是创建函数对象相当便捷的一种方法对于日常的C++开发影响是巨大的。没有<em>lambda</em>STL中的“<code>_if</code>”算法(比如,<code>std::find_if</code><code>std::remove_if</code><code>std::count_if</code>等)通常需要繁琐的谓词,但是当有<em>lambda</em>可用时,这些算法使用起来就变得相当方便。用比较函数(比如,<code>std::sort</code><code>std::nth_element</code><code>std::lower_bound</code>来自定义算法也是同样方便的。在STL外<em>lambda</em>可以快速创建<code>std::unique_ptr</code><code>std::shared_ptr</code>的自定义删除器(见<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item18.md">Item18</a><a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item19.md">19</a>并且使线程API中条件变量的谓词指定变得同样简单参见<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/7.TheConcurrencyAPI/item39.md">Item39</a>)。除了标准库,<em>lambda</em>有利于即时的回调函数,接口适配函数和特定上下文中的一次性函数。<em>lambda</em>确实使C++成为更令人愉快的编程语言。</p>
<p><em>lambda</em>相关的词汇可能会令人疑惑,这里做一下简单的回顾:</p>
<ul>
<li>
<p><strong><em>lambda</em>表达式</strong><em>lambda expression</em>)就是一个表达式。下面是部分源代码。在</p>
<pre><code class="language-cpp">std::find_if(container.begin(), container.end(),
[](int val){ return 0 &lt; val &amp;&amp; val &lt; 10; }); //译者注:本行高亮
</code></pre>
<p>中,代码的高亮部分就是<em>lambda</em></p>
</li>
<li>
<p><strong>闭包</strong><em>enclosure</em>)是<em>lambda</em>创建的运行时对象。依赖捕获模式,闭包持有被捕获数据的副本或者引用。在上面的<code>std::find_if</code>调用中,闭包是作为第三个实参在运行时传递给<code>std::find_if</code>的对象。</p>
</li>
<li>
<p><strong>闭包类</strong><em>closure class</em>)是从中实例化闭包的类。每个<em>lambda</em>都会使编译器生成唯一的闭包类。<em>lambda</em>中的语句成为其闭包类的成员函数中的可执行指令。</p>
</li>
</ul>
<p><em>lambda</em>通常被用来创建闭包,该闭包仅用作函数的实参。上面对<code>std::find_if</code>的调用就是这种情况。然而,闭包通常可以拷贝,所以可能有多个闭包对应于一个<em>lambda</em>。比如下面的代码:</p>
<pre><code class="language-cpp">{
int x; //x是局部对象
auto c1 = //c1是lambda产生的闭包的副本
[x](int y) { return x * y &gt; 55; };
auto c2 = c1; //c2是c1的拷贝
auto c3 = c2; //c3是c2的拷贝
}
</code></pre>
<p><code>c1</code><code>c2</code><code>c3</code>都是<em>lambda</em>产生的闭包的副本。</p>
<p>非正式的讲,模糊<em>lambda</em>闭包和闭包类之间的界限是可以接受的。但是在随后的Item中区分什么存在于编译期<em>lambdas</em> 和闭包类),什么存在于运行时(闭包)以及它们之间的相互关系是重要的。</p>
<h2 id="条款三十一避免使用默认捕获模式"><a class="header" href="#条款三十一避免使用默认捕获模式">条款三十一:避免使用默认捕获模式</a></h2>
<p><strong>Item 31: Avoid default capture modes</strong></p>
<p>C++11中有两种默认的捕获模式按引用捕获和按值捕获。但默认按引用捕获模式可能会带来悬空引用的问题而默认按值捕获模式可能会诱骗你让你以为能解决悬空引用的问题实际上并没有还会让你以为你的闭包是独立的事实上也不是独立的</p>
<p>这就是本条款的一个总结。如果你偏向技术,渴望了解更多内容,就让我们从按引用捕获的危害谈起吧。</p>
<p>按引用捕获会导致闭包中包含了对某个局部变量或者形参的引用,变量或形参只在定义<em>lambda</em>的作用域中可用。如果该<em>lambda</em>创建的闭包生命周期超过了局部变量或者形参的生命周期那么闭包中的引用将会变成悬空引用。举个例子假如我们有元素是过滤函数filtering function的一个容器该函数接受一个<code>int</code>,并返回一个<code>bool</code>,该<code>bool</code>的结果表示传入的值是否满足过滤条件:</p>
<pre><code class="language-c++">using FilterContainer = //“using”参见条款9
std::vector&lt;std::function&lt;bool(int)&gt;&gt;; //std::function参见条款2
FilterContainer filters; //过滤函数
</code></pre>
<p>我们可以添加一个过滤器用来过滤掉5的倍数</p>
<pre><code class="language-c++">filters.emplace_back( //emplace_back的信息见条款42
[](int value) { return value % 5 == 0; }
);
</code></pre>
<p>然而我们可能需要的是能够在运行期计算除数divisor即不能将5硬编码到<em>lambda</em>中。因此添加的过滤器逻辑将会是如下这样:</p>
<pre><code class="language-c++">void addDivisorFilter()
{
auto calc1 = computeSomeValue1();
auto calc2 = computeSomeValue2();
auto divisor = computeDivisor(calc1, calc2);
filters.emplace_back( //危险对divisor的引用
[&amp;](int value) { return value % divisor == 0; } //将会悬空!
);
}
</code></pre>
<p>这个代码实现是一个定时炸弹。<em>lambda</em>对局部变量<code>divisor</code>进行了引用,但该变量的生命周期会在<code>addDivisorFilter</code>返回时结束,刚好就是在语句<code>filters.emplace_back</code>返回之后。因此添加到<code>filters</code>的函数添加完,该函数就死亡了。使用这个过滤器(译者注:就是那个添加进<code>filters</code>的函数)会导致未定义行为,这是由它被创建那一刻起就决定了的。</p>
<p>现在,同样的问题也会出现在<code>divisor</code>的显式按引用捕获。</p>
<pre><code class="language-c++">filters.emplace_back(
[&amp;divisor](int value) //危险对divisor的引用将会悬空
{ return value % divisor == 0; }
);
</code></pre>
<p>但通过显式的捕获,能更容易看到<em>lambda</em>的可行性依赖于变量<code>divisor</code>的生命周期。另外写下“divisor”这个名字能够提醒我们要注意确保<code>divisor</code>的生命周期至少跟<em>lambda</em>闭包一样长。比起“<code>[&amp;]</code>”传达的意思,显式捕获能让人更容易想起“确保没有悬空变量”。</p>
<p>如果你知道一个闭包将会被马上使用例如被传入到一个STL算法中并且不会被拷贝那么在它的<em>lambda</em>被创建的环境中,将不会有持有的引用比局部变量和形参活得长的风险。在这种情况下,你可能会争论说,没有悬空引用的危险,就不需要避免使用默认的引用捕获模式。例如,我们的过滤<em>lambda</em>只会用做C++11中<code>std::all_of</code>的一个实参,返回满足条件的所有元素:</p>
<pre><code class="language-c++">template&lt;typename C&gt;
void workWithContainer(const C&amp; container)
{
auto calc1 = computeSomeValue1(); //同上
auto calc2 = computeSomeValue2(); //同上
auto divisor = computeDivisor(calc1, calc2); //同上
using ContElemT = typename C::value_type; //容器内元素的类型
using std::begin; //为了泛型见条款13
using std::end;
if (std::all_of( //如果容器内所有值都为
begin(container), end(container), //除数的倍数
[&amp;](const ContElemT&amp; value)
{ return value % divisor == 0; })
) {
… //它们...
} else {
… //至少有一个不是的话...
}
}
</code></pre>
<p>的确如此,这是安全的做法,但这种安全是不确定的。如果发现<em>lambda</em>在其它上下文中很有用(例如作为一个函数被添加在<code>filters</code>容器中),然后拷贝粘贴到一个<code>divisor</code>变量已经死亡,但闭包生命周期还没结束的上下文中,你又回到了悬空的使用上了。同时,在该捕获语句中,也没有特别提醒了你注意分析<code>divisor</code>的生命周期。</p>
<p>从长期来看,显式列出<em>lambda</em>依赖的局部变量和形参,是更加符合软件工程规范的做法。</p>
<p>额外提一下C++14支持了在<em>lambda</em>中使用<code>auto</code>来声明变量上面的代码在C++14中可以进一步简化<code>ContElemT</code>的别名可以去掉,<code>if</code>条件可以修改为:</p>
<pre><code class="language-c++">if (std::all_of(begin(container), end(container),
[&amp;](const auto&amp; value) // C++14
{ return value % divisor == 0; }))
</code></pre>
<p>一个解决问题的方法是,<code>divisor</code>默认按值捕获进去,也就是说可以按照以下方式来添加<em>lambda</em><code>filters</code></p>
<pre><code class="language-c++">filters.emplace_back( //现在divisor不会悬空了
[=](int value) { return value % divisor == 0; }
);
</code></pre>
<p>这足以满足本实例的要求,但在通常情况下,按值捕获并不能完全解决悬空引用的问题。这里的问题是如果你按值捕获的是一个指针,你将该指针拷贝到<em>lambda</em>对应的闭包里,但这样并不能避免<em>lambda</em><code>delete</code>这个指针的行为,从而导致你的副本指针变成悬空指针。</p>
<p>也许你要抗议说:“这不可能发生。看过了<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item18.md">第4章</a>我对智能指针的使用非常热衷。只有那些失败的C++98的程序员才会用裸指针和<code>delete</code>语句。”这也许是正确的,但却是不相关的,因为事实上你的确会使用裸指针,也的确存在被你<code>delete</code>的可能性。只不过在现代的C++编程风格中,不容易在源代码中显露出来而已。</p>
<p>假设在一个<code>Widget</code>类,可以实现向过滤器的容器添加条目:</p>
<pre><code class="language-c++">class Widget {
public:
… //构造函数等
void addFilter() const; //向filters添加条目
private:
int divisor; //在Widget的过滤器使用
};
</code></pre>
<p>这是<code>Widget::addFilter</code>的定义:</p>
<pre><code class="language-c++">void Widget::addFilter() const
{
filters.emplace_back(
[=](int value) { return value % divisor == 0; }
);
}
</code></pre>
<p>这个做法看起来是安全的代码。<em>lambda</em>依赖于<code>divisor</code>,但默认的按值捕获确保<code>divisor</code>被拷贝进了<em>lambda</em>对应的所有闭包中,对吗?</p>
<p>错误,完全错误。</p>
<p>捕获只能应用于<em>lambda</em>被创建时所在作用域里的non-<code>static</code>局部变量(包括形参)。在<code>Widget::addFilter</code>的视线里,<code>divisor</code>并不是一个局部变量,而是<code>Widget</code>类的一个成员变量。它不能被捕获。而如果默认捕获模式被删除,代码就不能编译了:</p>
<pre><code class="language-c++">void Widget::addFilter() const
{
filters.emplace_back( //错误!
[](int value) { return value % divisor == 0; } //divisor不可用
);
}
</code></pre>
<p>另外,如果尝试去显式地捕获<code>divisor</code>变量(或者按引用或者按值——这不重要),也一样会编译失败,因为<code>divisor</code>不是一个局部变量或者形参。</p>
<pre><code class="language-c++">void Widget::addFilter() const
{
filters.emplace_back(
[divisor](int value) //错误没有名为divisor局部变量可捕获
{ return value % divisor == 0; }
);
}
</code></pre>
<p>所以如果默认按值捕获不能捕获<code>divisor</code>,而不用默认按值捕获代码就不能编译,这是怎么一回事呢?</p>
<p>解释就是这里隐式使用了一个原始指针:<code>this</code>。每一个non-<code>static</code>成员函数都有一个<code>this</code>指针,每次你使用一个类内的数据成员时都会使用到这个指针。例如,在任何<code>Widget</code>成员函数中,编译器会在内部将<code>divisor</code>替换成<code>this-&gt;divisor</code>。在默认按值捕获的<code>Widget::addFilter</code>版本中,</p>
<pre><code class="language-c++">void Widget::addFilter() const
{
filters.emplace_back(
[=](int value) { return value % divisor == 0; }
);
}
</code></pre>
<p>真正被捕获的是<code>Widget</code><code>this</code>指针,而不是<code>divisor</code>。编译器会将上面的代码看成以下的写法:</p>
<pre><code class="language-c++">void Widget::addFilter() const
{
auto currentObjectPtr = this;
filters.emplace_back(
[currentObjectPtr](int value)
{ return value % currentObjectPtr-&gt;divisor == 0; }
);
}
</code></pre>
<p>明白了这个就相当于明白了<em>lambda</em>闭包的生命周期与<code>Widget</code>对象的关系,闭包内含有<code>Widget</code><code>this</code>指针的拷贝。特别是考虑以下的代码,参考<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item18.md">第4章</a>的内容,只使用智能指针:</p>
<pre><code class="language-c++">using FilterContainer = //跟之前一样
std::vector&lt;std::function&lt;bool(int)&gt;&gt;;
FilterContainer filters; //跟之前一样
void doSomeWork()
{
auto pw = //创建Widgetstd::make_unique
std::make_unique&lt;Widget&gt;(); //见条款21
pw-&gt;addFilter(); //添加使用Widget::divisor的过滤器
} //销毁Widgetfilters现在持有悬空指针
</code></pre>
<p>当调用<code>doSomeWork</code>时,就会创建一个过滤器,其生命周期依赖于由<code>std::make_unique</code>产生的<code>Widget</code>对象,即一个含有指向<code>Widget</code>的指针——<code>Widget</code><code>this</code>指针——的过滤器。这个过滤器被添加到<code>filters</code>中,但当<code>doSomeWork</code>结束时,<code>Widget</code>会由管理它的<code>std::unique_ptr</code>来销毁(见<a href="https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item18.md">Item18</a>)。从这时起,<code>filter</code>会含有一个存着悬空指针的条目。</p>
<p>这个特定的问题可以通过给你想捕获的数据成员做一个局部副本,然后捕获这个副本去解决:</p>
<pre><code class="language-c++">void Widget::addFilter() const
{
auto divisorCopy = divisor; //拷贝数据成员
filters.emplace_back(
[divisorCopy](int value) //捕获副本
{ return value % divisorCopy == 0; } //使用副本
);
}
</code></pre>
<p>事实上如果采用这种方法,默认的按值捕获也是可行的。</p>
<pre><code class="language-c++">void Widget::addFilter() const
{
auto divisorCopy = divisor; //拷贝数据成员
filters.emplace_back(
[=](int value) //捕获副本
{ return value % divisorCopy == 0; } //使用副本
);
}
</code></pre>
<p>但为什么要冒险呢?当一开始你认为你捕获的是<code>divisor</code>的时候,默认捕获模式就是造成可能意外地捕获<code>this</code>的元凶。</p>
<p>在C++14中一个更好的捕获成员变量的方式时使用通用的<em>lambda</em>捕获:</p>
<pre><code class="language-c++">void Widget::addFilter() const
{
filters.emplace_back( //C++14
[divisor = divisor](int value) //拷贝divisor到闭包
{ return value % divisor == 0; } //使用这个副本
);
}
</code></pre>
<p>这种通用的<em>lambda</em>捕获并没有默认的捕获模式因此在C++14中本条款的建议——避免使用默认捕获模式——仍然是成立的。</p>
<p>使用默认的按值捕获还有另外的一个缺点,它们预示了相关的闭包是独立的并且不受外部数据变化的影响。一般来说,这是不对的。<em>lambda</em>可能会依赖局部变量和形参(它们可能被捕获),还有<strong>静态存储生命周期</strong>static storage duration的对象。这些对象定义在全局空间或者命名空间或者在类、函数、文件中声明为<code>static</code>。这些对象也能在<em>lambda</em>里使用,但它们不能被捕获。但默认按值捕获可能会因此误导你,让你以为捕获了这些变量。参考下面版本的<code>addDivisorFilter</code>函数:</p>
<pre><code class="language-c++">void addDivisorFilter()
{
static auto calc1 = computeSomeValue1(); //现在是static
static auto calc2 = computeSomeValue2(); //现在是static
static auto divisor = //现在是static
computeDivisor(calc1, calc2);
filters.emplace_back(
[=](int value) //什么也没捕获到!
{ return value % divisor == 0; } //引用上面的static
);
++divisor; //调整divisor
}
</code></pre>
<p>随意地看了这份代码的读者可能看到“<code>[=]</code>”,就会认为“好的,<em>lambda</em>拷贝了所有使用的对象,因此这是独立的”。但其实不独立。这个<em>lambda</em>没有使用任何的non-<code>static</code>局部变量,所以它没有捕获任何东西。然而<em>lambda</em>的代码引用了<code>static</code>变量<code>divisor</code>,在每次调用<code>addDivisorFilter</code>的结尾,<code>divisor</code>都会递增,通过这个函数添加到<code>filters</code>的所有<em>lambda</em>都展示新的行为(分别对应新的<code>divisor</code>值)。这个<em>lambda</em>是通过引用捕获<code>divisor</code>,这和默认的按值捕获表示的含义有着直接的矛盾。如果你一开始就避免使用默认的按值捕获模式,你就能解除代码的风险。</p>
<p><strong>请记住:</strong></p>
<ul>
<li>默认的按引用捕获可能会导致悬空引用。</li>
<li>默认的按值捕获对于悬空指针很敏感(尤其是<code>this</code>指针),并且它会误导人产生<em>lambda</em>是独立的想法。</li>
</ul>
</main>
<nav class="nav-wrapper" aria-label="Page navigation">
<!-- Mobile navigation buttons -->
<a rel="prev" href="../5.RRefMovSemPerfForw/item30.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="../6.LambdaExpressions/item32.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="../5.RRefMovSemPerfForw/item30.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="../6.LambdaExpressions/item32.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>