使用C++范围从展平表创建层次树结构

xam8gpfp  于 2023-06-25  发布在  其他
关注(0)|答案(2)|浏览(193)

我有一个简单的结构体,包含三个字段和一个飞船操作符<=>,如下所示:

  1. using MyStruct = struct MyStruct {
  2. unsigned id;
  3. std::filesystem::path sourcePath;
  4. std::string functionName;
  5. auto operator<=>(const MyStruct&) const = default;
  6. };

数据初始化如下所示:

  1. std::vector<MyStruct> structs{
  2. {1, "c:/temp/file3.c", "chimp()"},
  3. {2, "c:/temp/file3.c", "ape()"},
  4. {3, "c:/temp/file1.c", "foo()"},
  5. {4, "c:/temp/file1.c", "bar()"},
  6. {5, "c:/temp/file1.c", "baz()"},
  7. {6, "c:/temp/sub/file3.c", "file3Fn2()"},
  8. {7, "c:/temp/sub/file3.c", "file3Fn1()"},
  9. {8, "c:/temp/file2.c", "file2Fn2()"},
  10. {9, "c:/temp/file2.c", "file2Fn3()"},
  11. {10, "c:/temp/file2.c", "file2Fn1()"},
  12. };

我试图从以下2个字段(sourcePathfunctionName)创建一个非常简单的排序树。我不在乎id字段。
为了创建树结构,我需要过滤出唯一的文件名,然后将函数名作为叶子添加到这些文件名中。
我想使用新的c++20范围或范围-v3来实现这一点,但我在将正确的范围/视图连接在一起时遇到了麻烦。我需要将展平的数据分解成两个循环:包含文件名的外部文件和包含与每个文件名相关联的函数名的内部文件。
我所追求的树是:

  1. ├───root
  2. ├───file1.c
  3. ├───foo()
  4. ├───foo()
  5. └───baz()
  6. ├───file2.c
  7. ├───file2Fn1()
  8. ├───file2Fn2()
  9. └───file2Fn3()
  10. ├───file3.c
  11. ├───ape()
  12. └───chimp()
  13. └───sub
  14. └───file3.c
  15. ├───file3Fn1()
  16. └───file3Fn2()

这是我到目前为止的代码(和结果)(Visual Studio 2022 Preview 2)

  1. #include <algorithm>
  2. #include <format>
  3. #include <iostream>
  4. #include <filesystem>
  5. using MyStruct = struct MyStruct {
  6. unsigned id;
  7. std::filesystem::path sourcePath;
  8. std::string functionName;
  9. auto operator<=>(const MyStruct&) const = default;
  10. };
  11. //! Template partial specialization for use with std::formatter
  12. template<>
  13. struct std::formatter<MyStruct> : std::formatter<std::string_view> {
  14. // parse inherited from std::formatter<std::string_view>
  15. template <typename FormatContext>
  16. auto format(const MyStruct& arg, FormatContext& ctx) {
  17. return formatter<string_view>::format(std::format(
  18. "{}\t{}\t{}"
  19. , arg.id
  20. , arg.sourcePath.generic_string()
  21. , arg.functionName), ctx);
  22. }
  23. };
  24. void
  25. print(std::string_view intro, const std::vector<MyStruct>& container) {
  26. std::cout << intro << '\n';
  27. for (const auto& next : container) {
  28. std::cout << std::format("{}\n", next);
  29. }
  30. }
  31. std::vector<MyStruct> structs{
  32. {1, "c:/temp/file3.c", "chimp()"},
  33. {2, "c:/temp/file3.c", "ape()"},
  34. {3, "c:/temp/file1.c", "foo()"},
  35. {4, "c:/temp/file1.c", "bar()"},
  36. {5, "c:/temp/file1.c", "baz()"},
  37. {6, "c:/temp/sub/file3.c", "file3Fn2()"},
  38. {7, "c:/temp/sub/file3.c", "file3Fn1()"},
  39. {8, "c:/temp/file2.c", "file2Fn2()"},
  40. {9, "c:/temp/file2.c", "file2Fn3()"},
  41. {10, "c:/temp/file2.c", "file2Fn1()"},
  42. };
  43. // comparator used for unique
  44. static const auto customComp = [](const auto& lhs, const auto& rhs) {
  45. return std::tie(lhs.sourcePath, lhs.functionName) <
  46. std::tie(rhs.sourcePath, rhs.functionName);
  47. };
  48. int
  49. main() {
  50. auto copy = structs;
  51. std::ranges::sort(copy, {}, &MyStruct::sourcePath);
  52. print("after sorting by sourcePath", copy);
  53. std::ranges::sort(copy, customComp);
  54. print("after sorting by customComp", copy);
  55. }

