C++20 范围库投影特性示例

C++20 Ranges 算法有一个很好的特性叫做“投影”。简而言之,它是在进一步处理之前应用于每个范围元素的可调用对象。在本文中,我将向您展示更多有关此便捷功能的示例。

简介

根据 C++20 标准: [defns.projection]

投影(projection):算法在检查元素值之前应用的转换。

std::pair<int, std::string_view> pairs[] = {
    {2, "foo"}, {1, "bar"}, {0, "baz"}
};

std::ranges::sort(pairs, std::ranges::less{}, 
    [](auto const& p) { return p.first; });

 以上代码中,我们有一个投影,它取一对然后只提取第一个成员,这个成员用于执行排序算法。默认情况下,每个算法都使用身份投影。例如,这是 copy_if  的摘录:

constexpr copy_if_result<I, O>
    copy_if( I first, S last, O result, Pred pred, Proj proj = {} );

然后通过 std::invoke 调用这样的投影并支持以下转换:

  • lambda 或其他可调用对象
  • 指向成员函数的指针
  • 指向数据成员的指针

我在另一篇文章中详细描述了它的工作原理:C++20 范围、投影、std::invoke 和 if constexpr - C++ 故事系列

让我们看一些示例以了解此功能的实际应用。

排序

让我们从一些简单的事情开始,比如排序;我们可以从标准扩展示例:

#include <algorithm>
#include <ranges>
#include <iostream>

int main() {
    std::pair<int, std::string_view> pairs[] = {
        {2, "foo"}, {1, "bar"}, {0, "baz"}
    };

    // member access:
    std::ranges::sort(pairs, std::ranges::less{}, 
        &std::pair<int, std::string_view>::first);

    // a lambda:
    std::ranges::sort(pairs, std::ranges::less{}, 
        [](auto const& p) { return p.first; });
}

在 Compiler Explorer 上运行它。

我们也可以尝试排序结构体:

struct Box {
    std::string name;    
    double w = 0.0;
    double h = 0.0;
    double d = 0.0;

    constexpr double volume() const { return w*h*d; }
};

void print(std::string_view intro, const std::vector<Box>& container) {
    std::cout << intro << '\n';
    for (const auto &elem : container)
        std::cout << std::format("{}, volume {}\n", elem.name, elem.volume());
}

int main() {
    const std::vector<Box> container {
        {"large cubic", 10, 10, 10}, {"small cubic", 3, 3, 3},
        {"large long", 10, 2, 2}, {"small", 3, 2, 2}
    };
    
    print("initial", container);

    // the ranges version:
    auto copy = container;   
    std::ranges::sort(copy, {}, &Box::name);    
    print("after sorting by name", copy);           
    std::ranges::sort(copy, {}, &Box::volume);    
    print("after sorting by volume", copy);     
}

在 Compiler Explorer 上运行它。

这是输出:

initial
large cubic, volume 1000
small cubic, volume 27
large long, volume 40
small, volume 12
after sorting by name
large cubic, volume 1000
large long, volume 40
small, volume 12
small cubic, volume 27
after sorting by volume
small, volume 12
small cubic, volume 27
large long, volume 40
large cubic, volume 1000

在示例中,我使用了一个简单的结构,然后传递了一个指向数据成员的指针或一个指向成员函数的指针。

转换

请参阅下面的代码:

#include <algorithm>
#include <vector>
#include <iostream>
#include <ranges>

struct Product {
    std::string name_;
    double value_ { 0.0 };
};

int main() {
    std::vector<Product> prods{7, {"Box ", 1.0}};

    // standard version:  
    std::transform(begin(prods), end(prods), begin(prods), 
        [v = 0](const Product &p) mutable {
            return Product { p.name_ + std::to_string(v++), 1.0};
        }
    );
    for (auto &p : prods) std::cout << p.name_ << ", ";
    std::cout << '\n';

    // ranges version:  
    std::ranges::transform(prods, begin(prods), 
        [v = 0](const std::string &n) mutable {
            return Product { n + std::to_string(v++), 1.0};
        }, 
        &Product::name_);
    for (auto &p : prods) std::cout << p.name_ << ", ";
}

