現代C++ 如何使用 Lambda 使代碼更具表現力、更容易理解?

使用 Lambda 使代碼更具表現力

  • 一、Lambda VS. 仿函數
  • 二、總結

一、Lambda VS. 仿函數

Lambda 是 C++11 中最引人注目的語言特性之一。它是一個強大的工具,但必須正確使用才能使代碼更具表現力,而不是更難理解。

首先,要明確的是,Lambda 并沒有為語言添加新的功能。任何可以用 Lambda 完成的事情,都可以用仿函數(Functor)來完成,雖然仿函數的語法更繁瑣,需要更多的類型聲明。

例如,比較檢查一個整數集合中所有元素是否都在兩個整數 a 和 b 之間的兩種方式:

  • 仿函數。
  • Lambda 表達式。

仿函數版本:

class IsBetween
{
public:IsBetween(int a, int b) : a_(a), b_(b) {}bool operator()(int x) { return a_ <= x && x <= b_; }
private:int a_;int b_;
};bool allBetweenAandB = std::all_of(numbers.begin(), numbers.end(), IsBetween(a, b));

Lambda 版本:

bool allBetweenAandB = std::all_of(numbers.begin(), numbers.end(),[a,b](int x) { return a <= x && x <= b; });

很明顯,Lambda 版本更簡潔,更易于編寫,這可能是 Lambda 在 C++ 中備受關注的原因。

對于像檢查一個數字是否在兩個邊界之間這樣簡單的操作,許多人可能會同意 Lambda 是更好的選擇。但也并非所有情況下都是如此。

除了編寫和簡潔性之外,在前面的例子中,Lambda 和仿函數之間的兩個主要區別是:

  • Lambda 沒有名字。
  • Lambda 不隱藏其代碼,而是直接在調用點展示。

但是,通過調用具有有意義名稱的函數將代碼從調用點移出,是管理抽象級別的一種基本技巧。但是,上面的例子是可以接受的,因為這兩個表達式:

IsBetween(a, b)

[a,b](int x) { return a <= x && x <= b; }

讀起來很相似。它們的抽象級別是一致的。

但是,當代碼變得更加復雜時,結果就會大不相同,以下例子將說明這一點。

一個表示盒子的類的例子,它可以根據尺寸和材質(金屬、塑料、木材等)進行構建,并提供對盒子特性的訪問:

class Box
{
public:Box(double length, double width, double height, Material material);double getVolume() const;double getSidesSurface() const;Material getMaterial() const;
private:double length_;double width_;double height_;Material material_;
};

有一個這樣的盒子集合:

std::vector<Box> boxes = ....

想要選擇能夠安全地容納某種產品(水、油、果汁等)的盒子。

通過一些物理推理,可以近似地將產品對盒子四個側面的壓力視為產品的重量,它分布在這些側面的表面上。如果材料能夠承受施加的壓力,則盒子足夠堅固。

假設材料可以承受的最大壓力為:

class Material
{
public:double getMaxPressure() const;....
};

產品提供了它的密度,以便計算它的重量:

class Product
{
public:double getDensity() const;....
};

現在,要選擇能夠安全地容納產品 product 的盒子,可以使用 STL 和 Lambda 編寫以下代碼:

std::vector<Box> goodBoxes;
std::copy_if(boxes.begin(), boxes.end(), std::back_inserter(goodBoxes),[product](const Box& box){const double volume = box.getVolume();const double weight = volume * product.getDensity();const double sidesSurface = box.getSidesSurface();const double pressure = weight / sidesSurface;const double maxPressure = box.getMaterial().getMaxPressure();return pressure <= maxPressure;});

以下是等效的仿函數定義:

class Resists
{
public:explicit Resists(const Product& product) : product_(product) {}bool operator()(const Box& box){const double volume = box.getVolume();const double weight = volume * product_.getDensity();const double sidesSurface = box.getSidesSurface();const double pressure = weight / sidesSurface;const double maxPressure = box.getMaterial().getMaxPressure();return pressure <= maxPressure;}
private:Product product_;
};

在主代碼中:

std::vector<Box> goodBoxes;
std::copy_if(boxes.begin(), boxes.end(), std::back_inserter(goodBoxes), Resists(product));