程序输出如下,注意customComp比较器的顺序似乎产生正确排序的结果。

  1. after sorting by sourcePath
  2. 3 c:/temp/file1.c foo()
  3. 4 c:/temp/file1.c bar()
  4. 5 c:/temp/file1.c baz()
  5. 8 c:/temp/file2.c file2Fn2()
  6. 9 c:/temp/file2.c file2Fn3()
  7. 10 c:/temp/file2.c file2Fn1()
  8. 1 c:/temp/file3.c chimp()
  9. 2 c:/temp/file3.c ape()
  10. 6 c:/temp/sub/file3.c file3Fn2()
  11. 7 c:/temp/sub/file3.c file3Fn1()
  12. after sorting by customComp
  13. 4 c:/temp/file1.c bar()
  14. 5 c:/temp/file1.c baz()
  15. 3 c:/temp/file1.c foo()
  16. 10 c:/temp/file2.c file2Fn1()
  17. 8 c:/temp/file2.c file2Fn2()
  18. 9 c:/temp/file2.c file2Fn3()
  19. 2 c:/temp/file3.c ape()
  20. 1 c:/temp/file3.c chimp()
  21. 7 c:/temp/sub/file3.c file3Fn1()
  22. 6 c:/temp/sub/file3.c file3Fn2()
p3rjfoxz

p3rjfoxz1#

这是一个冗长的答案,因为它包含四种不同的方法,每种方法都有其优缺点。

使用std::map代替范围

根据我的理解,一个简单的std::map就足够了:

  1. #include <vector>
  2. #include <algorithm>
  3. #include <iostream>
  4. #include <filesystem>
  5. #include <map>
  6. using MyStruct = struct MyStruct {
  7. unsigned id;
  8. std::filesystem::path sourcePath;
  9. std::string functionName;
  10. auto operator<=>(const MyStruct&) const = default;
  11. };
  12. std::vector<MyStruct> structs{
  13. {1, "c:/temp/file3.c", "chimp()"},
  14. {2, "c:/temp/file3.c", "ape()"},
  15. {3, "c:/temp/file1.c", "foo()"},
  16. {4, "c:/temp/file1.c", "bar()"},
  17. {5, "c:/temp/file1.c", "baz()"},
  18. {6, "c:/temp/sub/file3.c", "file3Fn2()"},
  19. {7, "c:/temp/sub/file3.c", "file3Fn1()"},
  20. {8, "c:/temp/file2.c", "file2Fn2()"},
  21. {9, "c:/temp/file2.c", "file2Fn3()"},
  22. {10, "c:/temp/file2.c", "file2Fn1()"},
  23. };
  24. using Map = std::map<std::filesystem::path, std::vector<std::string>>;
  25. int
  26. main() {
  27. auto copy = structs;
  28. // sort by functionName
  29. std::ranges::sort(copy, std::less{}, [](auto const& s){ return s.functionName; });
  30. Map map;
  31. for (auto const& s : copy) {
  32. map[s.sourcePath].push_back(s.functionName);
  33. }
  34. for (auto const& [file, functions] : map) {
  35. std::cout << file << ":\t";
  36. int n{0};
  37. for (auto const& fun : functions) {
  38. std::cout << fun << (++n == functions.size()? "\n" : ", ");
  39. };
  40. }
  41. }

输出:

  1. c:/temp/file1.c: bar(), baz(), foo()
  2. c:/temp/file2.c: file2Fn1(), file2Fn2(), file2Fn3()
  3. c:/temp/file3.c: ape(), chimp()
  4. c:/temp/sub/file3.c: file3Fn1(), file3Fn2()

https://godbolt.org/z/6P498sTGM

使用范围

