跳转至

现代嵌入式C++教程——C++20范围库基础与视图

引言

每次处理数组、容器数据的时候,我总觉得少点什么。用STL算法吧,那些std::transformstd::filter写起来简直是一坨——迭代器开头、迭代器结尾、临时容器、最后再贴回去,一套操作下来,代码逻辑被拆得七零八落,读起来像在啃干面包。

然后C++20带来了Ranges库,就像给你的代码装上了一台"数据处理流水线"。更重要的是,它引入了"视图"(View)这个概念——惰性求值、零开销拷贝,这对嵌入式开发来说简直就是量身定做的。

一句话总结:Ranges让你像Unix管道一样组合操作,视图(View)让你处理数据时不产生额外拷贝,既优雅又高效。

我们现在的目标是搞懂两件事:什么是Range,什么是View,以及它们为什么在嵌入式场景下如此有用。


从痛点说起:传统STL算法有多烦

先看看我们以前是怎么处理数据的。假设我们从传感器读了一组数据,要过滤掉异常值,然后把剩下的都乘以一个系数:

#include <vector>
#include <algorithm>

void process_sensor_readings() {
    // 原始数据
    std::vector<int> readings = {120, 45, 230, 67, 340, 89, 56, 180};

    // 第一步:过滤掉小于50或大于300的异常值
    std::vector<int> filtered;
    std::copy_if(readings.begin(), readings.end(),
                 std::back_inserter(filtered),
                 [](int v) { return v >= 50 && v <= 300; });

    // 第二步:对过滤后的数据进行校准(乘以系数)
    std::vector<int> calibrated;
    std::transform(filtered.begin(), filtered.end(),
                   std::back_inserter(calibrated),
                   [](int v) { return v * 2; });

    // calibrated 现在是 {240, 90, 460, 134, 178, 112, 360}
}