盡管仿函數仍然需要更多的類型聲明,但使用仿函數的算法代碼行看起來比使用 Lambda 更清晰。不幸的是,對于 Lambda 版本來說,這一行代碼更重要,因為它是主要代碼。

在這里,Lambda 的問題在于它展示了如何進行盒子檢查,而不是簡單地說檢查已經完成,因此它的抽象級別太低了。在該示例中,它會影響代碼的可讀性,因為它迫使讀者深入 Lambda 的主體以弄清楚它做了什么,而不是簡單地說明它做了什么。

在這里,有必要將代碼從調用點隱藏,并為它賦予一個有意義的名稱。仿函數在這方面做得更好。

但這是否意味著不應該在任何非平凡的情況下使用 Lambda?當然不是。

Lambda 被設計得比仿函數更輕便、更方便,同時仍然保持抽象級別有序。這里的技巧是通過使用中間函數將 Lambda 的代碼隱藏在一個有意義的名稱后面。以下是 C++14 中實現此目的的方法:

auto resists(const Product& product)
{return [product](const Box& box){const double volume = box.getVolume();const double weight = volume * product.getDensity();const double sidesSurface = box.getSidesSurface();const double pressure = weight / sidesSurface;const double maxPressure = box.getMaterial().getMaxPressure();return pressure <= maxPressure;};
}

在這里,Lambda 被封裝在一個函數中,該函數只是創建它并返回它。這個函數的作用是將 Lambda 隱藏在一個有意義的名稱后面。

以下是主代碼,它從實現負擔中解脫出來:

std::vector<Box> goodBoxes;
std::copy_if(boxes.begin(), boxes.end(), std::back_inserter(goodBoxes), resists(product));

現在,為了使代碼更具表現力,在本文的其余部分使用范圍(Range)而不是 STL 迭代器:

auto goodBoxes = boxes | ranges::view::filter(resists(product));

當調用算法周圍有其他代碼時,隱藏實現的必要性變得更加重要。為了說明這一點,添加一個要求,即盒子必須從用逗號分隔的文本測量描述(例如,“16,12.2,5”)和所有盒子的唯一材料進行初始化。

如果直接調用即時 Lambda,結果將如下所示:

auto goodBoxes = boxesDescriptions| ranges::view::transform([material](std::string const& textualDescription){std::vector<std::string> strSizes;boost::split(strSizes, textualDescription, [](char c){ return c == ','; });const auto sizes = strSizes | ranges::view::transform([](const std::string& s) {return std::stod(s); });if (sizes.size() != 3) throw InvalidBoxDescription(textualDescription);return Box(sizes[0], sizes[1], sizes[2], material);})| ranges::view::filter([product](Box const& box){const double volume = box.getVolume();const double weight = volume * product.getDensity();const double sidesSurface = box.getSidesSurface();const double pressure = weight / sidesSurface;const double maxPressure = box.getMaterial().getMaxPressure();return pressure <= maxPressure;});

這變得非常難以閱讀。但是,通過使用中間函數來封裝 Lambda,代碼將變成:

auto goodBoxes = textualDescriptions | ranges::view::transform(createBox(material))| ranges::view::filter(resists(product));

這才是希望代碼呈現的樣子。

請注意,這種技術在 C++14 中有效,但在 C++11 中略有不同。

Lambda 的類型沒有在標準中指定,而是由編譯器的實現決定。這里,auto 作為返回值類型允許編譯器將函數的返回值類型寫為 Lambda 的類型。但在 C++11 中,不能這樣做,因此需要指定一些返回值類型。Lambda 可以隱式轉換為具有正確類型參數的 std::function,并且可以在 STL 和范圍算法中使用。請注意,std::function 會帶來與堆分配和虛擬調用間接相關的額外成本。

在 C++11 中,resists 函數的建議代碼將是:

std::function<bool(const Box&)> resists(const Product& product)
{return [product](const Box& box){const double volume = box.getVolume();const double weight = volume * product.getDensity();const double sidesSurface = box.getSidesSurface();const double pressure = weight / sidesSurface;const double maxPressure = box.getMaterial().getMaxPressure();return pressure <= maxPressure;};
}

請注意,在 C++11 和 C++14 的實現中,resists 函數返回的 Lambda 可能不會被復制,因為返回值優化可能會優化掉它。還要注意,返回 auto 的函數必須在其調用點可見。因此,這種技術最適合在與調用代碼相同的文件中定義的 Lambda。