假设您无论如何都想使用范围,用于可组合性和延迟求值。那么你有几个选择。

使用std::ranges::unique

对输入范围排序后,可以使用std::ranges::unique基于sourcePath过滤唯一条目。然后,你可以使用std::ranges::equal_range将唯一范围中的每个条目转换为函数名的范围:

  1. // sort first by sourcePath, then by functionName
  2. auto copy = structs;
  3. std::ranges::sort(copy, std::ranges::less{}, [](auto const& s){ return std::tie(s.sourcePath, s.functionName); });
  4. // create a second copy which contains only the first element for each sourcePath
  5. auto sorted = copy;
  6. auto const ret = std::ranges::unique(sorted, std::ranges::equal_to{}, &MyStruct::sourcePath);
  7. sorted.erase(ret.begin(), ret.end());
  8. print("unique", sorted);
  9. // transform each entry in the unique range to the list of functions
  10. auto outer = sorted |
  11. std::views::transform(
  12. [&copy](auto const& s){
  13. return std::ranges::equal_range(
  14. copy,
  15. s,
  16. [](auto const& l, auto const& r){ return l.sourcePath < r.sourcePath; }
  17. );
  18. }
  19. );
  20. for (auto const& inner : outer)
  21. print("", inner);
  1. unique
  2. 4 c:/temp/file1.c bar()
  3. 10 c:/temp/file2.c file2Fn1()
  4. 2 c:/temp/file3.c ape()
  5. 7 c:/temp/sub/file3.c file3Fn1()
  6. 4 c:/temp/file1.c bar()
  7. 5 c:/temp/file1.c baz()
  8. 3 c:/temp/file1.c foo()
  9. 10 c:/temp/file2.c file2Fn1()
  10. 8 c:/temp/file2.c file2Fn2()
  11. 9 c:/temp/file2.c file2Fn3()
  12. 2 c:/temp/file3.c ape()
  13. 1 c:/temp/file3.c chimp()
  14. 7 c:/temp/sub/file3.c file3Fn1()
  15. 6 c:/temp/sub/file3.c file3Fn2()

https://godbolt.org/z/888734r79
这样你就能获得可组合性。内部循环中存在懒惰,但std::ranges::unique并不懒惰。

std::map存储范围

或者,你可以创建一个sourcePaths的Map,而不是存储一个带有函数名的容器,你可以存储一个用std::views::filter创建的惰性视图作为Map的值:

  1. auto sorted_tree(auto const& rng)
  2. {
  3. // this lambda returns a lambda, that checks if a MyStruct instance's sourcePath is equal to the input argument
  4. auto has_source_path = [](std::filesystem::path const& path){
  5. return [&](auto const& s){
  6. return s.sourcePath == path;
  7. };
  8. };
  9. // store results in a map that maps strings to a std::ranges::filter_view
  10. using SubRangeType = std::ranges::filter_view<
  11. std::ranges::ref_view<std::remove_reference_t<decltype(rng)>>,
  12. std::invoke_result_t<decltype(has_source_path), std::filesystem::path>
  13. >;
  14. using Map = std::map<std::filesystem::path, SubRangeType>;
  15. Map map;
  16. for (auto const& s : rng) {
  17. if (!map.contains(s.sourcePath)) {
  18. map.emplace(
  19. std::make_pair(
  20. s.sourcePath,
  21. std::views::filter(rng, has_source_path(s.sourcePath))
  22. )
  23. );
  24. }
  25. }
  26. return map;
  27. }

你可以这样使用它:

  1. auto copy = structs;
  2. // optional: Only if you want the function names to be alphabetical
  3. std::ranges::sort(copy, std::ranges::less{}, &MyStruct::functionName);
  4. for (auto& [key, range] : sorted_tree(copy))
  5. print(key.generic_string(), range);

输出:

  1. c:/temp/file1.c
  2. 4 c:/temp/file1.c bar()
  3. 5 c:/temp/file1.c baz()
  4. 3 c:/temp/file1.c foo()
  5. c:/temp/file2.c
  6. 10 c:/temp/file2.c file2Fn1()
  7. 8 c:/temp/file2.c file2Fn2()
  8. 9 c:/temp/file2.c file2Fn3()
  9. c:/temp/file3.c
  10. 2 c:/temp/file3.c ape()
  11. 1 c:/temp/file3.c chimp()
  12. c:/temp/sub/file3.c
  13. 7 c:/temp/sub/file3.c file3Fn1()
  14. 6 c:/temp/sub/file3.c file3Fn2()

