【C++】Ranges 工厂视图与投影机制

文章目录

一、样例代码

cpp 复制代码
#include <ranges>
#include <iostream>
#include <fstream>
#include <sstream>
#include <string>
#include <functional>  // 必须包含:std::invoke、std::quoted
#include <utility>     // std::forward
#include <type_traits> // 类型判断(invoke实现用)

// 生成斐波那契数列的范围
auto fibonacci() {
    return std::views::iota(0)
        | std::views::transform([](int) {
            // 静态变量初始化:避免首次调用返回错误值
            static int a = 0, b = 1;
            int current = b; // 先保存当前值(初始为1)
            int next = a + b;
            a = b;
            b = next;
            return current;
        });
}

// 读取文件行并输出
void read_file_lines(const std::string& filename) {
    std::ifstream file(filename);
    if (!file.is_open()) { // 判断文件是否打开
        std::cerr << "Failed to open file: " << filename << "\n";
        return;
    }
    // 将文件视为行的范围(std::views::istream接收流对象)
    auto lines = std::views::istream<std::string>(file);
    // 处理每行内容
    for (const auto& line : lines) {
        std::cout << "Line: " << line << '\n';
    }
    file.close(); // 好习惯:显式关闭文件
}

// 补充:std::invoke 简化实现(帮助理解原理)
namespace detail {
    // 辅助判断是否为成员指针
    template<class T>
    struct is_member_pointer_helper : std::false_type {};
    
    template<class T, class U>
    struct is_member_pointer_helper<T U::*> : std::true_type {};
    // 指向类 U 的成员 T 的指针
    
    template<class T>
    inline constexpr bool is_member_pointer_v =
        is_member_pointer_helper<std::decay_t<T>>::value; 
        // std::decay_t<T>:移除类型的引用、const/volatile 限定符,数组转指针,函数转函数指针

    // 核心调用实现
    template<class F, class... Args>
    constexpr auto invoke_impl(F&& f, Args&&... args) {
        if constexpr (is_member_pointer_v<F>) {
            static_assert(sizeof...(Args) > 0, "member pointer requires an object");
            // 拆分第一个参数(对象)和剩余参数
            auto& obj = std::get<0>(std::forward_as_tuple(std::forward<Args>(args)...));
            if constexpr (std::is_member_function_pointer_v<std::decay_t<F>>) {
                // 成员函数指针:obj.*f(剩余参数)
                return (obj.*f)(std::forward<Args>(args)...);
            } else {
                // 成员变量指针:obj.*f
                return obj.*f;
            }
        } else {
            // 普通可调用对象:直接调用
            return std::forward<F>(f)(std::forward<Args>(args)...);
        }
    }
}

// 简化版 std::invoke 实现
template<class F, class... Args>
constexpr std::invoke_result_t<F, Args...> invoke(F&& f, Args&&... args) {
    return detail::invoke_impl(std::forward<F>(f), std::forward<Args>(args)...);
}

// 补充:投影版 max_element 简化实现
template<typename It, typename Comp = std::ranges::less<>, typename Proj = std::identity>
It max_element(It first, It last, Comp comp = {}, Proj proj = {}) {
    if (first == last) return last;
    It largest = first;
    ++first;
    for (; first != last; ++first) {
        // 使用自定义invoke实现投影+比较
        if (detail::invoke_impl(comp,
                                detail::invoke_impl(proj, *largest),
                                detail::invoke_impl(proj, *first))) {
            largest = first;
        }
    }
    return largest;
}

