Fluent C++:使用lambda讓代碼更有表現(xiàn)力

原文

Lambda可以說(shuō)是C ++ 11語(yǔ)言中最著名的功能之一。 它是一種有用的工具,但必須確保正確使用它們,以使代碼更具表現(xiàn)力,而不是晦澀難懂。

image.png

首先,讓我們明確一點(diǎn),lambda不會(huì)為語(yǔ)言添加功能。 使用lambda可以執(zhí)行的所有操作都可以使用函子來(lái)完成,盡管語(yǔ)法更繁重且要敲更多代碼。

例如,這是檢查一個(gè)int集合的所有元素是否包含在另外兩個(gè)int a和b之間的比較示例:

函子版本:

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版本更簡(jiǎn)潔,更容易鍵入,這可能可以解釋為什么大肆宣傳C ++中加入了lambda。

對(duì)于檢查數(shù)字是否在兩個(gè)邊界之間這樣的簡(jiǎn)單處理,我想許多人都同意應(yīng)優(yōu)先選擇lambda。 但我想證明并非所有情況都如此。

除了輸入少和簡(jiǎn)潔之外,上一個(gè)示例中的lambda和函子之間的兩個(gè)主要區(qū)別是:

  • lambda沒(méi)有名字,
  • Lambda不會(huì)在調(diào)用處隱藏其代碼。

但是,通過(guò)調(diào)用具有有意義名稱的函數(shù)將代碼從調(diào)用處刪除是管理抽象級(jí)別的基本技術(shù)。 但是上面的示例也還可以,因?yàn)檫@兩個(gè)表達(dá)式:

IsBetween(a, b)

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

讀起來(lái)差不多。 它們處于相同的抽象級(jí)別(盡管可以爭(zhēng)辯說(shuō)第一個(gè)表達(dá)式包含較少的噪音)。

但是,當(dāng)代碼更加詳細(xì)時(shí),結(jié)果可能會(huì)非常不同,如以下示例所示。

讓我們考慮一個(gè)代表盒子的類的示例,該類可以根據(jù)其尺寸以及其材料(金屬,塑料,木材等)構(gòu)造而成,并可以訪問(wèn)該盒子的特征:

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_;
};

我們有一個(gè)盒子的容器:

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

我們希望選擇足夠堅(jiān)固的盒子來(lái)容納某種產(chǎn)品(水,油,果汁等)。

通過(guò)一點(diǎn)物理推理,我們將產(chǎn)品施加在盒子四個(gè)側(cè)面上的強(qiáng)度近似為產(chǎn)品的重量。 如果材料可以承受施加在其上的壓力,則該盒子足夠堅(jiān)固。

假設(shè)該材料可以提供可以承受的最大壓力:

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

該產(chǎn)品提供其密度以計(jì)算其重量:

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

現(xiàn)在要選擇足以容納產(chǎn)品的盒子,我們可以使用帶有l(wèi)ambda的STL編寫以下代碼:

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相比,使用算法庫(kù)的這一行應(yīng)該清晰得多。不幸的是,對(duì)于lambdas版本,這一行更為重要(但不清晰),因?yàn)樗侵饕a,你和其他開發(fā)人員需要讀這些代碼來(lái)了解它做了什么。

在這里,lambda的問(wèn)題在于顯示如何執(zhí)行檢查,而不是僅僅說(shuō)執(zhí)行了檢查,因此它的抽象級(jí)別太低了。在此示例中,它損害了代碼的可讀性,因?yàn)樗仁棺x者深入研究lambda的主體以弄清楚它的作用,而不僅僅是聲明它的作用。

在這里,有必要從調(diào)用處隱藏代碼,并在其上貼上有意義的名稱。函子在這方面做得更好。

但是是說(shuō)我們?cè)谌魏吻闆r下都不應(yīng)該使用lambda嗎?當(dāng)然不會(huì)。

與函子相比,Lambda變得更輕便,更方便,你實(shí)際上可以從中受益,同時(shí)仍然保持抽象級(jí)別的井井有條。這里的技巧是通過(guò)使用中介函數(shù)將lambda的代碼隱藏在有意義的名稱后面。這是在C ++ 14中執(zhí)行的方法:

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封裝在一個(gè)函數(shù)中,該函數(shù)只是創(chuàng)建并返回它。 此功能的作用是將lambda隱藏在有意義的名稱后面。

這是主要代碼,減輕了實(shí)現(xiàn)負(fù)擔(dān):

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

現(xiàn)在,在本文的其余部分中,我們使用range而不是STL迭代器來(lái)獲得更具表現(xiàn)力的代碼:

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

當(dāng)算法調(diào)用的周圍還有其他代碼時(shí),隱藏實(shí)現(xiàn)的必要性變得更加重要。為了說(shuō)明這一點(diǎn),讓我們?cè)黾右粋€(gè)要求,即這些盒子必須用以逗號(hào)分隔的測(cè)量文字說(shuō)明(例如“ 16,12.2,5”)和唯一的材料來(lái)初始化。

如果我們直接調(diào)用即時(shí)lambda,結(jié)果將如下所示:

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;
    });

這真的很難閱讀。

但是通過(guò)使用中介函數(shù)封裝lambda,代碼將變?yōu)椋?/p>

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

在我看來(lái),這就是你希望代碼看起來(lái)像的樣子。

請(qǐng)注意,此技術(shù)在C ++ 14中有效,但在需要稍作更改的C ++ 11中無(wú)效。

lambda的類型不是由標(biāo)準(zhǔn)指定的,而是由編譯器實(shí)現(xiàn)的。 在這里,將auto作為返回類型可以使編譯器將函數(shù)的返回類型編寫為lambda類型。 盡管在C ++ 11中無(wú)法做到這一點(diǎn),所以您需要指定一些返回類型。 Lambda可通過(guò)正確的類型參數(shù)隱式轉(zhuǎn)換為std :: function,并且可以在STL和range算法中使用。 請(qǐng)注意,正如Antoine在評(píng)論部分中指出的那樣,std :: function會(huì)產(chǎn)生與堆分配和虛擬調(diào)用間接相關(guān)的額外費(fèi)用。

在C ++ 11中,resists函數(shù)的建議代碼為:

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;
    };
}

請(qǐng)注意,在C ++ 11和C ++ 14的實(shí)現(xiàn)中,都可能沒(méi)有在resists函數(shù)返回時(shí)對(duì)lambda做任何拷貝,因?yàn)榉祷刂祪?yōu)化可能會(huì)將其優(yōu)化掉。 還請(qǐng)注意,返回auto的函數(shù)必須在其調(diào)用位置可見(jiàn)其定義。 因此,此技術(shù)最適合與調(diào)用代碼在同一文件中定義的lambda。

結(jié)論

  • 使用在其調(diào)用處定義的匿名lambda來(lái)實(shí)現(xiàn)對(duì)于抽象級(jí)別透明的函數(shù)

  • 否則,將您的lambda封裝在中介函數(shù)中。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容