C++ std--valarray 用法实例详解

发布时间:2026/9/29 15:29:41
C++ std--valarray 用法实例详解
前言std::valarray是 C98 就进入标准库的数值数组类numeric array头文件是valarray。它的设计目标非常明确面向数值计算让a b c * 2.0这样的整体数组运算能像标量一样写出来而不必手写for循环。但尴尬的是它成了标准库里最被冷落的组件之一。大多数教程里它只是一行了解一下即可原因有几个名字长得离谱std::valarray里就有两个a提供的接口和std::vector高度重叠很多人觉得直接vector就够了标准对它的实现策略几乎不做约束是否用表达式模板、是否 SIMD 化都随意导致写起来简洁但性能不一定赢没有迭代器严格说没有begin()/end()不能直接用std::sort等算法但valarray有几样vector完全没有的能力切片slice、掩码mask、间接索引indirect以及sum()、min()、max()、apply()这类数值接口。这些在矩阵行/列抽取、条件筛选场景里非常好用。本文把valarray的设计意图、核心接口、三种视图机制和真实踩坑一次讲透。一、它到底是什么设计意图与定位1.1 vector 是容器valarray 是数值维度std::vectorTstd::valarrayT设计目标通用序列容器数值计算迭代器有随机访问无标准迭代器能否用 STL 算法可以基本不行整体运算需要手写循环a b c直接写是否连续存储是保证不保证大小可变push_back等只能resize整体调整切片能力无slice/gslice数值聚合需accumulate自带sum()一句话vector管的是一堆元素valarray管的是一个数学向量。1.2 关键限制没有迭代器这是valarray最反直觉的地方。标准里valarray提供begin()/end()是C11 才加的而且返回的是T*只对非const版本可用const valarray没有begin()这是标准的一个著名瑕疵。更坑的是std::begin(v)/std::end(v)对valarray不可用没有重载所以基于范围推导的泛型代码对valarray一律失效。#include valarray #include algorithm #include iostream int main() { std::valarrayint v{5, 2, 8, 1}; // v.begin() 可用C11 起返回 int* std::sort(v.begin(), v.end()); // 可以但这是借了指针接口 std::cout v[0] \n; // 1 // std::begin(v) 编译不过 // std::sort(std::begin(v), std::end(v)); // ❌ 没有对应重载 }结论想把valarray当能排序的数组用那vector是更正确的选择想用它的数值语义和切片才轮到valarray出场。二、基础用法2.1 构造方式#include valarray #include iostream void show(const std::valarrayint v) { for (size_t i 0; i v.size(); i) std::cout v[i] ; std::cout \n; } int main() { std::valarrayint a; // 空 std::valarrayint b(5); // 5 个元素值初始化0 std::valarrayint c(7, 5); // 5 个元素全部为 7 std::valarrayint d{1, 2, 3, 4, 5}; // 初始化列表 std::valarrayint e(d); // 拷贝 int arr[] {9, 8, 7}; std::valarrayint f(arr, 3); // 从 C 数组拷贝 3 个 show(b); // 0 0 0 0 0 show(c); // 7 7 7 7 7 show(d); // 1 2 3 4 5 show(f); // 9 8 7 }注意std::valarrayint b(5)和std::valarrayint b{5}的差别 —— 前者是5 个 0后者是1 个 5。花括号初始化列表会优先匹配initializer_list构造函数这是 C11 之后所有容器共通的陷阱。2.2 整体算术这才是它的卖点#include valarray #include iostream int main() { std::valarraydouble a{1.0, 2.0, 3.0}; std::valarraydouble b{10.0, 20.0, 30.0}; std::valarraydouble c a b; // 逐元素加 std::valarraydouble d b - a; // 逐元素减 std::valarraydouble e a * b; // 逐元素乘不是点积 std::valarraydouble f b / a; // 逐元素除 // 与标量混合运算 std::valarraydouble g a * 2.0 1.0; for (double x : g) std::cout x ; // 3 5 7 std::cout \n; }关键认知a * b是逐元素乘法element-wise product也叫 Hadamard 积不是数学上的点积。点积要自己写或用std::inner_product但valarray没有迭代器所以需要绕路double dot(const std::valarraydouble a, const std::valarraydouble b) { return (a * b).sum(); // 逐元素乘完再求和 点积 }2.3 复合赋值与原地修改#include valarray int main() { std::valarrayint a{1, 2, 3, 4}; a 10; // 每个元素 10 a * 2; // 每个元素 *2 a - std::valarrayint{1, 1, 1, 1}; a / 3; // 每个元素 /3 a.resize(6, 0); // 变大新增元素填 0 a.resize(2); // 变小截断 }resize是整体操作等价于重新分配一个数组不是push_back。valarray没有push_back/pop_back元素个数只能通过resize一次性改变。2.4 数值聚合sum / min / max / apply#include valarray #include iostream int main() { std::valarrayint v{3, 1, 4, 1, 5, 9, 2, 6}; std::cout sum v.sum() \n; // 31 std::cout min v.min() \n; // 1 std::cout max v.max() \n; // 9 // apply对每个元素施加一元函数返回新的 valarray std::valarrayint sq v.apply([](int x) { return x * x; }); // 也支持传入函数指针C98 风格 std::valarraydouble r std::valarraydouble{0.0, 1.0, 4.0, 9.0} .apply(static_castdouble(*)(double)(std::sqrt)); for (int x : sq) std::cout x ; std::cout \n; for (double x : r) std::cout x ; std::cout \n; }注意apply接受的是一元函数且会返回一个同尺寸的新valarray不修改原对象。三、三种视图slice / gslice / mask / indirect这才是valarray真正区别于vector的地方。它们都返回代理对象proxy赋值时才会逐元素写回。3.1 slice等步长的等差数列下标#include valarray #include iostream int main() { std::valarrayint v(12); for (int i 0; i 12; i) v[i] i; // slice(起始下标, 元素个数, 步长) // 取下标 0,3,6,9 std::valarrayint s v[std::slice(0, 4, 3)]; for (int x : s) std::cout x ; // 0 3 6 9 std::cout \n; // 取奇数位下标 1,3,5,7,9,11 std::valarrayint odd v[std::slice(1, 6, 2)]; for (int x : odd) std::cout x ; // 1 3 5 7 9 11 std::cout \n; // slice_array 是可赋值的把偶数位全部置 0 v[std::slice(0, 6, 2)] 0; for (int x : v) std::cout x ; // 0 1 0 3 0 5 0 7 0 9 0 11 std::cout \n; }这是矩阵按行/列访问的经典用法。3×4 矩阵按行优先存储时第r行就是slice(r*4, 4, 1)第c列就是slice(c, 3, 4)。#include valarray #include iostream int main() { const int R 3, C 4; std::valarrayint mat(R * C); for (int i 0; i R * C; i) mat[i] i; auto print [](const char* name) { std::cout name :\n; for (int r 0; r R; r) { for (int c 0; c C; c) std::cout mat[r * C c] \t; std::cout \n; } }; print(原始); // 0 1 2 3 / 4 5 6 7 / 8 9 10 11 // 第 1 列下标 1,5,9整体 100 mat[std::slice(1, R, C)] 100; print(第 1 列 100); // 0 101 2 3 / 4 105 6 7 / 8 109 10 11 }3.2 gslice多维切片gslicegeneralized slice允许分段步长适合抽取多维数组的一块区域。#include valarray #include iostream int main() { std::valarrayint v(24); for (int i 0; i 24; i) v[i] i; // 把一维数组看成 4x6 的二维取 2x3 的子块 // 起始 0长度 {2,3}步长 {6,1} // 行 0 的第 0..2 列 行 1 的第 0..2 列 std::valarrayint sub v[std::gslice(0, {2, 3}, {6, 1})]; for (int x : sub) std::cout x ; // 0 1 2 6 7 8 std::cout \n; // 取偶数行0、2 行的全部 6 列 std::valarrayint rows v[std::gslice(0, {2, 6}, {12, 1})]; for (int x : rows) std::cout x ; // 0..5 和 12..17 std::cout \n; }gslice的语义结果的下标序列是起始 各维步长的组合一维结果按行优先展开。3.3 mask_array布尔掩码筛选#include valarray #include iostream int main() { std::valarrayint v{5, -3, 8, -1, 0, 7}; // 掩码长度必须等于 valarray 长度 std::valarraybool mask (v 0); std::valarrayint pos v[mask]; for (int x : pos) std::cout x ; // 5 8 7 std::cout \n; // 掩码赋值把所有负数置 0 v[v 0] 0; for (int x : v) std::cout x ; // 5 0 8 0 0 7 std::cout \n; }(v 0)这种比较会逐元素比较并返回valarraybool非常直观。这是valarray里最舒服的一处设计。3.4 indirect_array按给定下标抽取#include valarray #include iostream int main() { std::valarrayint v{10, 20, 30, 40, 50}; // 取出下标 4、0、2 的元素 std::valarraysize_t idx{4, 0, 2}; std::valarrayint picked v[idx]; for (int x : picked) std::cout x ; // 50 10 30 std::cout \n; // 反向写回 v[idx] std::valarrayint{1, 2, 3}; for (int x : v) std::cout x ; // 2 20 3 40 1 std::cout \n; }这相当于numpy里的 fancy indexing用它实现按索引置换gather/scatter很方便。四、完整实战一个简易统计器#include valarray #include iostream #include cmath int main() { std::valarraydouble data{2.5, 3.1, -1.0, 4.7, 0.0, 8.2, 5.5}; const double n static_castdouble(data.size()); const double mean data.sum() / n; // 方差E[(x - u)^2] std::valarraydouble dev data - mean; const double var (dev * dev).sum() / n; const double sd std::sqrt(var); std::cout 均值 mean \n; std::cout 方差 var \n; std::cout 标准差 sd \n; // 标准化 z-score std::valarraydouble z dev / sd; std::cout z-score: ; for (double x : z) std::cout x ; std::cout \n; // 挑出超过 1 个标准差的样本 std::valarraydouble outliers data[std::abs(z) 1.0]; std::cout 离群点: ; for (double x : outliers) std::cout x ; std::cout \n; }这段代码最能体现valarray的价值data - mean、dev * dev、data[mask]全都是一行一个数学式子可读性远高于手写嵌套循环。编译g -stdc17 -O2 stats.cpp -o stats ./stats常见坑点坑 1v[i]不做边界检查❌ 错误写法std::valarrayint v(3); v[10] 1; // 越界未定义行为operator[]只做调试断言且很多实现根本不检查。valarray不提供at()这是它和其他容器的一大差异。✅ 正确写法自己保证下标合法或用size()显式校验。if (i v.size()) v[i] 1;坑 2切片的偏移与步长顺序记反slice(start, size, stride)的参数顺序容易记混。❌ 错误写法// 想取 0,3,6,9却写成了 (0, 3, 4) std::valarrayint s v[std::slice(0, 3, 4)]; // 实际取 0,4,8✅ 正确写法// start0, count4, stride3 std::valarrayint s v[std::slice(0, 4, 3)]; // 0,3,6,9记忆法先定从哪开始再定要几个最后定每隔几跳。坑 3切片越界不会报错std::valarrayint v(10); // start8, count5, stride1 → 会访问 v[8..12]越界 std::valarrayint s v[std::slice(8, 5, 1)]; // 未定义行为最大合法下标是start (count - 1) * stride必须 size()。写循环切矩阵时尤其容易算错边界。// 安全公式 const size_t last start (count - 1) * stride; if (last v.size()) { /* 可以切 */ }坑 4掩码长度不匹配❌ 错误写法std::valarrayint v{1, 2, 3, 4}; std::valarraybool mask{true, false}; // 长度 2不匹配 std::valarrayint r v[mask]; // 未定义行为C11 起才明确要求掩码长度必须等于v.size()不匹配就是 UB。✅ 正确写法让掩码由原数组比较生成长度天然一致。std::valarrayint r v[v 2]; // 长度自动对齐坑 5sum()/min()/max()在空数组上调用std::valarrayint empty; std::cout empty.sum(); // 0这个还行 std::cout empty.min(); // 未定义行为 std::cout empty.max(); // 未定义行为sum()对空数组返回T()但min()/max()在空数组上是 UB。使用前必须检查!v.size()。坑 6代理对象寿命问题v[std::slice(...)]返回的是slice_array它内部只保存了指向v的指针和下标信息是一个轻量代理。❌ 错误写法auto s v[std::slice(0, 3, 1)]; // 这里 v 被析构了比如离开作用域 s 0; // 悬空指针UB✅ 正确写法std::valarrayint s v[std::slice(0, 3, 1)]; // 立刻拷贝出真实数据 s 0; // 改的是副本不影响 v注意这里还有第二层语义差异auto绑定时s 0会改原数组拷贝成valarray后s 0只改副本。想改原数组就直接写v[slice] 0。坑 7以为valarray一定比手写循环快标准不要求实现使用表达式模板expression templates。a b c d在朴素实现下会产生多个临时valarray// 朴素实现可能展开成 std::valarraydouble t1 b c; // 临时数组 1 std::valarraydouble t2 t1 d; // 临时数组 2 a t2; // 拷贝MSVC STL 的valarray就基本不做表达式模板优化而 libstdc 做得更激进。同一份代码在不同编译器上性能可能差几倍。✅ 建议性能敏感的场景先实测再决定或者把长表达式拆开控制临时对象数量。g -stdc17 -O2 -DNDEBUG bench.cpp -o bench坑 8跨类型混算导致意外的类型提升std::valarrayint a{1, 2, 3}; std::valarraydouble b{0.5, 0.5, 0.5}; // auto 推出来的可能是 std::valarraydouble也可能因实现而异 auto c a * b;由于valarray的运算符返回类型依赖实现尤其是valarraybool和混合类型尽量显式写出目标类型别过度依赖auto推导。std::valarraydouble c a * b; // 意图明确总结能力valarray说明逐元素算术内置运算符a b、a * 2.0聚合sum/min/max空数组上min/max是 UB变换apply(f)一元函数返回新对象等步长切片slice矩阵行列访问多维切片gslice子块抽取条件筛选mask_arrayv[v 0]索引抽取indirect_arrayfancy indexing迭代器仅begin/end非 const无法配合 STL 算法边界检查无没有at()什么时候该用它你在做数值计算代码里大量出现整体加减乘除和按行/列/条件取子集那么valarray写出来的代码比vector短且更贴合数学表达。什么时候别用它你需要push_back、需要排序/查找算法、需要迭代器、需要可预测的内存布局 —— 那std::vector是更好的答案。valarray是一个用对场景才好用的工具理解它的边界比记住它的接口更重要。