int main() {
    // 1. iota视图:无限/有限序列
    auto infinite = std::views::iota(0);          // 无限序列:0,1,2,...
    auto finite = std::views::iota(1, 11);        // 有限序列:1-10
    std::cout << "Finite iota (1-10): ";
    for (auto e : finite) {
        std::cout << e << " ";
    }
    std::cout << "\n";

    // 2. empty视图(需加括号,且指定类型)
    auto e = std::views::empty<int>(); 
    std::cout << "Empty view size: " << std::ranges::size(e) << "\n"; // 输出0

    // 3. single视图:单个元素
    auto s = std::views::single(42);
    std::cout << "Single view element: ";
    for (auto x : s) std::cout << x << " ";
    std::cout << "\n";

    // 4. repeat视图(C++23特性,需注意编译器支持)
    // 注:std::views::repeat(值, 次数) 
    auto r = std::views::repeat(42, 10); // 10个42
    std::cout << "Repeat view (10x42): ";
    for (auto x : r) std::cout << x << " ";
    std::cout << "\n";

    // 5. 斐波那契视图
    std::cout << "Fibonacci first 10: ";
    for (int i : fibonacci() | std::views::take(10)) {
        std::cout << i << " "; // 正确输出:1 1 2 3 5 8 13 21 34 55
    }
    std::cout << "\n";

    // 6. istream视图:字符串流处理
    auto words = std::istringstream{ "today is yesterday's tomorrow" };
    std::cout << "Istream view (words): ";
    for (const auto& s : std::views::istream<std::string>(words)) {
        std::cout << std::quoted(s, '/') << ' '; // 输出:/today/ /is/ /yesterday's/ /tomorrow/
    }
    std::cout << "\n";

    // 7. 读取文件行(需确保main.cpp存在)
    // read_file_lines("main.cpp"); // 取消注释可测试,注意文件路径

    // 8. 投影机制示例:用自定义max_element找年龄最大的人
    struct Person {
        std::string name;
        int age;
    };
    std::vector<Person> people = {{"Alice",25}, {"Bob",30}, {"Charlie",20}};
    auto oldest = max_element(people.begin(), people.end(),
                              std::ranges::less{}, // 比较器:小于
                              &Person::age);       // 投影:取age成员
    std::cout << "Oldest person: " << oldest->name << " (" << oldest->age << ")\n";

    return 0;
}

二、范围工厂视图(Range Factory Views)深度解析

1. 工厂视图的核心定义(补充)

工厂视图是不依赖外部容器、直接生成元素序列的视图,核心特性:

  • 惰性求值:仅在遍历/访问时生成元素,无内存存储开销;
  • 轻量级:视图对象仅存储生成规则(如起始值、重复次数),而非数据;
  • 可组合:可与其他视图(filter/transform等)链式调用。

2. 常用工厂视图(修正+扩展)

(1)std::views::iota(最常用)
  • 作用:生成整数序列,支持无限/有限范围;

  • 语法:

    cpp 复制代码
    // 无限序列:从value开始,递增1(int类型)
    std::views::iota(value);
    // 有限序列:[start, end),左闭右开
    std::views::iota(start, end);
  • 扩展:支持任意可递增类型(如std::size_tchar):

    cpp 复制代码
    // 生成字符序列:a-z
    auto chars = std::views::iota('a', 'z'+1);
    for (char c : chars) std::cout << c << " "; // a b c ... z
(2)std::views::repeat(C++23,注意兼容性)
  • 作用:生成重复元素的序列;

  • 语法:

    cpp 复制代码
    // 无限重复value
    std::views::repeat(value);
    // 有限重复:value重复count次
    std::views::repeat(value, count);
  • 注意:部分编译器(如GCC 12+、Clang 15+)才支持,低版本需自行实现简易版:

    cpp 复制代码
    // 简易版repeat视图(C++20兼容)
    template<typename T>
    auto repeat_view(T value, std::size_t count) {
        return std::views::iota(0, count)
            | std::views::transform([=](int) { return value; });
    }
(3)std::views::empty
  • 作用:生成空范围,类型安全;

  • 语法:std::views::empty<T>()(必须指定类型+调用);

  • 适用场景:函数返回默认空范围、初始化空视图等;

  • 扩展:可与std::ranges::empty配合判断:

    cpp 复制代码
    auto e = std::views::empty<int>();
    std::cout << std::boolalpha << std::ranges::empty(e); // true
(4)std::views::single
  • 作用:生成仅包含单个元素的范围;

  • 语法:std::views::single(value)

  • 优势:相比临时容器(如std::vector{42}),无内存分配开销;

  • 扩展:可用于函数返回单个元素的范围:

    cpp 复制代码
    std::ranges::range auto get_single_value() {
        return std::views::single(3.14); // 返回double类型的单元素视图
    }
