现代嵌入式C++教程——C++20范围库基础与视图¶
引言¶
每次处理数组、容器数据的时候,我总觉得少点什么。用STL算法吧,那些std::transform、std::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来存中间结果 - 逻辑被中间变量打断,不能一眼看懂"原始数据 → 过滤 → 校准"这条链路
- 内存分配至少两次(
filtered和calibrated)
在嵌入式场景下,这种临时内存分配尤其让人头疼——你确定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>头文件里的一整套新工具。
首先我们得区分两个概念:Range和View。
- Range(范围):所有可以迭代的玩意儿,包括
vector、array、原生数组 - 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个"。没有临时变量,没有中间容器,逻辑清晰得令人感动。
嵌入式实战:传感器数据处理流水线¶
让我们用一个实际的嵌入式场景来演示视图的威力。假设我们在做温度监控系统,有一组温度传感器,需要:
- 过滤掉无效读数(<-50或>150)
- 摄氏度转华氏度
- 计算移动平均
- 输出结果
#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_readings、fahrenheit_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 */ }
// 某些实现下第二次迭代可能有问题
// 虽然大多数现代实现没问题,但最好避免多次迭代同一视图
如果要多次迭代,考虑转成容器:
坑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++中完美实现的。