理解范围库中的cartesian_product适配器
一、简介
view::cartesian_product
适配器是range-v3 库一个新的组件。本文主要理解这个组件的功能以及它背后的设计理念,可以更好地掌握range
库。虽然可以通过研究 zip
适配器来理解这些内容,但 cartesian_product
是一个全新的适配器,探索它既可以掌握range
库,也可以学习新技术,一举两得。
为什么要花时间理解范围库呢?
因为范围是 STL 的未来。STL 是一个强大的工具,可以编写富有表现力的代码,而范围库是一个设计精良的库,它将 STL 的功能推向了更远。简而言之,学习范围库可以了解编写富有表现力的 C++ 代码的趋势。
二、cartesian_product 适配器的动机
cartesian_product
适配器的目的是遍历多个集合元素的所有可能组合。
为了避免引入业务逻辑,本文将使用一些简单的示例,但它在实际应用中非常有用,例如当对象具有版本时。在这种情况下,可能希望生成所有对象在所有可能日期上的组合。
为了说明问题,使用以下三个集合:
-
一个包含数字的集合:
std::vector<int> numbers = {3, 5, 12, 2, 7};
-
一个包含字符串表示的常见聚会食物类型的集合:
std::vector<std::string> dishes = {"pizzas", "beers", "chips"};
-
一个包含字符串表示的聚会地点的集合:
std::vector<std::string> places = {"London", "Paris", "NYC", "Berlin"};
现在,希望对这三个集合元素的所有可能组合执行一个操作,例如打印一个句子。
三、将行为封装到算法中
编写一个通用的函数,可以将一个函数应用于多个集合元素的所有可能组合。为了专注于算法的职责,这里省略了可变参数的内容:
template<typename Collection1, typename Collection2, typename Collection3, typename Function>
void cartesian_product(Collection1&& collection1, Collection2&& collection2, Collection3&& collection3, Function func)
{
for (auto& element1 : collection1)
for (auto& element2 : collection2)
for (auto& element3 : collection3)
func(element1, element2, element3);
}
这个函数可以完成任务。调用:
cartesian_product(numbers, dishes, places,
[](int number, std::string const& dish, std::string const& place)
{ std::cout << "I took " << number << ' ' << dish << " in " << place << ".\n";});
四、算法的局限性
虽然看起来不错,但如果稍微改变一下需求,上面的代码就会失效。假设不再希望函数直接写入控制台,而是希望将各种组合输出到一个字符串容器中。
在这种情况下,无法使用上面的实现,因为它没有返回值。
实际上,上面的算法类似于 std::for_each
的所有可能组合版本,因为它遍历所有组合并应用一个函数。真正需要的是类似于 std::transform
的东西。
难道要重新编写一个新的 cartesian_product
函数,它接受一个输出集合和一个函数,就像 std::transform
一样吗?这感觉不对劲,不是吗?更希望将迭代职责从算法中分离出来。而 cartesian_product
适配器正是为此而生的。
cartesian_product
适配器在多个集合上构建一个视图,将其表示为一个包含所有可能组合的元组的范围。然后,函数需要接受一个包含其参数的元组。需要注意的是,最好直接获取参数,而不是通过元组。
以下是一个满足将句子输出到字符串容器需求的示例:
std::string meetupRecap(std::tuple<int, std::string, std::string> const& args)
{
int number = std::get<0>(args);
std::string const& dish = std::get<1>(args);
std::string const& place = std::get<2>(args);
std::ostringstream result;
result << "I took " << number << ' ' << dish << " in " << place << '.';
return result.str();
}
std::vector<std::string> results;
transform(ranges::view::cartesian_product(numbers, dishes, places), std::back_inserter(results), meetupRecap);
同一个适配器也可以用于将输出写入控制台,而无需编写特定的算法:
void meetupRecapToConsole(std::tuple<int, std::string, std::string> const& args)
{
int number = std::get<0>(args);
std::string const& dish = std::get<1>(args);
std::string const& place = std::get<2>(args);
std::cout << "I took " << number << ' ' << dish << " in " << place << ".\n";
}
for_each(ranges::view::cartesian_product(numbers, dishes, places), meetupRecapToConsole);
这个适配器有效地承担了生成所有可能元素组合的职责,从而能够重用常规算法,例如 for_each
和 transform
。
五、总结
cartesian_product
适配器是范围库中一个强大的工具,它可以简化生成多个集合元素所有可能组合的过程。通过使用 cartesian_product
适配器,我们可以避免编写复杂的循环嵌套,从而提高代码的可读性和可维护性。
此外,cartesian_product
适配器还能够与其他范围库组件(如 transform
和 for_each
)无缝衔接,进一步提升代码的灵活性和表达力。
虽然目前 cartesian_product
适配器使用元组来传递参数,但未来可能会引入更简洁的接口,例如直接传递参数而不使用元组。这将进一步简化代码编写,并使 cartesian_product
适配器更加易于使用。
cartesian_product
适配器是范围库中一个值得关注的组件,它可以帮助我们更高效地处理元素组合问题,并为编写更富有表现力的 C++ 代码提供新的思路。