(5)std::views::istream
  • 作用:将输入流(std::istream/std::ifstream等)转换为范围;

  • 语法:std::views::istream<T>(stream)(T是流读取的元素类型);

  • 核心特性:

    • 惰性读取:遍历到下一个元素时才从流中读取;
    • 自动处理EOF:流结束时范围自动终止;
  • 扩展:读取文件中的整数:

    cpp 复制代码
    std::ifstream file("nums.txt"); // 假设文件内容:1 2 3 4 5
    auto nums = std::views::istream<int>(file);
    int sum = 0;
    for (int x : nums) sum += x; // 求和:15

3. 工厂视图综合案例:生成自定义序列(扩展)

需求:生成1-10的平方数序列(用iota+transform),过滤偶数平方,取前3个:

cpp 复制代码
auto square_odd = std::views::iota(1, 11)
    | std::views::transform([](int x) { return x*x; }) // 平方
    | std::views::filter([](int x) { return x%2 != 0; }) // 过滤奇数平方
    | std::views::take(3); // 取前3个

for (int x : square_odd) std::cout << x << " "; // 1 9 25

三、投影(Projection)机制深度解析

1. 投影的核心价值(补充)

投影是将元素转换为目标值后再参与算法计算的机制,核心价值:

  • 简化代码:无需手动写lambda提取成员/转换值;
  • 通用性:支持任意可调用对象作为投影(函数指针、成员指针、lambda等);
  • 零开销:编译期展开,无运行时性能损失。

2. std::invoke 核心原理(扩展)

std::invoke 是投影的底层支撑,作用是统一调用各种可调用对象,其处理逻辑:
普通函数 / lambda / 函数对象
成员函数指针 T U::*
成员变量指针 T U::*
std::invoke(f, args...)
f 是什么类型?
直接调用:f(args...)
取第一个参数为对象 obj

调用:obj.*f(其余 args)
取第一个参数为对象 obj

访问:obj.*f
返回结果

(1)std::invoke 支持的投影类型示例
cpp 复制代码
#include <string>
#include <vector>

struct Student {
    std::string name;
    int score;
    int get_score() const { return score; } // 成员函数
};

int main() {
    std::vector<Student> students = {{"Alice", 90}, {"Bob", 85}};
    
    // 投影类型1:成员变量指针
    auto max1 = std::ranges::max_element(students, {}, &Student::score);
    
    // 投影类型2:成员函数指针
    auto max2 = std::ranges::max_element(students, {}, &Student::get_score);
    
    // 投影类型3:lambda(自定义转换)
    auto max3 = std::ranges::max_element(students, {}, 
        [](const Student& s) { return s.score * 2; });
    
    return 0;
}
(2)投影版算法的执行流程(可视化)

std::ranges::max_element为例:
std::ranges::max_element 为例


输入:原始Range

(如vector)
遍历元素
对每个元素执行投影

proj = &Person::age

value = std::invoke(proj, element)
比较投影后的值

comp = std::ranges::less{}

comp(value1, value2)
是否更新最值?
记录当前元素为最值
遍历结束
返回最值对应的原始元素迭代器

3. max_element的执行过程

核心上下文:

cpp 复制代码
// 自定义简化版 max_element + invoke_impl
// 调用场景:找 people 中 age 最大的人
std::vector<Person> people = {{"Alice",25}, {"Bob",30}, {"Charlie",20}};
auto oldest = max_element(people.begin(), people.end(),
                          std::ranges::less{}, // 比较器:小于
                          &Person::age);       // 投影:取age成员
一、先梳理核心函数的角色分工
函数/参数 核心作用
max_element 遍历元素,通过投影+比较器找到最大值,是"流程控制器"
invoke_impl 统一处理"投影函数调用"和"比较器调用",是"通用调用器"
&Person::age 投影函数(成员变量指针),作用是从 Person 对象中提取 age 成员
std::ranges::less{} 比较器(函数对象),作用是判断"投影后的值1 < 投影后的值2"
二、逐行拆解 max_element 的执行流程

先贴完整的 max_element 简化实现(和之前一致,方便对照):