二、總結

  • 對于對抽象級別透明的函數,請使用在調用點定義的匿名 Lambda。
  • 否則,將 Lambda 封裝在一個中間函數中。

在這里插入圖片描述

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/14933.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/14933.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/14933.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

向npm發布自己寫的vue組件,使用vite創建項目

向npm發布自己寫的vue組件&#xff0c;使用vite創建項目 創建項目 pnpm create vite輸入項目名稱 由于我的組件是基于 ant-design-vue和vue的&#xff0c;需要解析.vue文件&#xff0c;我又安裝了下面4個。 然后執行 pnpm i安裝依賴 vite.config.ts import { defineC…

防范TOCTOU競態條件攻擊

防范TOCTOU競態條件攻擊 在軟件開發過程中&#xff0c;我們常常會遇到需要在使用資源之前檢查其狀態的情況。然而&#xff0c;如果資源的狀態在檢查和使用之間發生了變化&#xff0c;那么檢查的結果可能會失效&#xff0c;導致軟件在資源處于非正常狀態時執行無效操作。這種時…

[datawhale202405]從零手搓大模型實戰:TinyAgent

結論速遞 TinyAgent項目實現了一個簡單的Agent智能體&#xff0c;主要是實現了ReAct策略&#xff08;推理調用工具的能力&#xff09;&#xff0c;及封裝了一個Tool。 項目實現有一定的疏漏。為了正確運行代碼&#xff0c;本次對代碼Agent部分進行了簡單修改&#xff08;完善…

windows安裝rocketmq

1.下載連接 https://rocketmq.apache.org/download/ 2.解壓到D盤下&#xff08;其他位置也可以&#xff09; 3.配置環境變量 需要有jdk環境 新建ROCKETMQ_HOME&#xff0c;剛剛解壓的位置 編輯Path&#xff0c;新增%ROCKETMQ_HOME%\bin 4.啟動mqnameserver 進入安裝bin目錄下…

ERC314協議

314協議功能詳解 這兩天花時間研究了一下314協議&#xff0c;總體感覺還不錯&#xff0c;有創新。 功能亮點 314協議作為一種創新的代幣標準&#xff0c;致力于降低用戶交易成本與簡化授權流程&#xff0c;通過“轉賬即交易”模式革新傳統Swap體驗。此協議簡化了買賣代幣的過程…

什么是react

React 是一個用于構建用戶界面的 JavaScript 庫&#xff0c;由 Facebook&#xff08;現在的 Meta&#xff09;開發和維護。它首次發布于2013年&#xff0c;并迅速成為最受歡迎的前端庫之一。React 的主要目標是提供一種高效、靈活的方式來構建用戶界面&#xff0c;特別是在大型…

gc和gccgo編譯器

Go 語言有兩個主要的編譯器&#xff0c;分別是 Go 編譯器&#xff08;通常簡稱為 gc&#xff09;和 GCCGO。它們之間有一些重要的異同點&#xff1a; gc 編譯器&#xff1a; gc 是 Go 語言的官方編譯器&#xff0c;由 Go 語言的開發團隊維護。它是 Go 語言最常用的編譯器&#…

PHP代碼審計前期準備

1 php代碼審計的意義 1.1 什么是代碼審計 就是獲取目標的代碼&#xff0c;這個目標可以是一個網站&#xff0c;也可以是一個手機app 1.2 黑盒測試與白盒測試的區別 在代碼審計中黑盒和白盒的主要區別就在于是否可以拿到源代碼&#xff0c;黑盒是拿不到源代碼的&#xff0c;…

交叉編譯——

什么是交叉編譯 交叉編譯 是在一個平臺上生成臨海一個平臺可執行代碼. eg.在windows上面編寫C51代碼&#xff0c;并編譯生成可執行代碼。如xx.hex 我們在Ubuntu上編寫樹莓派的代碼&#xff0c;并編譯成可執行代碼。a.out. 是在樹莓派上運行&#xff0c;不在Ubuntu Linux上面運…

便攜式iv測試儀特點

TH-PV30便攜式IV測試儀是一種用于測量半導體器件電學特性的設備&#xff0c;它具有體積小、重量輕、便于攜帶等特點&#xff0c;廣泛應用于半導體行業、科研實驗室以及教育領域。 該測試儀的工作原理基于四探針法&#xff0c;通過在半導體器件表面放置四個金屬探針&#xff0c…