https://godbolt.org/z/G38WeG5hT

使用std::map作为std::ranges::unique的懒惰版本

您可以通过使用std::views::filter和一个 predicate 来获得std::ranges::unique的懒惰版本,该 predicate 存储了已访问的sourcePaths的Map。

  1. auto lazy_unique() {
  2. struct unique_predicate
  3. {
  4. bool operator()(MyStruct const& s) {
  5. if (visited.contains(s.sourcePath)) {
  6. return false;
  7. }
  8. visited[s.sourcePath] = {};
  9. return true;
  10. }
  11. struct Empty {};
  12. std::unordered_map<std::filesystem::path, Empty> visited;
  13. };
  14. return std::views::filter(unique_predicate{});
  15. };

你可以这样使用它:

  1. // sort first by sourcePath, then by functionName
  2. auto copy = structs;
  3. std::ranges::sort(copy, std::ranges::less{}, [](auto const& s){ return std::tie(s.sourcePath, s.functionName); });
  4. // transform each entry in the unique range to the list of functions
  5. auto outer = copy | lazy_unique() |
  6. std::views::transform(
  7. [&copy](auto const& s){
  8. return std::ranges::equal_range(
  9. copy,
  10. s,
  11. [](auto const& l, auto const& r){ return l.sourcePath < r.sourcePath; }
  12. );
  13. }
  14. );
  15. for (auto const& inner : outer) {
  16. print(inner.begin()->sourcePath.generic_string(), inner);
  17. }

输出:

  1. "c:/temp/file1.c"
  2. 4 c:/temp/file1.c bar()
  3. 5 c:/temp/file1.c baz()
  4. 3 c:/temp/file1.c foo()
  5. "c:/temp/file2.c"
  6. 10 c:/temp/file2.c file2Fn1()
  7. 8 c:/temp/file2.c file2Fn2()
  8. 9 c:/temp/file2.c file2Fn3()
  9. "c:/temp/file3.c"
  10. 2 c:/temp/file3.c ape()
  11. 1 c:/temp/file3.c chimp()
  12. "c:/temp/sub/file3.c"
  13. 7 c:/temp/sub/file3.c file3Fn1()
  14. 6 c:/temp/sub/file3.c file3Fn2()

https://godbolt.org/z/8ds96dG7G
但是,这种方法存在一个问题:unique_predicate返回的视图适配器在调用 predicate 时改变Map。因此,您不能重复使用范围:在它上面迭代两次将在第二遍中返回无效的范围。当你传递一个 * 附近的常量引用时,你也可能会遇到不直观的编译器错误(例如:print函数)*,因为不可能迭代范围的const引用,因为它需要变异。
https://godbolt.org/z/edr1PGfPc

展开查看全部
lf3rwulv

lf3rwulv2#

你的投影使用是错误的,你按路径排序。它的目的是接受一个参数并返回比较的基础,例如。按函数名排序:

  1. std::ranges::sort(copy, {}, // same as &MyStruct::functionName
  2. [](MyStruct& s) ->decltype(s.functionName)& {
  3. return s.functionName;
  4. } );

显然,除非使用std::identity作为投影,否则宇宙飞船操作符不会影响排序。
可以使用std::tuple投影。

  1. std::ranges::sort( copy, {}, [](const MyStruct& s) {
  2. return std::tie(s.sourcePath, s.functionName);
  3. } );

这将对std::tuple使用默认的operator<=>,而不是对您的类。很明显,如果你不想像上面的例子一样,在引用/复制问题上遇到麻烦,也不想担心排序的语法是否正确,那么你必须编写一个自定义的操作符。在这种情况下,您可以使用更简单的sort函数版本。
为了在排序后打印为树,文件路径应该被视为一个范围(fs::path可以使用,因为它在其词法元素上有迭代器,或者你自己的等价物),这将增加print()函数中循环的深度。

相关问题