cpp 复制代码
template<typename It, typename Comp = std::ranges::less<>, typename Proj = std::identity>
It max_element(It first, It last, Comp comp = {}, Proj proj = {}) {
    if (first == last) return last;
    It largest = first;  // 初始:第一个元素是"当前最大值"
    ++first;             // 从第二个元素开始遍历
    for (; first != last; ++first) {
        // 核心行:调用 invoke_impl 执行"投影+比较"
        if (detail::invoke_impl(comp,
                                detail::invoke_impl(proj, *largest),
                                detail::invoke_impl(proj, *first))) {
            largest = first; // 如果新元素更大,更新最大值
        }
    }
    return largest;
}
步骤1:初始化阶段
  • first 初始指向 people[0](Alice,25),last 指向 people.end()
  • largest 初始赋值为 first(指向Alice);
  • ++first 后,first 指向 people[1](Bob,30)。
步骤2:第一次循环(first 指向 Bob)

核心是执行这行判断:

cpp 复制代码
if (invoke_impl(comp, invoke_impl(proj, *largest), invoke_impl(proj, *first)))

我们把这行拆成 3个 invoke_impl 调用 逐一分析:

子步骤2.1:调用 invoke_impl(proj, *largest)(提取 Alice 的 age)
  • 入参:
    • proj = &Person::age(成员变量指针,类型是 int Person::*);
    • *largest = people[0](Alice 对象,类型是 Person&);
  • invoke_impl 内部执行逻辑:
    1. 判断 proj 类型:is_member_pointer_v<Proj> = true(是成员指针);
    2. 进一步判断:不是成员函数指针(是成员变量指针);
    3. 执行调用:obj.*f(*largest).*projAlice.age → 结果是 25
  • 返回值:25(int 类型)。
子步骤2.2:调用 invoke_impl(proj, *first)(提取 Bob 的 age)
  • 入参:
    • proj = &Person::age
    • *first = people[1](Bob 对象);
  • 执行逻辑和子步骤2.1完全一致;
  • 返回值:30(int 类型)。
子步骤2.3:调用 invoke_impl(comp, 25, 30)(比较 25 < 30)
  • 入参:
    • comp = std::ranges::less{}(函数对象,重载了 operator());
    • 后续参数:2530
  • invoke_impl 内部执行逻辑:
    1. 判断 comp 类型:is_member_pointer_v<Comp> = false(不是成员指针);
    2. 执行普通调用:comp(25,30)25 < 30 → 结果是 true
  • 返回值:true
子步骤2.4:更新最大值

因为判断结果是 true(说明 Bob 的 age 比 Alice 大),所以 largest = firstlargest 现在指向 Bob)。

步骤3:第二次循环(first 指向 Charlie)
  • ++first 后,first 指向 people[2](Charlie,20);
  • 重复子步骤2.1-2.3:
    1. invoke_impl(proj, *largest) → Bob.age = 30
    2. invoke_impl(proj, *first) → Charlie.age = 20
    3. invoke_impl(comp, 30, 20)30 < 20false
  • 因为判断结果是 false,不更新 largest(仍指向 Bob)。
步骤4:循环结束,返回结果
  • 遍历完成,largest 指向 Bob(30),这就是最终的"年龄最大的人";
  • 主函数中 oldest->name 输出 Boboldest->age 输出 30
三、深度拆解 invoke_impl 的核心逻辑

贴完整的 invoke_impl 实现(修正后,方便对照):

cpp 复制代码
namespace detail {
    // 辅助判断:是否是成员指针(成员函数/成员变量)
    template<class T>
    struct is_member_pointer_helper : std::false_type {};
    template<class T, class U>
    struct is_member_pointer_helper<T U::*> : std::true_type {};
    template<class T>
    inline constexpr bool is_member_pointer_v =
        is_member_pointer_helper<std::decay_t<T>>::value;