你看这代码有多烦:

  • 每个操作都要写两遍迭代器范围
  • 需要创建临时容器filtered来存中间结果
  • 逻辑被中间变量打断,不能一眼看懂"原始数据 → 过滤 → 校准"这条链路
  • 内存分配至少两次(filteredcalibrated

在嵌入式场景下,这种临时内存分配尤其让人头疼——你确定heap有足够空间?确定不会碎片化?确定实时性不会被分配影响?

这些问题的答案,都在Ranges库里。


Range是什么:简单来说就是"一对迭代器"

C++20标准库给"Range"下的定义很简单:任何可以提供迭代器的东西

std::vector<int> vec = {1, 2, 3, 4, 5};
std::array<int, 4> arr = {10, 20, 30, 40};
int native_arr[] = {100, 200, 300};

这些全是Range。以前我们写算法要用vec.begin()vec.end(),现在可以直接把整个容器丢给算法:

#include <ranges>
#include <algorithm>

// C++20之前的写法
std::sort(vec.begin(), vec.end());

// C++20的写法
std::sort(vec);  // 直接传整个容器

但这只是表面糖衣,真正的威力在于<ranges>头文件里的一整套新工具。

首先我们得区分两个概念:RangeView

  • Range(范围):所有可以迭代的玩意儿,包括vectorarray、原生数组
  • View(视图):一种特殊的Range,它不拥有数据,只是对现有数据的"某种角度的观察",并且惰性求值

视图这个概念太重要了,我们用整节来聊它。


视图(View):零开销的数据透镜

视图的本质可以用四个字概括:懒、不拥有、可组合、O(1)拷贝

懒惰的视图

视图是"懒惰"的——你定义它的时候不会计算任何东西,只有当你真正迭代它的时候,计算才会发生:

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

void demo_lazy_view() {
    std::vector<int> data = {1, 2, 3, 4, 5};

    // 创建一个过滤视图:只保留大于2的元素
    auto filtered = std::views::filter(data, [](int x) { return x > 2; });

    // 到这里为止,什么都没发生!没有新容器被创建

    // 只有当你迭代的时候,过滤逻辑才会执行
    for (int x : filtered) {
        std::cout << x << ' ';  // 输出:3 4 5
    }
}

不拥有数据

视图只是"看着"底层数据,不拥有它们:

void demo_view_ownership() {
    std::vector<int> data = {1, 2, 3, 4, 5};

    auto view = std::views::filter(data, [](int x) { return x > 2; });

    // 修改底层数据
    data[0] = 100;

    // 视图反映的是底层数据的变化
    for (int x : view) {
        std::cout << x << ' ';  // 输出:3 4 5(100被过滤掉了)
    }
}
查看完整可编译示例
// View Basics - Lazy Evaluation and Non-Owning Ranges
// Demonstrates fundamental view concepts in C++20 Ranges

#include <iostream>
#include <ranges>
#include <vector>
#include <array>

void demo_lazy_evaluation() {
    std::cout << "=== Lazy Evaluation Demo ===" << std::endl;

    std::vector<int> data = {1, 2, 3, 4, 5};

    // Create a view - nothing is computed yet
    auto filtered = std::views::filter(data, [](int x) {
        std::cout << "  Filtering: " << x << std::endl;
        return x > 2;
    });

    std::cout << "View created, now iterating..." << std::endl;
    for (int x : filtered) {
        std::cout << "Got: " << x << std::endl;
    }
}

void demo_non_owning() {
    std::cout << "\n=== Non-Owning View ===" << std::endl;

    std::vector<int> data = {10, 20, 30, 40, 50};

    auto view = std::views::filter(data, [](int x) { return x > 25; });

    std::cout << "First iteration: ";
    for (int x : view) {
        std::cout << x << " ";
    }
    std::cout << std::endl;

    // Modify original data
    data[2] = 5;
    data[3] = 100;

    std::cout << "After modification: ";
    for (int x : view) {
        std::cout << x << " ";
    }
    std::cout << std::endl;
}

void demo_o1_copy() {
    std::cout << "\n=== O(1) Copy ===" << std::endl;

    std::vector<int> data = {1, 2, 3, 4, 5};

    auto view1 = std::views::filter(data, [](int x) { return x > 2; });
    auto view2 = view1;  // O(1) copy - no element copying!

    std::cout << "view1 and view2 both refer to same underlying data" << std::endl;

    std::cout << "view1: ";
    for (int x : view1) {
        std::cout << x << " ";
    }
    std::cout << std::endl;

    std::cout << "view2: ";
    for (int x : view2) {
        std::cout << x << " ";
    }
    std::cout << std::endl;
}

void demo_range_interface() {
    std::cout << "\n=== Range Interface ===" << std::endl;

    std::vector<int> vec = {1, 2, 3, 4, 5};
    std::array<int, 4> arr = {10, 20, 30, 40};
    int native_arr[] = {100, 200, 300};

    // All are ranges
    auto view1 = std::views::filter(vec, [](int x) { return x > 2; });
    auto view2 = std::views::filter(arr, [](int x) { return x > 15; });
    auto view3 = std::views::filter(native_arr, [](int x) { return x > 150; });

    std::cout << "Vector view: ";
    for (int x : view1) std::cout << x << " ";
    std::cout << std::endl;

    std::cout << "Array view: ";
    for (int x : view2) std::cout << x << " ";
    std::cout << std::endl;

    std::cout << "Native array view: ";
    for (int x : view3) std::cout << x << " ";
    std::cout << std::endl;
}

void demo_view_lifetime() {
    std::cout << "\n=== View Lifetime Warning ===" << std::endl;

    // DON'T DO THIS
    /*
    auto get_bad_view() {
        std::vector<int> local = {1, 2, 3};
        return std::views::filter(local, [](int x) { return x > 1; });
        // local is destroyed, returned view is dangling!
    }
    */

    // DO THIS
    class DataView {
        std::vector<int> data_;
    public:
        DataView(std::initializer_list<int> init) : data_(init) {}

        auto get_view() {
            return std::views::filter(data_, [](int x) { return x > 1; });
            // data_ lives as long as the object
        }
    };

    DataView dv({1, 2, 3, 4, 5});
    auto safe_view = dv.get_view();

    std::cout << "Safe view: ";
    for (int x : safe_view) {
        std::cout << x << " ";
    }
    std::cout << std::endl;
}

void demo_const_view() {
    std::cout << "\n=== Const View ===" << std::endl;

    const std::vector<int> data = {1, 2, 3, 4, 5};

    auto view = std::views::transform(data, [](int x) {
        return x * 2;
    });

    std::cout << "Transformed const data: ";
    for (int x : view) {
        std::cout << x << " ";
    }
    std::cout << std::endl;

    // Original data unchanged
    std::cout << "Original data: ";
    for (int x : data) {
        std::cout << x << " ";
    }
    std::cout << std::endl;
}

int main() {
    demo_lazy_evaluation();
    demo_non_owning();
    demo_o1_copy();
    demo_range_interface();
    demo_view_lifetime();
    demo_const_view();

    std::cout << "\n=== Key Takeaways ===" << std::endl;
    std::cout << "- Views are lazy: computation happens on iteration" << std::endl;
    std::cout << "- Views don't own data: just a window into existing data" << std::endl;
    std::cout << "- Views are O(1) to copy: just a few pointers" << std::endl;
    std::cout << "- View lifetime must not exceed underlying data" << std::endl;

    return 0;
}

O(1)拷贝

视图的拷贝成本是常数级别的——它只存几个指针/迭代器,不会复制底层数据:

void demo_view_copy() {
    std::vector<int> data = {1, 2, 3, 4, 5};

    auto view1 = std::views::filter(data, [](int x) { return x > 2; });
    auto view2 = view1;  // 拷贝视图:O(1),不复制任何元素!

    // view1和view2都指向同一个底层数据
}

这对嵌入式来说非常重要——你可以到处传递视图,不用担心数据拷贝的开销。


常用视图工厂函数

<ranges>头文件提供了一系列"视图工厂"函数,用来创建各种视图。我们挑嵌入式开发中最常用的几个来讲。

filter:过滤数据

std::views::filter创建一个只满足条件的元素的视图:

#include <ranges>
#include <vector>

void filter_example() {
    std::vector<int> readings = {120, 45, 230, 67, 340, 89, 56, 180};

    // 创建过滤视图:只保留50到300之间的读数
    auto valid_readings = std::views::filter(
        readings,
        [](int v) { return v >= 50 && v <= 300; }
    );

    // 迭代视图
    for (int v : valid_readings) {
        // v会是:120, 230, 67, 89, 56, 180(45和340被过滤)
        process_reading(v);
    }

    // 原始readings没有被修改,也没有创建新vector
}

transform:转换每个元素

std::views::transform对每个元素应用一个函数:

void transform_example() {
    std::vector<int> raw_values = {100, 150, 200, 250};

    // 创建转换视图:将ADC原始值转换为电压
    auto voltages = std::views::transform(
        raw_values,
        [](int adc) { return adc * 3.3f / 4095; }  // 12位ADC,3.3V参考
    );

    for (float v : voltages) {
        // v会是转换后的电压值
    }
}
查看完整可编译示例
// Filter Views - Selecting Elements with Predicates
// Demonstrates std::views::filter for data filtering

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

void demo_basic_filter() {
    std::cout << "=== Basic Filter ===" << std::endl;

    std::vector<int> readings = {12, 45, 23, 67, 34, 89, 56};

    // Filter for values > 50
    auto high_values = std::views::filter(readings, [](int v) {
        return v > 50;
    });

    std::cout << "Values > 50: ";
    for (int v : high_values) {
        std::cout << v << " ";
    }
    std::cout << std::endl;
}

void demo_range_filter() {
    std::cout << "\n=== Filter with Range ===" << std::endl;

    std::vector<int> data = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

    // Filter even numbers
    auto evens = std::views::filter(data, [](int x) { return x % 2 == 0; });

    std::cout << "Even numbers: ";
    for (int n : evens) {
        std::cout << n << " ";
    }
    std::cout << std::endl;
}

void demo_combined_filter() {
    std::cout << "\n=== Combined Filter ===" << std::endl;

    std::vector<int> readings = {12, 45, 230, 67, 340, 89, 56, 180};

    // Chain multiple filters
    auto valid = readings
        | std::views::filter([](int v) { return v >= 50; })
        | std::views::filter([](int v) { return v <= 300; });

    std::cout << "Valid readings (50-300): ";
    for (int v : valid) {
        std::cout << v << " ";
    }
    std::cout << std::endl;
}

void demo_embedded_adc_filtering() {
    std::cout << "\n=== Embedded: ADC Filtering ===" << std::endl;

    // Simulated ADC readings with some invalid values
    std::vector<uint16_t> adc_readings = {
        100, 4500,  // Invalid (> 4095)
        2048, 2100,
        0,          // Invalid (too low)
        2000, 2050,
        4096,       // Invalid
        1980
    };

    // Filter for valid ADC range (100-4095)
    auto valid_readings = std::views::filter(adc_readings, [](uint16_t v) {
        return v >= 100 && v <= 4095;
    });

    std::cout << "Valid ADC readings: ";
    for (uint16_t v : valid_readings) {
        std::cout << v << " ";
    }
    std::cout << std::endl;

    // Count invalid readings
    auto invalid_readings = std::views::filter(adc_readings, [](uint16_t v) {
        return v < 100 || v > 4095;
    });

    size_t invalid_count = 0;
    for ([[maybe_unused]] auto v : invalid_readings) {
        invalid_count++;
    }
    std::cout << "Invalid readings: " << invalid_count << std::endl;
}

void demo_filter_with_capture() {
    std::cout << "\n=== Filter with Capture ===" << std::endl;

    std::vector<int> data = {10, 20, 30, 40, 50};

    int threshold = 25;

    auto above_threshold = std::views::filter(data, [threshold](int x) {
        return x > threshold;
    });

    std::cout << "Values > " << threshold << ": ";
    for (int x : above_threshold) {
        std::cout << x << " ";
    }
    std::cout << std::endl;
}

void demo_filter_mutating() {
    std::cout << "\n=== Filter View Modification ===" << std::endl;

    std::vector<int> data = {1, 2, 3, 4, 5};

    // Note: Filter view doesn't allow direct modification
    // But you can iterate and modify underlying data
    auto is_even = [](int x) { return x % 2 == 0; };
    auto evens = std::views::filter(data, is_even);

    std::cout << "Even numbers: ";
    for (int& x : data) {  // Iterate original, check condition manually
        if (is_even(x)) {
            std::cout << x << " ";
            x *= 10;  // Modify
        }
    }
    std::cout << std::endl;

    std::cout << "After modification: ";
    for (int x : data) {
        std::cout << x << " ";
    }
    std::cout << std::endl;
}

void demo_filter_with_indices() {
    std::cout << "\n=== Filter with Indices ===" << std::endl;

    std::vector<int> data = {10, 20, 30, 40, 50};

    // Use iota to get indices, then filter
    auto indices = std::views::iota(size_t{0}, data.size());
    auto selected_indices = std::views::filter(indices, [&](size_t i) {
        return data[i] > 25;
    });

    std::cout << "Indices of values > 25: ";
    for (size_t idx : selected_indices) {
        std::cout << idx << "(" << data[idx] << ") ";
    }
    std::cout << std::endl;
}

int main() {
    demo_basic_filter();
    demo_range_filter();
    demo_combined_filter();
    demo_embedded_adc_filtering();
    demo_filter_with_capture();
    demo_filter_mutating();
    demo_filter_with_indices();

    std::cout << "\n=== Key Points ===" << std::endl;
    std::cout << "- Filter views select elements matching a predicate" << std::endl;
    std::cout << "- Multiple filters can be chained with |" << std::endl;
    std::cout << "- No temporary storage needed" << std::endl;
    std::cout << "- Perfect for sensor data validation" << std::endl;

    return 0;
}

take和drop:取前N个或跳过前N个

void take_drop_example() {
    std::vector<int> data = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

    // 取前5个元素
    auto first_five = std::views::take(data, 5);  // {1, 2, 3, 4, 5}

    // 跳过前3个元素,然后取剩下的
    auto after_skip = std::views::drop(data, 3);  // {4, 5, 6, 7, 8, 9, 10}

    // 组合使用:跳过前2个,再取4个
    auto middle = std::views::take(std::views::drop(data, 2), 4);  // {3, 4, 5, 6}
}

在嵌入式场景里,这在处理协议头的时候特别有用:

void parse_packet(std::span<const uint8_t> packet) {
    // 跳过2字节头部,取接下来的数据部分
    auto payload = std::views::drop(packet, 2);

    // 再取最后4字节作为CRC(假设CRC在末尾)
    size_t payload_size = packet.size() - 2 - 4;
    auto data = std::views::take(payload, payload_size);

    // 处理data...
}
查看完整可编译示例
// Protocol Parsing with Ranges Pipelines
// Demonstrates parsing binary protocols using ranges

#include <iostream>
#include <ranges>
#include <vector>
#include <cstdint>
#include <iomanip>

// Simulated SPI data reception (16-bit big-endian words)
std::vector<uint8_t> receive_spi_data() {
    return {0x01, 0x00,  // 0x0100
            0x00, 0x64,  // 0x0064
            0x00, 0x02,  // 0x0002
            0xFF, 0xFF};  // 0xFFFF (padding)
}

void demo_spi_parsing() {
    std::cout << "=== SPI Protocol Parsing ===" << std::endl;

    auto data = receive_spi_data();

    // Chunk into pairs
    auto chunks = data | std::views::chunk(2);

    std::cout << "Parsed words:" << std::endl;
    for (auto chunk : chunks) {
        if (chunk.size() == 2) {
            uint16_t high = chunk[0];
            uint16_t low = chunk[1];
            uint16_t word = (high << 8) | low;
            std::cout << "  0x" << std::hex << std::setw(4) << std::setfill('0') << word << std::dec << std::endl;
        }
    }
}

void demo_spi_filter_padding() {
    std::cout << "\n=== SPI with Padding Filter ===" << std::endl;

    auto data = receive_spi_data();

    // Chunk, convert to 16-bit, filter out padding
    auto valid_words = data
        | std::views::chunk(2)
        | std::views::transform([](auto chunk) {
            uint16_t high = chunk[0];
            uint16_t low = chunk[1];
            return (high << 8) | low;
        })
        | std::views::filter([](uint16_t w) { return w != 0xFFFF; });

    std::cout << "Valid words (no padding):" << std::endl;
    for (uint16_t w : valid_words) {
        std::cout << "  0x" << std::hex << w << std::dec << " (" << w << ")" << std::endl;
    }
}

void demo_packet_structure() {
    std::cout << "\n=== Structured Packet Parsing ===" << std::endl;

    // Packet: [CMD(1)] [LEN(1)] [DATA(N)] [CRC(2)]
    std::vector<uint8_t> packet = {
        0x01,        // Command
        0x04,        // Length
        0x10, 0x20, 0x30, 0x40,  // Data
        0xAA, 0xBB   // CRC
    };

    auto cmd = packet[0];
    auto len = packet[1];

    auto payload = std::views::drop(packet, 2);
    auto data = std::views::take(payload, len);
    auto crc = std::views::drop(payload, len);

    std::cout << "Command: 0x" << std::hex << static_cast<int>(cmd) << std::dec << std::endl;
    std::cout << "Length: " << static_cast<int>(len) << std::endl;
    std::cout << "Data: ";
    for (uint8_t b : data) {
        std::cout << std::hex << "0x" << static_cast<int>(b) << " " << std::dec;
    }
    std::cout << std::endl;
    std::cout << "CRC: ";
    for (uint8_t b : crc) {
        std::cout << std::hex << "0x" << static_cast<int>(b) << " " << std::dec;
    }
    std::cout << std::endl;
}

void demo_nmea_sentence() {
    std::cout << "\n=== NMEA Sentence Parsing ===" << std::endl;

    std::string nmea = "$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47";

    // Remove checksum for demo
    auto clean = std::views::take(nmea, nmea.find('*'));

    // Split by comma
    auto parts = std::views::split(clean, ',');

    const char* field_names[] = {"Type", "Time", "Lat", "NS", "Lon", "EW", "Quality", "Sats"};
    int idx = 0;

    for (auto part : parts) {
        std::string_view sv(part.begin(), part.end());
        if (idx < 8) {
            std::cout << "  " << field_names[idx] << ": " << sv << std::endl;
        }
        idx++;
        if (idx >= 8) break;
    }
}

void demo_variable_length_packets() {
    std::cout << "\n=== Variable Length Packets ===" << std::endl;

    // Stream with multiple packets
    std::vector<uint8_t> stream = {
        // Packet 1
        0xAA, 0x02, 0x10, 0x20,
        // Packet 2
        0xAA, 0x03, 0x11, 0x22, 0x33,
        // Packet 3
        0xAA, 0x01, 0x40
    };

    size_t pos = 0;
    while (pos < stream.size()) {
        // Check for sync byte
        if (stream[pos] == 0xAA) {
            if (pos + 1 < stream.size()) {
                uint8_t len = stream[pos + 1];

                if (pos + 2 + len <= stream.size()) {
                    std::cout << "Packet at pos " << pos << ", len " << static_cast<int>(len) << ": ";

                    auto packet_data = std::views::drop(stream, pos + 2)
                                     | std::views::take(len);

                    for (uint8_t b : packet_data) {
                        std::cout << std::hex << "0x" << static_cast<int>(b) << " " << std::dec;
                    }
                    std::cout << std::endl;

                    pos += 2 + len;
                    continue;
                }
            }
        }
        pos++;
    }
}

void demo_hex_dump_pipeline() {
    std::cout << "\n=== Hex Dump Pipeline ===" << std::endl;

    std::vector<uint8_t> data = {0x01, 0x02, 0x03, 0x04, 0x05};

    // Format as hex dump
    auto hex_format = data | std::views::transform([](uint8_t b) {
        char buf[8];
        snprintf(buf, sizeof(buf), "0x%02X", b);
        return std::string(buf);
    });

    std::cout << "Hex dump: ";
    for (const auto& s : hex_format) {
        std::cout << s << " ";
    }
    std::cout << std::endl;
}

int main() {
    demo_spi_parsing();
    demo_spi_filter_padding();
    demo_packet_structure();
    demo_nmea_sentence();
    demo_variable_length_packets();
    demo_hex_dump_pipeline();

    std::cout << "\n=== Key Points ===" << std::endl;
    std::cout << "- chunk(n) splits range into n-element subranges" << std::endl;
    std::cout << "- Pipelines allow complex parsing in readable form" << std::endl;
    std::cout << "- No temporary storage for intermediate results" << std::endl;
    std::cout << "- Perfect for embedded protocol parsing" << std::endl;

    return 0;
}

split:按分隔符切分

std::views::split把一个Range按分隔符切分成多个子Range:

#include <ranges>
#include <string>
#include <iostream>

void split_example() {
    std::string data = "sensor1=25,sensor2=30,sensor3=28";

    // 按逗号切分
    auto fields = std::views::split(data, ',');

    for (auto field : fields) {
        // field是一个子Range,不是string
        // 可以把它转成string_view使用
        std::string_view field_sv(field.begin(), field.end());
        // field_sv依次是:"sensor1=25", "sensor2=30", "sensor3=28"
    }
}

解析NMEA语句(GPS数据格式)时特别好用:

void parse_nmea(std::string_view line) {
    // NMEA格式:$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47
    auto parts = std::views::split(line, ',');

    // parts[0]是"$GPGGA",parts[1]是时间"123519",以此类推
}

iota:生成序列

std::views::iota生成一个递增的序列:

void iota_example() {
    // 生成0到9的序列
    auto numbers = std::views::iota(0, 10);  // [0, 10)

    for (int n : numbers) {
        // 0, 1, 2, ..., 9
    }

    // 生成ADC通道编号序列
    auto adc_channels = std::views::iota(0, 16);  // 通道0-15
    for (int ch : adc_channels) {
        adc_read(ch);
    }
}

组合视图:开始构建管道

单个视图威力有限,但组合起来就强大了。我们可以用管道操作符|来把视图串联起来(这部分下一章会详细讲,这里先预热一下):

void composition_example() {
    std::vector<int> readings = {120, 45, 230, 67, 340, 89, 56, 180};

    // 过滤异常值,然后转换为电压,最后取前5个
    auto result = readings
        | std::views::filter([](int v) { return v >= 50 && v <= 300; })
        | std::views::transform([](int v) { return v * 3.3f / 4095; })
        | std::views::take(5);

    for (float v : result) {
        // v是处理后的前5个有效读数的电压值
    }
}

这段代码读起来就像一句话:"从readings中过滤出有效值,转换为电压,取前5个"。没有临时变量,没有中间容器,逻辑清晰得令人感动。


嵌入式实战:传感器数据处理流水线

让我们用一个实际的嵌入式场景来演示视图的威力。假设我们在做温度监控系统,有一组温度传感器,需要:

  1. 过滤掉无效读数(<-50或>150)
  2. 摄氏度转华氏度
  3. 计算移动平均
  4. 输出结果
#include <ranges>
#include <vector>
#include <iostream>
#include <numeric>

class TemperatureMonitor {
public:
    void add_reading(int celsius) {
        readings_.push_back(celsius);

        // 保持最近100个读数
        if (readings_.size() > 100) {
            readings_.erase(readings_.begin());
        }
    }

    void process_and_report() {
        // 构建处理流水线
        auto processed = readings_
            | std::views::filter([](int t) {
                return t >= -50 && t <= 150;  // 过滤无效值
            })
            | std::views::transform([](int t) {
                return t * 9.0f / 5.0f + 32.0f;  // 摄氏度转华氏度
            });

        // 计算平均值
        float sum = 0.0f;
        int count = 0;
        for (float f : processed) {
            sum += f;
            count++;
        }

        if (count > 0) {
            float avg = sum / count;
            report_temperature(avg);
        }
    }

private:
    std::vector<int> readings_;

    void report_temperature(float fahrenheit) {
        // 实际项目中这里会通过串口输出或显示
        std::cout << "Average temp: " << fahrenheit << " F\n";
    }
};

注意这段代码的美妙之处:

  • 没有filtered_readingsfahrenheit_readings这种临时容器
  • 整个处理过程只遍历数据一次
  • 内存开销是O(1)——视图只存几个指针
查看完整可编译示例
// Complete Sensor Data Processing Pipeline
// Demonstrates a real embedded scenario using Ranges views

#include <iostream>
#include <ranges>
#include <vector>
#include <cmath>
#include <iomanip>

// Sensor reading structure
struct SensorReading {
    uint8_t sensor_id;
    uint16_t raw_value;
    bool valid;

    float to_voltage() const {
        return raw_value * 3.3f / 4095.0f;
    }

    float to_celsius() const {
        // LM35: 10mV per degree C
        return to_voltage() * 100.0f;
    }

    float to_fahrenheit(float celsius) const {
        return celsius * 9.0f / 5.0f + 32.0f;
    }
};

std::vector<SensorReading> get_readings() {
    return {
        {1, 120, true},
        {2, 45, false},   // Invalid
        {3, 230, true},
        {4, 67, true},
        {5, 340, false},  // Invalid (> 300)
        {6, 89, true},
        {7, 4100, true},  // Invalid (> 4095)
        {8, 56, true}
    };
}

void demo_basic_pipeline() {
    std::cout << "=== Basic Sensor Pipeline ===" << std::endl;

    auto readings = get_readings();

    // Pipeline: filter valid -> convert to celsius -> filter reasonable temps
    auto valid_temps = readings
        | std::views::filter([](const SensorReading& r) { return r.valid; })
        | std::views::transform([](const SensorReading& r) { return r.to_celsius(); })
        | std::views::filter([](float t) { return t >= -50 && t <= 150; });

    std::cout << "Valid temperatures (C): ";
    for (float t : valid_temps) {
        std::cout << std::fixed << std::setprecision(1) << t << " ";
    }
    std::cout << std::endl;
}

void demo_fahrenheit_pipeline() {
    std::cout << "\n=== Fahrenheit Pipeline ===" << std::endl;

    auto readings = get_readings();

    // Pipeline: valid readings -> celsius -> fahrenheit -> display
    auto display_data = readings
        | std::views::filter([](const SensorReading& r) { return r.valid; })
        | std::views::transform([](const SensorReading& r) {
            float c = r.to_celsius();
            return std::make_pair(r.sensor_id, c * 9.0f / 5.0f + 32.0f);
        })
        | std::views::filter([](const auto& p) {
            return p.second >= -58 && p.second <= 302;  // Valid F range
        });

    std::cout << "Sensor readings (F):" << std::endl;
    for (const auto& [id, temp_f] : display_data) {
        std::cout << "  Sensor " << static_cast<int>(id) << ": "
                  << std::fixed << std::setprecision(1) << temp_f << " F" << std::endl;
    }
}

void demo_voltage_display() {
    std::cout << "\n=== Voltage Display ===" << std::endl;

    auto readings = get_readings();

    // Show voltage for valid readings
    auto voltages = readings
        | std::views::filter([](const SensorReading& r) {
            return r.valid && r.raw_value >= 100 && r.raw_value <= 4000;
        })
        | std::views::transform([](const SensorReading& r) {
            return std::make_tuple(r.sensor_id, r.to_voltage());
        });

    std::cout << "Sensor voltages:" << std::endl;
    for (const auto& [id, voltage] : voltages) {
        std::cout << "  Sensor " << static_cast<int>(id) << ": "
                  << std::fixed << std::setprecision(3) << voltage << " V" << std::endl;
    }
}

void demo_statistics() {
    std::cout << "\n=== Statistics Pipeline ===" << std::endl;

    auto readings = get_readings();

    // Get valid celsius readings for statistics
    auto temps = readings
        | std::views::filter([](const SensorReading& r) { return r.valid; })
        | std::views::transform([](const SensorReading& r) { return r.to_celsius(); });

    float sum = 0.0f;
    size_t count = 0;
    float min_val = 1000.0f;
    float max_val = -1000.0f;

    for (float t : temps) {
        sum += t;
        count++;
        min_val = std::min(min_val, t);
        max_val = std::max(max_val, t);
    }

    if (count > 0) {
        float avg = sum / count;
        std::cout << "Temperature statistics:" << std::endl;
        std::cout << "  Count: " << count << std::endl;
        std::cout << "  Min: " << min_val << " C" << std::endl;
        std::cout << "  Max: " << max_val << " C" << std::endl;
        std::cout << "  Avg: " << avg << " C" << std::endl;
    }
}

void demo_alarm_check() {
    std::cout << "\n=== Alarm Check ===" << std::endl;

    auto readings = get_readings();

    // Check for alarm conditions
    auto alarms = readings
        | std::views::filter([](const SensorReading& r) { return r.valid; })
        | std::views::transform([](const SensorReading& r) {
            return std::make_pair(r.sensor_id, r.to_celsius());
        })
        | std::views::filter([](const auto& p) {
            return p.second > 80;  // High temp alarm
        });

    std::cout << "High temperature alarms (> 80 C):" << std::endl;
    for (const auto& [id, temp] : alarms) {
        std::cout << "  ALARM: Sensor " << static_cast<int>(id)
                  << " at " << temp << " C" << std::endl;
    }
}

void demo_processing_chain() {
    std::cout << "\n=== Complete Processing Chain ===" << std::endl;

    auto readings = get_readings();

    // Full processing chain:
    // 1. Filter valid readings
    // 2. Filter reasonable ADC range
    // 3. Convert to celsius
    // 4. Filter reasonable temperature range
    // 5. Convert to fahrenheit
    // 6. Round to integer
    auto processed = readings
        | std::views::filter([](const SensorReading& r) { return r.valid; })
        | std::views::filter([](const SensorReading& r) {
            return r.raw_value >= 500 && r.raw_value <= 3500;
        })
        | std::views::transform([](const SensorReading& r) {
            return r.to_celsius();
        })
        | std::views::filter([](float c) {
            return c >= 0 && c <= 100;
        })
        | std::views::transform([](float c) {
            return static_cast<int>(c * 9.0f / 5.0f + 32.0f);
        });

    std::cout << "Processed values (int F): ";
    for (int f : processed) {
        std::cout << f << " F ";
    }
    std::cout << std::endl;
}

int main() {
    demo_basic_pipeline();
    demo_fahrenheit_pipeline();
    demo_voltage_display();
    demo_statistics();
    demo_alarm_check();
    demo_processing_chain();

    std::cout << "\n=== Key Points ===" << std::endl;
    std::cout << "- Views compose naturally into pipelines" << std::endl;
    std::cout << "- No temporary allocations" << std::endl;
    std::cout << "- Single pass through data" << std::endl;
    std::cout << "- Perfect for embedded sensor processing" << std::endl;

    return 0;
}

视图 vs 容器:什么时候用什么

视图虽然强大,但不是万能的。这里有个简单的决策树:

用视图(View)的情况:

  • 只读数据,不需要修改
  • 需要组合多个操作
  • 想要零开销拷贝
  • 数据源生命周期足够长
  • 一次性遍历

用容器(Container)的情况:

  • 需要修改数据
  • 需要多次遍历同一结果
  • 数据源可能被销毁
  • 需要拥有数据的所有权
// 场景1:用视图——一次性转换输出
void report_filtered(const std::vector<int>& data) {
    auto filtered = data | std::views::filter([](int x) { return x > 0; });
    for (int x : filtered) { output(x); }
    // filtered用完就丢,不需要保留
}

// 场景2:用容器——需要缓存结果多次使用
std::vector<int> get_valid_values(const std::vector<int>& data) {
    std::vector<int> result;
    for (int x : data | std::views::filter([](int x) { return x > 0; })) {
        result.push_back(x);
    }
    return result;  // 返回拥有的容器
}

避坑指南

坑1:视图的生命周期

视图不拥有数据,所以底层数据销毁了,视图就悬垂了:

// ❌ 危险:返回指向局部变量的视图
auto get_bad_view() {
    std::vector<int> local = {1, 2, 3};
    return std::views::filter(local, [](int x) { return x > 1; });
    // local被销毁,返回的视图悬垂!
}

// ✅ 正确:确保底层数据生命周期足够长
class DataHolder {
    std::vector<int> data_ = {1, 2, 3};
public:
    auto get_view() {
        return std::views::filter(data_, [](int x) { return x > 1; });
        // data_与对象同生命周期,安全
    }
};

坑2:迭代后失效

某些视图只能迭代一次,或者迭代后状态改变:

std::vector<int> data = {1, 2, 3, 4, 5};
auto filtered = data | std::views::filter([](int x) { return x > 2; });

// 第一次迭代
for (int x : filtered) { /* 输出 3, 4, 5 */ }

// 某些实现下第二次迭代可能有问题
// 虽然大多数现代实现没问题,但最好避免多次迭代同一视图

如果要多次迭代,考虑转成容器:

auto filtered_vec = filtered | std::ranges::to<std::vector<int>>();
// 现在可以多次迭代filtered_vec

坑3:视图的类型

视图的类型是复杂的模板实例化产物,别试图手动写它,用auto

// ❌ 别尝试写这个类型
std::ranges::filter_view<std::ranges::transform_view<...>> view = ...;

// ✅ 用auto
auto view = data | std::views::filter(...) | std::views::transform(...);

坏消息:不是所有编译器都完全支持

C++20的Ranges是新东西,一些老编译器可能支持不完整。GCC 11+、Clang 13+、MSVC 2019+基本没问题。如果你的编译器报错一堆模板错误,先检查版本。


小结

视图(View)是C++20 Ranges库的核心概念:

  • 懒惰求值:定义时不计算,迭代时才计算
  • 不拥有数据:只是底层数据的"透镜"
  • O(1)拷贝:到处传递视图无开销
  • 可组合:用管道操作符串联多个操作

对嵌入式开发来说,视图让我们既能写出优雅的数据处理代码,又能保持零开销的运行时性能。不用在"优雅代码"和"高效代码"之间做选择——我们全都要。

下一章,我们深入探讨管道操作符|的用法,以及更多Ranges的实战技巧。到时候你会看到,Unix管道的哲学是如何在C++中完美实现的。