在 Compiler Explorer 上运行它。

这是输出:

Box 0, Box 1, Box 2, Box 3, Box 4, Box 5, Box 6, 
Box 00, Box 11, Box 22, Box 33, Box 44, Box 55, Box 66, 

std::transform 的标准版本使用整个 Product& p 参数创建名称。然后范围版本仅采用字符串参数并扩展该名称。请注意传递到转换 lambda 中的不同参数。

最大元素

在我关于 std::format 的文章中,我需要在名称映射中找到最长的字符串:

const std::map<std::string, std::array<double, 5>> productToOrders{
        { "apples", {100, 200, 50.5, 30, 10}},
        { "bananas", {80, 10, 100, 120, 70}},
        { "carrots", {130, 75, 25, 64.5, 128}},
        { "tomatoes", {70, 100, 170, 80, 90}}
};

最初,我有以下函数:

template <typename T>
size_t MaxKeyLength(const std::map<std::string, T>& m) {
    if (m.empty())
        return 0;
        
	auto res = std::ranges::max_element(std::views::keys(m), 
	[](const auto& a, const auto& b) {
		return a.length() < b.length();
		});
	return (*res).length();
}

上面的代码使用了一个自定义比较器,它从键视图中查看给定的属性。 但我们可以将其更改为使用投影:

template <typename T>
size_t MaxKeyLength(const std::map<std::string, T>& m) {
    if (m.empty())
        return 0;
    auto res = std::ranges::max_element(std::views::keys(m), 
        std::less{}, &std::string::length // <<
        );
    return (*res).length();
}

注意这一行:

std::less{}, &std::string::length // <<

我正在使用一个默认比较器和一个指向成员函数的自定义投影。

自定义算法

投影不是为范围保留的。您也可以在代码中使用它。这是一个打印容器的函数示例:

void PrintEx(const std::ranges::range auto& container, auto proj) {
	for (size_t i = 0; auto && elem : container)
		std::cout << std::invoke(proj, elem) 
                  << (++i == container.size() ? "\n" : ", ");
};

int main() {
    std::vector<std::pair<int, char>> pairs { {0, 'A'}, {1, 'B'}, {2, 'C'}};
    PrintEx(pairs, &std::pair<int, char>::first);
    PrintEx(pairs, &std::pair<int, char>::second);
}

见 Compiler Explorer 。

“神奇”的部分是在 proj 参数上使用 std::invoke。就这样。 我们还可以使用默认模板参数来增强代码:

template <typename Proj = std::identity>
void PrintEx(const std::ranges::range auto& container, Proj proj = {}) {
	for (size_t i = 0; auto && elem : container)
		std::cout << std::invoke(proj, elem) 
                  << (++i == container.size() ? "\n" : ", ");
};

现在,我们可以编写以下用例:

int main() {
    std::vector<std::pair<int, char>> pairs { {0, 'A'}, {1, 'B'}, {2, 'C'}};
    PrintEx(pairs, &std::pair<int, char>::first);
    PrintEx(pairs, &std::pair<int, char>::second);

    std::vector numbers {1, 2, 3, 4, 5};
    PrintEx(numbers); // default proj
    PrintEx(numbers, [](auto& v) { return v*v;});

    std::map<std::string, int> words { {"Hello", 1}, {"World", 2 }};
    PrintEx(words, [](auto& keyVal){ return keyVal.first;});
    PrintEx(words, &std::map<std::string, int>::value_type::second);
}

在 Compiler Explorer 上运行它。

总结

投影是方便的“变形金刚”,您可以在将其发送到算法之前将其应用于每个范围元素。一旦学习了基础知识,就可以使用它们来编写更短、更紧凑的代码。

那么对于你来说,你试过投影吗? 你在你的项目中使用范围吗? 加入下面的讨论。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值