    // 核心调用实现
    template<class F, class... Args>
    constexpr auto invoke_impl(F&& f, Args&&... args) {
        if constexpr (is_member_pointer_v<F>) {
            static_assert(sizeof...(Args) > 0, "member pointer requires an object");
            // 拆分第一个参数(对象)和剩余参数
            auto& obj = std::get<0>(std::forward_as_tuple(std::forward<Args>(args)...));
            if constexpr (std::is_member_function_pointer_v<std::decay_t<F>>) {
                // 分支1:成员函数指针 → obj.*f(剩余参数)
                return (obj.*f)(std::forward<Args>(args)...);
            } else {
                // 分支2:成员变量指针 → obj.*f
                return obj.*f;
            }
        } else {
            // 分支3:普通可调用对象(函数/函数对象/lambda)→ f(args...)
            return std::forward<F>(f)(std::forward<Args>(args)...);
        }
    }
}
关键分支解析(对应本次调用)
分支2:处理成员变量指针(&Person::age)
  • 触发条件:is_member_pointer_v<F> = true!is_member_function_pointer_v<F>
  • 核心操作:
    1. std::get<0>(...):提取第一个参数(Person 对象);
    2. obj.*f:访问对象的成员变量(f&Person::age);
  • 为什么要这么设计?
    成员变量指针不能直接调用(不像函数),必须绑定到具体对象才能访问,invoke_impl 帮我们封装了"对象+成员指针"的访问逻辑,让投影调用和普通函数调用的语法统一。
分支3:处理普通可调用对象(std::ranges::less{})
  • 触发条件:is_member_pointer_v<F> = false
  • 核心操作:std::forward<F>(f)(std::forward<Args>(args)...)
    • std::forward 是为了完美转发参数(保持左值/右值属性);
    • 本质就是调用 comp(25,30),和直接写 25 < 30 等价;
  • 为什么要这么设计?
    不管是 std::ranges::less{}、lambda、普通函数指针,都能通过这行代码统一调用,无需为每种可调用类型写单独逻辑。
四、关键细节补充(新手易混淆点)
1. 为什么投影返回的是原始元素的迭代器?
  • max_element 中,largest 始终存储的是 原始元素的迭代器 (指向 Person 对象),而非投影后的值(int);
  • 这是核心设计:投影仅用于"比较判断",最终返回的还是原始数据,方便后续访问 name/age 等成员(比如我们最终要输出 oldest->name)。
2. invoke_impl 的"完美转发"有什么用?
  • 本次调用中参数都是左值,体现不出优势,但如果投影/比较器需要处理右值(比如临时对象),std::forward 能保证参数的"值类别"不丢失;
  • 例如:如果投影函数是 [](Person&& p) { return p.age; },完美转发能确保 p 是右值引用,避免不必要的拷贝。
3. 对比"不用投影"的写法(更体现投影的价值)

如果没有 invoke_impl 和投影,max_element 的判断逻辑需要手写:

cpp 复制代码
// 无投影的写法(冗余且不通用)
if (comp((*largest).age, (*first).age)) {
    largest = first;
}
  • 投影的价值:把 (*largest).age 这种"硬编码提取成员"的逻辑,变成通用的 invoke_impl(proj, *largest),支持任意投影函数(成员变量、成员函数、lambda等)。
五、总结
核心执行链路(一句话总结)

max_element 遍历元素 → 调用 invoke_impl 执行"成员变量指针投影"(提取age)→ 调用 invoke_impl 执行"比较器函数对象"(判断age大小)→ 根据比较结果更新最大值迭代器 → 返回指向Bob的迭代器。

invoke_impl 的核心价值
  1. 统一调用语法 :不管是成员变量指针、成员函数指针、lambda、普通函数,都能用 invoke_impl(f, args...) 调用;
  2. 编译期安全 :通过类型判断(is_member_pointer_v)在编译期分支处理,无运行时开销;
  3. 完美转发:保持参数的左值/右值属性,适配各种可调用对象的参数要求。
本次调用的最终结果

oldest 指向 people[1](Bob,30),输出:Oldest person: Bob (30),和预期一致。

投影的实际应用案例(扩展)

需求:对自定义类型数组,按"姓名长度"排序:

cpp 复制代码
#include <algorithm>
#include <vector>
#include <string>

struct User {
    std::string name;
    int id;
};