【vs2022】安裝copilot和reshaper

直接安裝新版vs 17.10 自帶集成的copilot支持安裝resharper 可以跳過市場里的reshper安裝好后依然可以直接使用vs。 resharper 2024.1.2 市場里還是i老版本&#xff1a; copilot 不兼容,這個是之前市場安裝的版本 官方建議用vs intall 安裝 安裝 GitHub Copilot GitHub.Co…

詳解http協議

什么是HTTP協議 定義 Http協議即超文本傳送協議 (HTTP-Hypertext transfer protocol) 。 它定義了瀏覽器&#xff08;即萬維網客戶進程&#xff09;怎樣向萬維網服務器請求萬維網文檔&#xff0c;以及服務器怎樣把文檔傳送給瀏覽器。從層次的角度看&#xff0c;HTTP是面向&am…

第四十一天 | 62.不同路徑 63.不同路徑|| 343.整數拆分 96.不同的二叉搜索樹

題目&#xff1a;62.不同路徑 1.二維dp數組dp[i][j]含義&#xff1a;到達&#xff08;i&#xff0c;j&#xff09;位置有dp[i][j]種方法。 2.動態轉移方程&#xff1a;dp[i][j] dp[i - 1][j] dp[i][j - 1] 3.初始化&#xff1a;dp[0][j] 1, dp[i][0] 1 &#xff08;第一…

Vue3設置緩存:storage.ts

在vue文件使用&#xff1a; import { Local,Session } from //utils/storage; // Local if (!Local.get(字段名)) Local.set(字段名, 字段的值);// Session Session.getToken()storage.ts文件&#xff1a; import Cookies from js-cookie;/*** window.localStorage 瀏覽器永…

uniapp 安卓 Pc端真機瀏覽器調試

下載插件:真機模擬瀏覽器 1. 安裝, 每次啟用時使用usb 線連接電腦, 并且打開手機或者POS (調試設備)開發者模式, 比如我的是pos 機 則在系統設置中找到版本號,點擊多次就會觸發開發者模式 2.打開真機模擬軟件,打開后會打開一個瀏覽器,如果想要模擬google的瀏覽器則 在瀏覽器地…

精準鍵位提示,鍵盤盲打輕松入門

在說明精準鍵位提示之前&#xff0c;我們先來看一張圖&#xff1a; 這是一張標準的基準鍵位圖&#xff0c;也就是打字時我們雙手的8個手指放在基準鍵位上&#xff0c;在打不同的字母時&#xff0c;我們的手指以基準鍵位為中心&#xff0c;或上、或下、或左、或右&#xff0c;在…

202109青少年軟件編程(Python)等級考試試卷(四級)

第 1 題 【單選題】 執行如下 Python 代碼后, 結果是?( ) def inverse(s,n=0): while s:n = n * 10 + s % 10s = s // 10return nprint

《拯救大學生課設不掛科第二期之Windows11下安裝VC6.0(VC++6.0)與跑通Hello,World!程序教程》【官方筆記】

背景與目標人群&#xff1a; 大學第一次學C語言的時候&#xff0c;大部分老師會選擇VC6這個編輯器。 但由于很多人是新手&#xff0c;第一次上大學學C語言。 老師要求VC6.0&#xff08;VC6.0&#xff09;寫C語言跑程序可能很多人還是第一次接觸電腦。 需要安裝VC6這個編輯器…

Docker常用軟件安裝

文章目錄 1.安裝Tomcat1.docker hub查找鏡像并復制拉取鏡像命令2.拉取鏡像到本地1.執行官網命令2.查看是否拉取成功 3.啟動tomcat4.退出和重啟1.由于是以交互方式啟動的&#xff0c;所以不方便&#xff0c;直接ctrl c退出2.查看當前的容器3.使用docker start 命令啟動容器&…

【cocos creator 】生成六邊形地圖

想要生成一個六邊形組成的地圖 完整代碼示例 以下是完整的代碼示例&#xff0c;包含了注釋來解釋每一步&#xff1a; cc.Class({extends: cc.Component,properties: {hexPrefab: {default: null,type: cc.Prefab},mapWidth: 10, // 網格的寬度&#xff08;六邊形的數量&am…