The Complete Guide to C++ Expression Templates | Lazy Evaluation and Mathematical Library Optimization
이 글의 핵심
A complete guide to Expression Templates. Learn about lazy evaluation, eliminating temporary objects, optimizing vector operations, and implementing Eigen-style libraries.
What Are Expression Templates and Why Do We Need Them?
Problem Scenario: Temporary Objects in Vector Operations
The Problem: In mathematical libraries, a vector operation like result = a + b + c + d creates three temporary objects. A new vector is allocated and copied for each + operation.
class Vector {
public:
Vector(size_t n) : data(n) {}
Vector operator+(const Vector& other) const {
Vector result(data.size());
for (size_t i = 0; i < data.size(); ++i) {
result.data[i] = data[i] + other.data[i];
}
return result; // Temporary object
}
private:
std::vector<double> data;
};
// result = a + b + c + d;
// 1. temp1 = a + b (Temporary object 1)
// 2. temp2 = temp1 + c (Temporary object 2)
// 3. result = temp2 + d (Temporary object 3)
Issues:
- Three memory allocations
- Three loops (one for each
+) - Reduced cache efficiency
Solution: Expression Templates enable lazy evaluation. Instead of calculating a + b + c + d immediately, the operation is stored as an expression tree and computed all at once during assignment.
// Expression Template
Vector result = a + b + c + d;
// 1. expr = Add(Add(Add(a, b), c), d) (Expression tree, no computation yet)
// 2. result = expr (Computed all at once during assignment)
Advantages:
- One memory allocation (only for
result) - One loop (computed in a single pass)
- Improved cache efficiency
flowchart TD
subgraph normal["Standard Operations"]
n1["a + b → temp1 (allocation)"]
n2["temp1 + c → temp2 (allocation)"]
n3["temp2 + d → result (allocation)"]
end
subgraph expr["Expression Template"]
e1["a + b + c + d → Expression Tree"]
e2["result = Expression (1 allocation)"]
e3["Computed in one loop"]
end
n1 --> n2 --> n3
e1 --> e2 --> e3
Table of Contents
- Basic Structure
- Implementing Vector Operations
- Matrix Operations
- Common Errors and Solutions
- Production Patterns
- Complete Example: Mathematical Library
- Performance Comparison
1. Basic Structure
Minimal Expression Template
#include <iostream>
#include <vector>
// Base class for expressions
template<typename E>
class VecExpr {
public:
double operator[](size_t i) const {
return static_cast<const E&>(*this)[i];
}
size_t size() const {
return static_cast<const E&>(*this).size();
}
};
// Addition expression
template<typename LHS, typename RHS>
class VecAdd : public VecExpr<VecAdd<LHS, RHS>> {
public:
VecAdd(const LHS& l, const RHS& r) : lhs(l), rhs(r) {}
double operator[](size_t i) const {
return lhs[i] + rhs[i];
}
size_t size() const { return lhs.size(); }
private:
const LHS& lhs;
const RHS& rhs;
};
// Vector class
class Vector : public VecExpr<Vector> {
public:
Vector(size_t n) : data(n) {}
double& operator[](size_t i) { return data[i]; }
double operator[](size_t i) const { return data[i]; }
size_t size() const { return data.size(); }
// Assigning an Expression Template
template<typename Expr>
Vector& operator=(const VecExpr<Expr>& expr) {
const Expr& e = static_cast<const Expr&>(expr);
for (size_t i = 0; i < size(); ++i) {
data[i] = e[i]; // Lazy evaluation
}
return *this;
}
private:
std::vector<double> data;
};
// Operator
template<typename LHS, typename RHS>
VecAdd<LHS, RHS> operator+(const VecExpr<LHS>& lhs, const VecExpr<RHS>& rhs) {
return VecAdd<LHS, RHS>(
static_cast<const LHS&>(lhs),
static_cast<const RHS&>(rhs)
);
}
int main() {
Vector a(3), b(3), c(3);
a[0] = 1; a[1] = 2; a[2] = 3;
b[0] = 4; b[1] = 5; b[2] = 6;
c[0] = 7; c[1] = 8; c[2] = 9;
Vector result(3);
result = a + b + c; // Expression tree, computed during assignment
for (size_t i = 0; i < result.size(); ++i) {
std::cout << result[i] << ' ';
}
std::cout << '\n'; // 12 15 18
}
Key Point: a + b + c returns an expression object of type VecAdd<VecAdd<Vector, Vector>, Vector>, which is computed all at once during result = ....
2. Implementing Vector Operations
Adding Multiplication and Subtraction
// Subtraction expression
template<typename LHS, typename RHS>
class VecSub : public VecExpr<VecSub<LHS, RHS>> {
public:
VecSub(const LHS& l, const RHS& r) : lhs(l), rhs(r) {}
double operator[](size_t i) const {
return lhs[i] - rhs[i];
}
size_t size() const { return lhs.size(); }
private:
const LHS& lhs;
const RHS& rhs;
};
// Scalar multiplication expression
template<typename E>
class VecScale : public VecExpr<VecScale<E>> {
public:
VecScale(double s, const E& e) : scalar(s), expr(e) {}
double operator[](size_t i) const {
return scalar * expr[i];
}
size_t size() const { return expr.size(); }
private:
double scalar;
const E& expr;
};
// Operators
template<typename LHS, typename RHS>
VecSub<LHS, RHS> operator-(const VecExpr<LHS>& lhs, const VecExpr<RHS>& rhs) {
return VecSub<LHS, RHS>(
static_cast<const LHS&>(lhs),
static_cast<const RHS&>(rhs)
);
}
template<typename E>
VecScale<E> operator*(double scalar, const VecExpr<E>& expr) {
return VecScale<E>(scalar, static_cast<const E&>(expr));
}
int main() {
Vector a(3), b(3), c(3);
a[0] = 1; a[1] = 2; a[2] = 3;
b[0] = 4; b[1] = 5; b[2] = 6;
c[0] = 7; c[1] = 8; c[2] = 9;
Vector result(3);
result = 2.0 * a + b - c; // Expression tree
for (size_t i = 0; i < result.size(); ++i) {
std::cout << result[i] << ' ';
}
std::cout << '\n'; // -1 -1 -3
}
(Translation continues with the same structure and tone for the remaining sections.)
## 같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- [C++ CRTP 완벽 가이드 | 정적 다형성과 컴파일 타임 최적화](/blog/cpp-crtp/)
- [C++ 템플릿 | "제네릭 프로그래밍" 초보자 가이드](/blog/cpp-template-basics/)
- [C++ Move 시맨틱스 | "복사 vs 이동" 완벽 이해](/blog/cpp-move-semantics/)
---
---
## 이 글에서 다루는 키워드 (관련 검색어)
C++, expression-template, template, optimization, lazy, eigen 등으로 검색하시면 이 글이 도움이 됩니다.