int main() {
    std::vector<User> users = {{"ZhangSan", 1}, {"LiSi", 2}, {"WangWu", 3}};
    
    // 投影:按姓名长度排序(无需手动写lambda比较)
    std::ranges::sort(users, std::ranges::less{}, 
        [](const User& u) { return u.name.size(); });
    
    // 输出排序结果:LiSi(2) → WangWu(3) → ZhangSan(1)
    for (const auto& u : users) {
        std::cout << u.name << " (" << u.id << ")\n";
    }
    return 0;
}

四、关键扩展:工厂视图+投影的综合实战

需求:从文件读取学生成绩(格式:姓名 分数),找出分数最高的学生,输出其姓名和分数:

cpp 复制代码
#include <ranges>
#include <iostream>
#include <fstream>
#include <vector>
#include <algorithm>
#include <string>

// 读取成绩文件并返回学生范围
auto read_scores(const std::string& filename) {
    std::ifstream file(filename);
    if (!file.is_open()) {
        throw std::runtime_error("Failed to open file: " + filename);
    }
    // 用istream视图读取文件,每行拆分为姓名+分数
    return std::views::istream<std::string>(file)
        | std::views::chunk(2) // 每2个元素为一组(C++23)
        | std::views::transform([](auto&& chunk) {
            auto it = chunk.begin();
            std::string name = *it++;
            int score = std::stoi(*it);
            return std::make_pair(name, score);
        });
}

int main() {
    try {
        auto scores = read_scores("scores.txt"); // 文件内容:Alice 90 Bob 85 Charlie 95
        
        // 投影:按分数找最大值
        auto max_score = std::ranges::max_element(scores, {}, 
            [](const auto& p) { return p.second; });
        
        std::cout << "Highest score: " << max_score->first 
                  << " (" << max_score->second << ")\n"; // Charlie (95)
    } catch (const std::exception& e) {
        std::cerr << "Error: " << e.what() << "\n";
    }
    return 0;
}

五、总结

核心要点回顾

  1. 工厂视图

    • 是不依赖外部容器的序列生成工具,惰性求值、轻量级;
    • 常用类型:iota(整数序列)、repeat(重复序列)、istream(流序列)、single/empty(单元素/空序列);
    • 注意repeat是C++23特性,低版本可通过iota+transform模拟。
  2. 投影机制

    • 核心是std::invoke,统一调用各种可调用对象(成员指针、lambda等);
    • 作用是将元素转换后再参与算法计算,简化代码且零开销;
    • 所有Ranges算法(sort/max_element/find等)都支持投影参数。
  3. 实战关键

    • 工厂视图适合生成基础序列,投影适合简化自定义类型的算法调用;
    • 组合使用视图+投影可实现高效、简洁的流式数据处理,无中间容器开销。

扩展建议

  • 编译器兼容性:C++23的views::repeat/views::chunk需升级编译器(GCC 12+/Clang 15+);
  • 自定义工厂视图:可基于iota+transform实现自定义序列(如斐波那契、质数序列);
  • 性能优化:视图的惰性求值特性适合处理大文件/无限序列,避免一次性加载所有数据。
相关推荐
.小墨迹1 小时前
局部规划中的TEB,DWA,EGOplanner等算法在自动驾驶中应用?
开发语言·c++·人工智能·学习·算法·机器学习·自动驾驶
哈基咩1 小时前
从零搭建校园活动平台:go-zero 微服务实战完整指南
开发语言·微服务·golang
前端程序猿i2 小时前
第 3 篇:消息气泡组件 —— 远比你想的复杂
开发语言·前端·javascript·vue.js
一晌小贪欢2 小时前
Python在物联网(IoT)中的应用:从边缘计算到云端数据处理
开发语言·人工智能·python·物联网·边缘计算
你的冰西瓜2 小时前
C++中的priority_queue容器详解
开发语言·c++·stl
H Corey2 小时前
Java字符串操作全解析
java·开发语言·学习·intellij-idea
brucelee1862 小时前
Java 开发AWS Lambda 实战指南(SAM CLI + IntelliJ)
java·开发语言
柒儿吖2 小时前
三方库 Emoji Segmenter 在 OpenHarmony 的 lycium 适配与测试
c++·c#·openharmony
tobias.b2 小时前
408真题解析-2010-37-计算机网络-子网划分与CIDR
开发语言·计算机网络·计算机考研·408真题解析