C++
Uttrycksmallar
Sök…
Grundläggande uttrycksmallar på elementvisa algebraiska uttryck
Introduktion och motivation
Uttrycksmallar ( benämnda ET: er i följande) är en kraftfull mallmetaprogrammeringsteknik som används för att påskynda beräkningar av ibland ganska dyra uttryck. Det används ofta inom olika domäner, till exempel vid implementering av linjära algebrabibliotek.
I det här exemplet ska du ta hänsyn till sammanhanget för linjära algebraiska beräkningar. Mer specifikt beräkningar som endast involverar elementvisa operationer . Denna typ av beräkningar är de mest grundläggande applikationerna för ET: er , och de fungerar som en bra introduktion till hur ET: er fungerar internt.
Låt oss titta på ett motiverande exempel. Tänk på beräkningen av uttrycket:
Vector vec_1, vec_2, vec_3;
// Initializing vec_1, vec_2 and vec_3.
Vector result = vec_1 + vec_2*vec_3;
Här för enkelhetens skull kommer jag att anta att klassen Vector
och operation + (vektor plus: elementmässigt plus operation) och operation * (här betyder vektorintern produkt: även elementmässig operation) båda är korrekt implementerade, som hur de borde vara, matematiskt.
I en konventionell implementering utan att använda ET: er (eller andra liknande tekniker) sker minst fem konstruktioner av Vector
instanser för att uppnå det slutliga result
:
- Tre fall som motsvarar
vec_1
,vec_2
ochvec_3
. - En temporär
Vector
instans_tmp
, representerande resultatet av_tmp = vec_2*vec_3;
. - Slutligen med korrekt användning av returvärdeoptimering , konstruktion av
result
iresult = vec_1 + _tmp;
.
Implementering med hjälp av ET: er kan eliminera skapandet av tillfälliga Vector _tmp
i 2, och därmed bara fyra konstruktioner av Vector
instanser. Mer intressant, tänk på följande uttryck som är mer komplex:
Vector result = vec_1 + (vec_2*vec3 + vec_1)*(vec_2 + vec_3*vec_1);
Det kommer också att finnas fyra konstruktioner av Vector
instanser totalt: vec_1, vec_2, vec_3
och result
. Med andra ord, i det här exemplet, där endast elementvisa operationer är involverade , är det garanterat att inga temporära objekt skapas från mellanliggande beräkningar .
Hur fungerar ET: er
I princip består ET: er för alla algebraiska beräkningar av två byggstenar:
- Rena algebraiska uttryck ( PAE ): de är proxyer / abstraktioner av algebraiska uttryck. En ren algebraik gör inte faktiska beräkningar, de är bara abstraktioner / modellering av beräkningens arbetsflöde. En PAE kan vara en modell av antingen ingången eller utgången från alla algebraiska uttryck . Instanser av PAE anses ofta vara billiga att kopiera.
- Lata utvärderingar : vilka är implementering av verkliga beräkningar. I följande exempel ser vi att för uttryck som endast involverar elementvisa operationer kan lata utvärderingar implementera faktiska beräkningar i den indexerade åtkomstoperationen på det slutliga resultatet och därmed skapa ett schema för utvärdering på begäran: en beräkning utförs inte endast om det slutliga resultatet nås / begärs.
Så, hur implementerar vi ET: er i detta exempel? Låt oss gå igenom det nu.
Tänk alltid på följande kodavsnitt:
Vector vec_1, vec_2, vec_3;
// Initializing vec_1, vec_2 and vec_3.
Vector result = vec_1 + vec_2*vec_3;
Uttrycket för att beräkna resultat kan sönderdelas ytterligare i två underuttryck:
- En vektor plus uttryck (betecknad som plus_expr )
- Ett vektor inre produktuttryck (betecknat innerprod_expr ).
Vad ET: er gör är följande:
Istället för att beräkna direkt varje deluttryck , modellerar ET: er först hela uttrycket med en grafisk struktur. Varje nod i diagrammet representerar en PAE . Nodans kantanslutning representerar det faktiska beräkningsflödet. Så för ovanstående uttryck får vi följande graf:
result = plus_expr( vec_1, innerprod_expr(vec_2, vec_3) ) / \ / \ / \ / innerprod_expr( vec_2, vec_3 ) / / \ / / \ / / \ vec_1 vec_2 vec_3
Den slutliga beräkningen genomförs genom att titta genom grafhierarkin : eftersom vi här bara har elementmässiga operationer kan beräkningen av varje indexerat värde i
result
göras oberoende : den slutliga utvärderingen avresult
kan latent skjutas upp till ett element- klok utvärdering av varjeresult
. Med andra ord, eftersom beräkningen av ettresult
,elem_res
, kan uttryckas med motsvarande element ivec_1
(elem_1
),vec_2
(elem_2
) ochvec_3
(elem_3
) som:elem_res = elem_1 + elem_2*elem_3;
det finns därför inget behov av att skapa en tillfällig Vector
att lagra resultatet av mellanprodukten: hela beräkningen för ett element kan göras helt och kodas i den indexerade åtkomstoperationen .
Här är exempelkoderna i aktion.
File vec.hh: wrapper for std :: vector, används för att visa logg när en konstruktion kallas.
#ifndef EXPR_VEC
# define EXPR_VEC
# include <vector>
# include <cassert>
# include <utility>
# include <iostream>
# include <algorithm>
# include <functional>
///
/// This is a wrapper for std::vector. It's only purpose is to print out a log when a
/// vector constructions in called.
/// It wraps the indexed access operator [] and the size() method, which are
/// important for later ETs implementation.
///
// std::vector wrapper.
template<typename ScalarType> class Vector
{
public:
explicit Vector() { std::cout << "ctor called.\n"; };
explicit Vector(int size): _vec(size) { std::cout << "ctor called.\n"; };
explicit Vector(const std::vector<ScalarType> &vec): _vec(vec)
{ std::cout << "ctor called.\n"; };
Vector(const Vector<ScalarType> & vec): _vec{vec()}
{ std::cout << "copy ctor called.\n"; };
Vector(Vector<ScalarType> && vec): _vec(std::move(vec()))
{ std::cout << "move ctor called.\n"; };
Vector<ScalarType> & operator=(const Vector<ScalarType> &) = default;
Vector<ScalarType> & operator=(Vector<ScalarType> &&) = default;
decltype(auto) operator[](int indx) { return _vec[indx]; }
decltype(auto) operator[](int indx) const { return _vec[indx]; }
decltype(auto) operator()() & { return (_vec); };
decltype(auto) operator()() const & { return (_vec); };
Vector<ScalarType> && operator()() && { return std::move(*this); }
int size() const { return _vec.size(); }
private:
std::vector<ScalarType> _vec;
};
///
/// These are conventional overloads of operator + (the vector plus operation)
/// and operator * (the vector inner product operation) without using the expression
/// templates. They are later used for bench-marking purpose.
///
// + (vector plus) operator.
template<typename ScalarType>
auto operator+(const Vector<ScalarType> &lhs, const Vector<ScalarType> &rhs)
{
assert(lhs().size() == rhs().size() &&
"error: ops plus -> lhs and rhs size mismatch.");
std::vector<ScalarType> _vec;
_vec.resize(lhs().size());
std::transform(std::cbegin(lhs()), std::cend(lhs()),
std::cbegin(rhs()), std::begin(_vec),
std::plus<>());
return Vector<ScalarType>(std::move(_vec));
}
// * (vector inner product) operator.
template<typename ScalarType>
auto operator*(const Vector<ScalarType> &lhs, const Vector<ScalarType> &rhs)
{
assert(lhs().size() == rhs().size() &&
"error: ops multiplies -> lhs and rhs size mismatch.");
std::vector<ScalarType> _vec;
_vec.resize(lhs().size());
std::transform(std::cbegin(lhs()), std::cend(lhs()),
std::cbegin(rhs()), std::begin(_vec),
std::multiplies<>());
return Vector<ScalarType>(std::move(_vec));
}
#endif //!EXPR_VEC
Fil expr.hh: implementering av expressionsmallar för elementvisa operationer (vektor plus och vektor inre produkt)
Låt oss dela upp det till sektioner.
- Avsnitt 1 implementerar en basklass för alla uttryck. Den använder CRiously ( Curiously Recurring Template Pattern ).
- Avsnitt 2 implementerar den första PAE : en terminal , som bara är en omslag (const-referens) av en inmatad datastruktur som innehåller verkligt inmatningsvärde för beräkning.
- Avsnitt 3 implementerar den andra PAE : binary_operation , som är en klassmall som senare används för vector_plus och vector_innerprod. Det parametriseras av typen av operation , PAE till vänster och PAE till höger . Den faktiska beräkningen kodas i den indexerade åtkomstoperatören.
- Avsnitt 4 definierar operationen vector_plus och vector_innerprod som elementvis funktion . Det överbelastar också operatören + och * för PAE : er så att dessa två operationer också returnerar PAE .
#ifndef EXPR_EXPR
# define EXPR_EXPR
/// Fwd declaration.
template<typename> class Vector;
namespace expr
{
/// -----------------------------------------
///
/// Section 1.
///
/// The first section is a base class template for all kinds of expression. It
/// employs the Curiously Recurring Template Pattern, which enables its instantiation
/// to any kind of expression structure inheriting from it.
///
/// -----------------------------------------
/// Base class for all expressions.
template<typename Expr> class expr_base
{
public:
const Expr& self() const { return static_cast<const Expr&>(*this); }
Expr& self() { return static_cast<Expr&>(*this); }
protected:
explicit expr_base() {};
int size() const { return self().size_impl(); }
auto operator[](int indx) const { return self().at_impl(indx); }
auto operator()() const { return self()(); };
};
/// -----------------------------------------
///
/// The following section 2 & 3 are abstractions of pure algebraic expressions (PAE).
/// Any PAE can be converted to a real object instance using operator(): it is in
/// this conversion process, where the real computations are done.
///
/// Section 2. Terminal
///
/// A terminal is an abstraction wrapping a const reference to the Vector data
/// structure. It inherits from expr_base, therefore providing a unified interface
/// wrapping a Vector into a PAE.
///
/// It provides the size() method, indexed access through at_impl() and a conversion
/// to referenced object through () operator.
///
/// It might no be necessary for user defined data structures to have a terminal
/// wrapper, since user defined structure can inherit expr_base, therefore eliminates
/// the need to provide such terminal wrapper.
///
/// -----------------------------------------
/// Generic wrapper for underlying data structure.
template<typename DataType> class terminal: expr_base<terminal<DataType>>
{
public:
using base_type = expr_base<terminal<DataType>>;
using base_type::size;
using base_type::operator[];
friend base_type;
explicit terminal(const DataType &val): _val(val) {}
int size_impl() const { return _val.size(); };
auto at_impl(int indx) const { return _val[indx]; };
decltype(auto) operator()() const { return (_val); }
private:
const DataType &_val;
};
/// -----------------------------------------
///
/// Section 3. Binary operation expression.
///
/// This is a PAE abstraction of any binary expression. Similarly it inherits from
/// expr_base.
///
/// It provides the size() method, indexed access through at_impl() and a conversion
/// to referenced object through () operator. Each call to the at_impl() method is
/// a element wise computation.
///
/// -----------------------------------------
/// Generic wrapper for binary operations (that are element-wise).
template<typename Ops, typename lExpr, typename rExpr>
class binary_ops: public expr_base<binary_ops<Ops,lExpr,rExpr>>
{
public:
using base_type = expr_base<binary_ops<Ops,lExpr,rExpr>>;
using base_type::size;
using base_type::operator[];
friend base_type;
explicit binary_ops(const Ops &ops, const lExpr &lxpr, const rExpr &rxpr)
: _ops(ops), _lxpr(lxpr), _rxpr(rxpr) {};
int size_impl() const { return _lxpr.size(); };
/// This does the element-wise computation for index indx.
auto at_impl(int indx) const { return _ops(_lxpr[indx], _rxpr[indx]); };
/// Conversion from arbitrary expr to concrete data type. It evaluates
/// element-wise computations for all indices.
template<typename DataType> operator DataType()
{
DataType _vec(size());
for(int _ind = 0; _ind < _vec.size(); ++_ind)
_vec[_ind] = (*this)[_ind];
return _vec;
}
private: /// Ops and expr are assumed cheap to copy.
Ops _ops;
lExpr _lxpr;
rExpr _rxpr;
};
/// -----------------------------------------
/// Section 4.
///
/// The following two structs defines algebraic operations on PAEs: here only vector
/// plus and vector inner product are implemented.
///
/// First, some element-wise operations are defined : in other words, vec_plus and
/// vec_prod acts on elements in Vectors, but not whole Vectors.
///
/// Then, operator + & * are overloaded on PAEs, such that: + & * operations on PAEs
/// also return PAEs.
///
/// -----------------------------------------
/// Element-wise plus operation.
struct vec_plus_t
{
constexpr explicit vec_plus_t() = default;
template<typename LType, typename RType>
auto operator()(const LType &lhs, const RType &rhs) const
{ return lhs+rhs; }
};
/// Element-wise inner product operation.
struct vec_prod_t
{
constexpr explicit vec_prod_t() = default;
template<typename LType, typename RType>
auto operator()(const LType &lhs, const RType &rhs) const
{ return lhs*rhs; }
};
/// Constant plus and inner product operator objects.
constexpr vec_plus_t vec_plus{};
constexpr vec_prod_t vec_prod{};
/// Plus operator overload on expressions: return binary expression.
template<typename lExpr, typename rExpr>
auto operator+(const lExpr &lhs, const rExpr &rhs)
{ return binary_ops<vec_plus_t,lExpr,rExpr>(vec_plus,lhs,rhs); }
/// Inner prod operator overload on expressions: return binary expression.
template<typename lExpr, typename rExpr>
auto operator*(const lExpr &lhs, const rExpr &rhs)
{ return binary_ops<vec_prod_t,lExpr,rExpr>(vec_prod,lhs,rhs); }
} //!expr
#endif //!EXPR_EXPR
File main.cc: testa src-fil
# include <chrono>
# include <iomanip>
# include <iostream>
# include "vec.hh"
# include "expr.hh"
# include "boost/core/demangle.hpp"
int main()
{
using dtype = float;
constexpr int size = 5e7;
std::vector<dtype> _vec1(size);
std::vector<dtype> _vec2(size);
std::vector<dtype> _vec3(size);
// ... Initialize vectors' contents.
Vector<dtype> vec1(std::move(_vec1));
Vector<dtype> vec2(std::move(_vec2));
Vector<dtype> vec3(std::move(_vec3));
unsigned long start_ms_no_ets =
std::chrono::duration_cast<std::chrono::milliseconds>
(std::chrono::system_clock::now().time_since_epoch()).count();
std::cout << "\nNo-ETs evaluation starts.\n";
Vector<dtype> result_no_ets = vec1 + (vec2*vec3);
unsigned long stop_ms_no_ets =
std::chrono::duration_cast<std::chrono::milliseconds>
(std::chrono::system_clock::now().time_since_epoch()).count();
std::cout << std::setprecision(6) << std::fixed
<< "No-ETs. Time eclapses: " << (stop_ms_no_ets-start_ms_no_ets)/1000.0
<< " s.\n" << std::endl;
unsigned long start_ms_ets =
std::chrono::duration_cast<std::chrono::milliseconds>
(std::chrono::system_clock::now().time_since_epoch()).count();
std::cout << "Evaluation using ETs starts.\n";
expr::terminal<Vector<dtype>> vec4(vec1);
expr::terminal<Vector<dtype>> vec5(vec2);
expr::terminal<Vector<dtype>> vec6(vec3);
Vector<dtype> result_ets = (vec4 + vec5*vec6);
unsigned long stop_ms_ets =
std::chrono::duration_cast<std::chrono::milliseconds>
(std::chrono::system_clock::now().time_since_epoch()).count();
std::cout << std::setprecision(6) << std::fixed
<< "With ETs. Time eclapses: " << (stop_ms_ets-start_ms_ets)/1000.0
<< " s.\n" << std::endl;
auto ets_ret_type = (vec4 + vec5*vec6);
std::cout << "\nETs result's type:\n";
std::cout << boost::core::demangle( typeid(decltype(ets_ret_type)).name() ) << '\n';
return 0;
}
Här är en möjlig utgång när den kompileras med -O3 -std=c++14
med GCC 5.3:
ctor called.
ctor called.
ctor called.
No-ETs evaluation starts.
ctor called.
ctor called.
No-ETs. Time eclapses: 0.571000 s.
Evaluation using ETs starts.
ctor called.
With ETs. Time eclapses: 0.164000 s.
ETs result's type:
expr::binary_ops<expr::vec_plus_t, expr::terminal<Vector<float> >, expr::binary_ops<expr::vec_prod_t, expr::terminal<Vector<float> >, expr::terminal<Vector<float> > > >
Observationerna är:
- Att använda ET: er ger en ganska betydande prestationsökning i detta fall (> 3x).
- Skapa ett tillfälligt vektorobjekt elimineras. Liksom i ET: s fall kallas ctor bara en gång.
- Boost :: demangle användes för att visualisera typen av ET: s återgång före konvertering: det konstruerade tydligt exakt samma uttrycksgraf som visas ovan.
Ryggstöd och varningar
En uppenbar nackdel med ET: er är inlärningskurvan, komplexiteten i implementeringen och problem med underhåll av kod. I exemplet ovan, där endast elementvisa operationer beaktas, innehåller implementeringen redan en enorm mängd pannplattor, än mindre i den verkliga världen, där mer komplexa algebraiska uttryck förekommer i varje beräkning och elementmässigt oberoende inte längre rymmer (till exempel matrismultiplikation ), kommer svårigheten att vara exponentiell.
Ett annat varumärke med att använda ET: er är att de spelar bra med
auto
nyckelordet. Som nämnts ovan är PAE: er huvudsakligen proxyer: och proxyer spelar i princip inte bra medauto
. Tänk på följande exempel:auto result = ...; // Some expensive expression: // auto returns the expr graph, // NOT the computed value. for(auto i = 0; i < 100; ++i) ScalrType value = result* ... // Some other expensive computations using result.
Här i varje iteration av for-loopen kommer resultatet att utvärderas , eftersom uttryckningsgrafen istället för det beräknade värdet skickas till for-loopen.
Befintliga bibliotek som implementerar ET: er
- boost :: proto är ett kraftfullt bibliotek som låter dig definiera dina egna regler och grammatik för dina egna uttryck och köra med ET: er .
- Eigen är ett bibliotek för linjär algebra som implementerar olika algebraiska beräkningar effektivt med hjälp av ET: er .
Ett grundläggande exempel som illustrerar expressionsmallar
En expressionsmall är en kompileringstidsoptimeringsteknik som mest används i vetenskaplig databehandling. Det huvudsakliga syftet är att undvika onödiga tillfälliga och optimera loopberäkningar med ett enda pass (vanligtvis när du utför operationer på numeriska aggregat). Uttrycksmallar utformades ursprungligen för att kringgå ineffektiviteten hos naiva operatörsöverbelastning vid implementering av numeriska Array
eller Matrix
typer. En motsvarande terminologi för expressionsmallar har introducerats av Bjarne Stroustrup, som kallar dem "fused operations" i den senaste versionen av sin bok, "The C ++ Programming Language".
Innan du dyker i uttrycksmallar bör du förstå varför du behöver dem i första hand. För att illustrera detta, tänk på den mycket enkla Matrix-klass som ges nedan:
template <typename T, std::size_t COL, std::size_t ROW>
class Matrix {
public:
using value_type = T;
Matrix() : values(COL * ROW) {}
static size_t cols() { return COL; }
static size_t rows() { return ROW; }
const T& operator()(size_t x, size_t y) const { return values[y * COL + x]; }
T& operator()(size_t x, size_t y) { return values[y * COL + x]; }
private:
std::vector<T> values;
};
template <typename T, std::size_t COL, std::size_t ROW>
Matrix<T, COL, ROW>
operator+(const Matrix<T, COL, ROW>& lhs, const Matrix<T, COL, ROW>& rhs)
{
Matrix<T, COL, ROW> result;
for (size_t y = 0; y != lhs.rows(); ++y) {
for (size_t x = 0; x != lhs.cols(); ++x) {
result(x, y) = lhs(x, y) + rhs(x, y);
}
}
return result;
}
Med tanke på den tidigare klassdefinitionen kan du nu skriva Matrix-uttryck som:
const std::size_t cols = 2000;
const std::size_t rows = 1000;
Matrix<double, cols, rows> a, b, c;
// initialize a, b & c
for (std::size_t y = 0; y != rows; ++y) {
for (std::size_t x = 0; x != cols; ++x) {
a(x, y) = 1.0;
b(x, y) = 2.0;
c(x, y) = 3.0;
}
}
Matrix<double, cols, rows> d = a + b + c; // d(x, y) = 6
Som illustreras ovan, genom att kunna överbelasta operator+()
ger dig en notation som härmar den naturliga matematiska notationen för matriser.
Tyvärr är den tidigare implementeringen också mycket ineffektiv jämfört med en motsvarande "handgjord" version.
För att förstå varför måste du överväga vad som händer när du skriver ett uttryck som Matrix d = a + b + c
. Detta expanderar faktiskt till ((a + b) + c)
eller operator+(operator+(a, b), c)
. Med andra ord exekveras slingan inuti operator+()
två gånger, medan den lätt kunde ha utförts i en enda pass. Detta resulterar också i att två tillfälliga skapas, vilket ytterligare försämrar prestandan. I huvudsak, genom att lägga till flexibiliteten för att använda en notation nära dess matematiska motsvarighet, har du också gjort Matrix
klassen mycket ineffektiv.
Exempelvis utan operatörens överbelastning kan du implementera en mycket effektivare Matrix-summering med ett enda pass:
template<typename T, std::size_t COL, std::size_t ROW>
Matrix<T, COL, ROW> add3(const Matrix<T, COL, ROW>& a,
const Matrix<T, COL, ROW>& b,
const Matrix<T, COL, ROW>& c)
{
Matrix<T, COL, ROW> result;
for (size_t y = 0; y != ROW; ++y) {
for (size_t x = 0; x != COL; ++x) {
result(x, y) = a(x, y) + b(x, y) + c(x, y);
}
}
return result;
}
Det föregående exemplet har emellertid sina egna nackdelar eftersom det skapar ett mycket mer invändigt gränssnitt för Matrix-klassen (du måste överväga metoder som Matrix::add2()
, Matrix::AddMultiply()
och så vidare).
Låt oss istället ta ett steg tillbaka och se hur vi kan anpassa operatörens överbelastning för att utföra på ett mer effektivt sätt
Problemet härrör från det faktum att uttrycket Matrix d = a + b + c
utvärderas för "ivrigt" innan du har haft en möjlighet att bygga hela uttrycksträdet. Med andra ord, vad du verkligen vill uppnå är att utvärdera a + b + c
i ett pass och bara en gång du faktiskt behöver tilldela det resulterande uttrycket till d
.
Detta är kärnidéen bakom expressionsmallar: istället för att operator+()
utvärdera omedelbart resultatet av att lägga till två Matrix-instanser kommer det att returnera en "expressionsmall" för framtida utvärdering när hela uttrycksträdet har byggts.
Här är till exempel en möjlig implementering för en uttrycksmall som motsvarar sammanfattningen av två typer:
template <typename LHS, typename RHS>
class MatrixSum
{
public:
using value_type = typename LHS::value_type;
MatrixSum(const LHS& lhs, const RHS& rhs) : rhs(rhs), lhs(lhs) {}
value_type operator() (int x, int y) const {
return lhs(x, y) + rhs(x, y);
}
private:
const LHS& lhs;
const RHS& rhs;
};
Och här är den uppdaterade versionen av operator+()
template <typename LHS, typename RHS>
MatrixSum<LHS, RHS> operator+(const LHS& lhs, const LHS& rhs) {
return MatrixSum<LHS, RHS>(lhs, rhs);
}
Som ni ser returnerar operator+()
inte längre en "ivrig utvärdering" av resultatet av att lägga till 2 Matrix-instanser (vilket skulle vara en annan Matrix-instans), utan istället en expressionsmall som representerar tilläggsoperationen. Den viktigaste punkten att komma ihåg är att uttrycket inte har utvärderats än. Den innehåller bara referenser till sina operander.
I själva verket hindrar ingenting dig från att instansera MatrixSum<>
sätt:
MatrixSum<Matrix<double>, Matrix<double> > SumAB(a, b);
Du kan dock i ett senare skede, när du faktiskt behöver resultatet av summeringen, utvärdera uttrycket d = a + b
enligt följande:
for (std::size_t y = 0; y != a.rows(); ++y) {
for (std::size_t x = 0; x != a.cols(); ++x) {
d(x, y) = SumAB(x, y);
}
}
Som du ser är en annan fördel med att använda en uttrycksmall att du i princip har lyckats utvärdera summan av a
och b
och tilldela den till d
i ett enda pass.
Ingenting hindrar dig också från att kombinera flera expressionsmallar. Exempelvis skulle a + b + c
resultera i följande uttrycksmall:
MatrixSum<MatrixSum<Matrix<double>, Matrix<double> >, Matrix<double> > SumABC(SumAB, c);
Och här igen kan du utvärdera det slutliga resultatet med ett enda pass:
for (std::size_t y = 0; y != a.rows(); ++y) {
for (std::size_t x = 0; x != a.cols(); ++x) {
d(x, y) = SumABC(x, y);
}
}
Slutligen är den sista biten i pusslet att faktiskt ansluta din expressionsmall till Matrix
klassen. Detta uppnås i huvudsak genom att tillhandahålla en implementering för Matrix::operator=()
, som tar uttrycksmallen som ett argument och utvärderar det i ett pass, som du gjorde "manuellt" innan:
template <typename T, std::size_t COL, std::size_t ROW>
class Matrix {
public:
using value_type = T;
Matrix() : values(COL * ROW) {}
static size_t cols() { return COL; }
static size_t rows() { return ROW; }
const T& operator()(size_t x, size_t y) const { return values[y * COL + x]; }
T& operator()(size_t x, size_t y) { return values[y * COL + x]; }
template <typename E>
Matrix<T, COL, ROW>& operator=(const E& expression) {
for (std::size_t y = 0; y != rows(); ++y) {
for (std::size_t x = 0; x != cols(); ++x) {
values[y * COL + x] = expression(x, y);
}
}
return *this;
}
private:
std::vector<T> values;
};