#087 – Using std::valarray for high-performance numerics

std::valarray is the C++ standard library’s most ambitious attempt — and most public failure — at high-performance numerics. It was added to C++98 with a specific promise: a numerical array type with aliasing rules so strict that compilers could optimise operations on it the way Fortran optimises its arrays, generating tight SIMD code from expressions like a = b + c * d. The design was novel at the time, the API was carefully crafted, and the standard explicitly permitted compilers to make assumptions ordinary containers don’t.

Two and a half decades later, the consensus is well-summarised by the standard reference’s blunt admission: “the consensus in the C++ user community seems to be that the standard failed to live up to the intentions.” This Nibble walks through what valarray was supposed to be, why it didn’t work out, and what modern C++ uses instead.

Three things to take away:

  • std::valarray was designed around a no-aliasing guarantee that lets the compiler treat operations on it the way Fortran treats array operations — single fused loops, no temporaries, vectorisable.
  • Expression templates, invented around the time valarray was standardised, made the same optimisations available to any C++ class, and the numerical-computing community built Eigen, Blaze, and xtensor around them rather than around valarray.
  • For serious numerical code, prefer a dedicated library (Eigen, Blaze, Armadillo). std::valarray is a reasonable fallback for simple element-wise arithmetic when third-party dependencies aren’t available.

What valarray was meant to be

In C and C++, two pointers can in general alias — they might point to the same object or to overlapping objects. The compiler has to assume they do, which prevents many loop optimisations. Fortran’s array model forbids aliasing by default, which is why Fortran has historically dominated high-performance numerical computing: the compiler can fuse loops, hoist invariants, and emit SIMD code with full confidence that array writes don’t perturb subsequent reads.

std::valarray was designed to bring that property to C++. The standard says, paraphrased from cppreference: valarray and its helper classes are defined to be free of certain forms of aliasing, allowing operations to be optimised similar to the effect of C’s restrict keyword. An expression like v1 = a * v2 + v3 can be evaluated as a single fused loop — v1[i] = a * v2[i] + v3[i] — with no intermediate temporaries, no multiple passes, and good vectorisation potential.

The API is built around element-wise operations:

#include <valarray>

std::valarray<double> a = { 1.0, 2.0, 3.0, 4.0 };
std::valarray<double> b = { 5.0, 6.0, 7.0, 8.0 };

std::valarray<double> c = a + b;       // element-wise add
std::valarray<double> d = a * 2.0;     // scalar multiply
std::valarray<double> e = std::sin(a); // element-wise sin

double sum = a.sum();                  // reduction
double mx  = a.max();

Plus a family of slicing types — slicegslice, mask arrays, indirect arrays — for selecting subsequences with strides, masks, or arbitrary index lists. On paper, this is a complete numerical-array toolkit.

Why it didn’t work out

Three things conspired to make valarray the standard library’s least-loved numerical type.

Compilers never invested in valarray-specific optimisations. The aliasing guarantees are real, but exploiting them required compiler-vendor work that nobody did at scale. valarray operations on most implementations historically compiled to ordinary loops — sometimes auto-vectorised, often not — with no special treatment beyond what the same loops on std::vector would receive.

Expression templates obsoleted the design before it shipped broadly. The technique, popularised by Todd Veldhuizen’s 1995 work on Blitz++ and Bolwijn’s PETE library, lets a library author build types whose operators return placeholder expression objects rather than computed values. The expression tree is assembled at compile time and evaluated in a single fused loop only when assigned to a result. Crucially, this technique works for any C++ class — it doesn’t need language-level aliasing guarantees, doesn’t need compiler buy-in, and gives the library author full control over how the expression compiles. valarray‘s aliasing-based promise was solving a problem that template metaprogramming could solve from inside the library itself.

The API was incomplete. valarray originally had no iterators (added later, in C++11), can’t be used with most standard algorithms, and its slice types are awkward — they require constructing intermediate slice and gslice objects with bare integers for stride and length, with no compile-time checking. The numerical-computing community’s needs (matrices, tensors, linear-algebra operations) outgrew valarray‘s one-dimensional shape almost immediately.

The result, as cppreference puts it: “the majority of numeric libraries prefer expression templates to valarrays for flexibility.”

What modern C++ uses instead

For real numerical work, the C++ ecosystem standardised on a handful of expression-template libraries:

  • Eigen — the dominant general-purpose linear algebra library. Header-only, supports dense and sparse matrices, decompositions, and SIMD vectorisation. The default choice for most projects.
  • Blaze — performance-focused linear algebra; often benchmarks faster than Eigen on large operations, with a similar expression-template API.
  • Armadillo — MATLAB-like syntax over BLAS/LAPACK. Good when you want to call established Fortran solvers from C++.
  • xtensor — multi-dimensional arrays with NumPy-like syntax. Strong for scientific code that translates from Python.
  • std::mdspan (C++23) — a non-owning view over a multi-dimensional buffer. It’s a building block, not a computation library — pair it with one of the above for arithmetic.
  • std::simd (proposed for C++26) — explicit SIMD vectorisation primitives, the spiritual successor to valarray‘s ambition.

Each of these is more capable than valarray for the case they’re designed for. None of them are part of the standard library today (other than mdspan, which is a building block rather than a numerical library), so they introduce a third-party dependency. That’s the only reason a project might still reach for valarray over Eigen.

When valarray is still reasonable

A short list of cases where valarray is genuinely a reasonable choice:

  • Small, header-only programs that need element-wise arithmetic on a one-dimensional array and can’t take a third-party dependency. A 200-line numerical utility doesn’t benefit from pulling in Eigen.
  • Teaching / illustrative code where the standard library alone is the constraint and the numerical work is simple.
  • Standard library implementations that internally use expression templates. GNU libstdc++ and LLVM libc++ both do this, so on those toolchains valarray operations do fuse and vectorise reasonably well — not as well as a dedicated library, but not dramatically worse for simple expressions either.
  • Element-wise transforms with a quick syntax. Writing auto y = std::sin(x) * 2.0 + 1.0; over a valarray is more compact than the equivalent loop or the equivalent ranges pipeline.

What valarray is not good for: matrices, tensors, linear algebra, anything where performance must be predictable across toolchains, anything that needs to compose with the rest of the standard library (algorithms, ranges, parallel execution policies — valarray predates concepts and doesn’t model the range concept, even with iterators bolted on in C++11).

A pragmatic recommendation

For new code, treat valarray the way the rest of the community does: as a niche tool that solves a specific narrow problem reasonably well on most modern implementations. Reach for it when:

  1. The data is genuinely one-dimensional.
  2. The operations are element-wise arithmetic and reductions.
  3. A third-party dependency is undesirable.

Reach for Eigen (or one of its peers) when any of those conditions doesn’t hold — which, for production numerical code, is essentially always. The third-party dependency cost buys you decades of optimisation work, multi-dimensional support, linear algebra, and a community that maintains the library actively.

Takeaway

std::valarray was an ambitious design — the only standard library type with explicit aliasing guarantees for compiler optimisation — that arrived at the same time as expression templates obsoleted the approach. Compilers didn’t invest in valarray-specific optimisation, the API didn’t grow into multi-dimensional territory, and the numerical-computing community built Eigen, Blaze, and xtensor instead.

Modern implementations of valarray are reasonable for simple element-wise work on one-dimensional arrays, but for serious numerical code the right answer is a dedicated library. The rule is simple: valarray is the standard library’s plausible-but-niche numerical type — use it for what it does well, and reach for Eigen for everything else.