Expression templates#

Why does a + b*s not allocate?

Because a + b*s does not compute a vector at all — it builds a tiny, cheap object describing the computation (an expression), and that description is only walked, element by element, at the moment you assign it into a real destination. Nothing is allocated, and nothing is computed, in between writing the expression and assigning it.

The expression base#

Every leaf (aether::Item, aether::View) and every composite node this library builds derives from aether::Expression (aether/expr/ Expression.h), a CRTP base that fixes a small, rank-generic protocol any conforming expression type exposes: an element_type (the type it stores), an element_extents shape, whether it isLeaf, and an eval<Is...>(SampleIndex) that computes ONE element at ONE sample, given the static element index Is.... The same protocol works whether Es... names a 3-vector or a 3x3 matrix — aether has no separate vector/matrix expression family the way some libraries do; one rank-generic Expression base covers both.

Operators build nodes, not values#

a + b, a - b, s * e, e * s and e / s (aether/expr/ Operations.h) do not touch a’s or b’s data — they construct a Sum or CWiseScale node (aether/expr/nodes/Arithmetic.h) that simply remembers its operands. (a + b) - s*a therefore builds a Sum of a Sum and a CWiseScale, three tiny objects deep, still without reading a single element. This is what lets you write ordinary arithmetic-looking C++ and get zero intermediate allocation: the compiler sees straight through the whole chain at compile time.

Where the work actually happens#

An expression only produces numbers when it is assigned into something — Item c = a + b; or view[i] = a + b;. That assignment is driven by aether::detail::RecursiveAssign (aether/expr/Assign.h), a compile-time recursive unroll over the destination’s element_extents that calls eval<Is...>(SampleIndex) on the expression tree once per element and writes the result into the destination — this is the ONE place an expression tree is actually walked. Every node computes in its working_type rather than its element_type (usually the same type and free of any extra cost — see Banded/emulated real arithmetic for the one case where they differ and why that indirection exists at all).

A runnable example#

TEST_F(ExprArithmeticTest, SumAddsComponentwise)
{
    Vec3d a, b;
    for (std::size_t i = 0; i < 3; ++i) {
        a(i) = static_cast<double>(i) + 1.0;
        b(i) = static_cast<double>(i) * 2.0 - 3.0;
    }
    Vec3d c = a + b;
    for (std::size_t i = 0; i < 3; ++i)
        EXPECT_DOUBLE_EQ(c(i), a(i) + b(i));
}

Vec3d c = a + b; is the assignment that actually runs the addition, component by component. Vector and matrix algebra cookbook builds on the exact same mechanism for cross products, dot products and matrix algebra.