C++
Modelli di espressione
Ricerca…
Modelli di espressioni di base sulle espressioni algebriche element-wise
Introduzione e motivazione
I modelli di espressione (indicati come ET in seguito) sono una potente tecnica di meta-programmazione del modello, utilizzata per accelerare i calcoli di espressioni talvolta piuttosto costose. È ampiamente utilizzato in diversi domini, ad esempio nell'implementazione di librerie di algebra lineare.
Per questo esempio, considera il contesto dei calcoli algebrici lineari. Più specificamente, calcoli che riguardano solo operazioni element-wise . Questo tipo di calcoli sono le applicazioni di base degli ET e rappresentano una buona introduzione al modo in cui gli ET lavorano internamente.
Diamo un'occhiata a un esempio motivante. Considera il calcolo dell'espressione:
Vector vec_1, vec_2, vec_3;
// Initializing vec_1, vec_2 and vec_3.
Vector result = vec_1 + vec_2*vec_3;
Qui per semplicità, presumo che la classe Vector
e l'operazione + (vector plus: operazione elemento-saggio plus) e operazione * (qui significa prodotto interno vettoriale: anche operazione elemento-saggio) siano entrambi correttamente implementati, come come dovrebbero essere, matematicamente.
In un'implementazione convenzionale senza utilizzare ET (o altre tecniche simili), per ottenere il result
finale sono necessarie almeno cinque costruzioni di istanze Vector
:
- Tre istanze corrispondenti a
vec_1
,vec_2
evec_3
. - Un'istanza
Vector
temporanea_tmp
, che rappresenta il risultato di_tmp = vec_2*vec_3;
. - Infine con l'uso corretto dell'ottimizzazione del valore di ritorno , la costruzione del
result
finale inresult = vec_1 + _tmp;
.
L'implementazione tramite ET può eliminare la creazione di Vector _tmp
temporaneo in 2, lasciando così solo quattro costruzioni di istanze Vector
. Più interessante, considera la seguente espressione che è più complessa:
Vector result = vec_1 + (vec_2*vec3 + vec_1)*(vec_2 + vec_3*vec_1);
Ci saranno anche quattro costruzioni di istanze Vector
in totale: vec_1, vec_2, vec_3
e result
. In altre parole, in questo esempio, in cui sono coinvolte solo operazioni basate su elementi , è garantito che non verranno creati oggetti temporanei da calcoli intermedi .
Come funzionano gli ET
In sostanza, gli ET per i calcoli algebrici sono costituiti da due elementi costitutivi:
- Pure algebraic expressions ( PAE ): sono proxy / astrazioni di espressioni algebriche. Un algebrico puro non esegue calcoli effettivi, sono semplicemente astrazioni / modellizzazione del flusso di lavoro di calcolo. Un PAE può essere un modello dell'ingresso o dell'output di qualsiasi espressione algebrica . Le istanze di PAE sono spesso considerate economiche da copiare.
- Valutazioni pigre : quali sono l'implementazione di calcoli reali. Nell'esempio seguente, vedremo che per le espressioni che coinvolgono solo operazioni element-wise, le valutazioni lazy possono implementare calcoli effettivi all'interno dell'operazione di accesso indicizzato sul risultato finale, creando così uno schema di valutazione on-demand: non viene eseguito un calcolo solo se si accede / chiede il risultato finale.
Quindi, in particolare come implementiamo gli ET in questo esempio? Passeggiamo ora.
Considera sempre il seguente frammento di codice:
Vector vec_1, vec_2, vec_3;
// Initializing vec_1, vec_2 and vec_3.
Vector result = vec_1 + vec_2*vec_3;
L'espressione per calcolare il risultato può essere scomposta ulteriormente in due sottoespressioni:
- Un vettore più un'espressione (indicato come plus_expr )
- Un'espressione di prodotto interno vettoriale (indicata come innerprod_expr ).
Cosa fanno gli ET è il seguente:
Invece di calcolare immediatamente ogni sottoespressione, gli ET modellano prima l'intera espressione usando una struttura grafica. Ogni nodo nel grafico rappresenta un PAE . La connessione di bordo dei nodi rappresenta il flusso di calcolo effettivo. Quindi per l'espressione sopra, otteniamo il seguente grafico:
result = plus_expr( vec_1, innerprod_expr(vec_2, vec_3) ) / \ / \ / \ / innerprod_expr( vec_2, vec_3 ) / / \ / / \ / / \ vec_1 vec_2 vec_3
Il calcolo finale viene implementato osservando la gerarchia del grafo : poiché qui si tratta solo di operazioni element-wise , il calcolo di ogni valore indicizzato nel
result
può essere fatto indipendentemente : la valutazione finale delresult
può essere spostata pigramente su un elemento- saggia valutazione di ogni elemento diresult
. In altre parole, poiché il calcolo di un elemento diresult
,elem_res
, può essere espresso utilizzando gli elementi corrispondenti invec_1
(elem_1
),vec_2
(elem_2
) evec_3
(elem_3
) come:elem_res = elem_1 + elem_2*elem_3;
non è quindi necessario creare un Vector
temporaneo per memorizzare il risultato del prodotto interno intermedio: l'intero calcolo per un elemento può essere eseguito del tutto e codificato all'interno dell'operazione di accesso indicizzato .
Ecco i codici di esempio in azione.
File vec.hh: wrapper per std :: vector, utilizzato per mostrare il log quando viene chiamata una costruzione.
#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
File expr.hh: implementazione di modelli di espressione per operazioni basate su elementi (vector plus e vector inner product)
Scopriamolo in sezioni.
- Sezione 1 implementa una classe base per tutte le espressioni. Impiega il modello di modello Curiosamente ricorrente ( CRTP ).
- La Sezione 2 implementa il primo PAE : un terminale , che è solo un wrapper (riferimento const) di una struttura di dati di input contenente un valore di input reale per il calcolo.
- La sezione 3 implementa il secondo PAE : binary_operation , che è un modello di classe utilizzato in seguito per vector_plus e vector_innerprod. È parametrizzato dal tipo di operazione , dal PAE sul lato sinistro e dal PAE sul lato destro . Il calcolo reale è codificato nell'operatore di accesso indicizzato.
- La sezione 4 definisce le operazioni vector_plus e vector_innerprod come operazioni basate sull'elemento . Sovraccarica anche l'operatore + e * per PAE s: in modo che queste due operazioni restituiscano anche 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: prova file src
# 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;
}
Ecco un output possibile quando compilato con -O3 -std=c++14
utilizzando 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> > > >
Le osservazioni sono:
- In questo caso l' utilizzo degli ET raggiunge un incremento delle prestazioni piuttosto significativo (> 3x).
- La creazione dell'oggetto vettoriale temporaneo è stata eliminata. Come nel caso degli ET , Ctor è chiamato solo una volta.
- Boost :: demangle è stato usato per visualizzare il tipo di ritorno ET prima della conversione: ha chiaramente costruito esattamente lo stesso grafico di espressione mostrato sopra.
Restituzioni e avvertimenti
Un ovvio svantaggio degli ET è la curva di apprendimento, la complessità dell'implementazione e la difficoltà di manutenzione del codice. Nell'esempio sopra in cui vengono prese in considerazione solo le operazioni basate sull'elemento, l'implementazione contiene già enormi quantità di piastre, e tanto meno nel mondo reale, dove espressioni algebriche più complesse si verificano in ogni calcolo e l'indipendenza dal punto di vista dell'elemento non è più valida (ad esempio la moltiplicazione della matrice ), la difficoltà sarà esponenziale.
Un altro avvertimento sull'uso degli ET è che giocano bene con la parola chiave
auto
. Come accennato in precedenza, i PAE sono essenzialmente proxy: e i proxy in pratica non funzionano bene con l'auto
. Considera il seguente esempio: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.
Qui in ogni iterazione del ciclo for, il risultato verrà rivalutato , poiché il grafico dell'espressione anziché il valore calcolato viene passato al ciclo for.
Librerie esistenti che implementano ET
- boost :: proto è una potente libreria che ti permette di definire le tue regole e grammatiche per le tue espressioni ed eseguirle usando ET .
- Eigen è una libreria per l'algebra lineare che implementa vari calcoli algebrici in modo efficiente usando ET .
Un esempio di base che illustra i modelli di espressione
Un modello di espressione è una tecnica di ottimizzazione in fase di compilazione utilizzata principalmente nel calcolo scientifico. Lo scopo principale è evitare temporanei inutili e ottimizzare i calcoli del loop usando un singolo passaggio (in genere quando si eseguono operazioni su aggregati numerici). Modelli di espressione sono stati inizialmente concepiti per aggirare le inefficienze naive sovraccarico operatore nell'attuazione numerici Array
o Matrix
tipi. Una terminologia equivalente per i modelli di espressione è stata introdotta da Bjarne Stroustrup, che li chiama "operazioni fuse" nell'ultima versione del suo libro, "Il linguaggio di programmazione C ++".
Prima di entrare effettivamente nei modelli di espressione, dovresti capire perché ne hai bisogno in primo luogo. Per illustrare questo, considera la semplice classe Matrix fornita di seguito:
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;
}
Data la definizione della classe precedente, ora puoi scrivere espressioni Matrix come:
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
Come illustrato sopra, essere in grado di sovraccaricare l' operator+()
fornisce una notazione che riproduce la notazione matematica naturale per le matrici.
Sfortunatamente, l'implementazione precedente è anche altamente inefficiente rispetto a una versione equivalente "realizzata a mano".
Per capire perché, devi considerare cosa succede quando scrivi un'espressione come Matrix d = a + b + c
. Questo infatti si espande in ((a + b) + c)
o operator+(operator+(a, b), c)
. In altre parole, il ciclo all'interno operator+()
viene eseguito due volte, mentre potrebbe essere facilmente eseguito in un singolo passaggio. Ciò comporta anche la creazione di 2 provini, che peggiorano ulteriormente le prestazioni. In sostanza, aggiungendo la flessibilità per usare una notazione vicina alla sua controparte matematica, hai anche reso la classe Matrix
altamente inefficiente.
Ad esempio, senza il sovraccarico dell'operatore, è possibile implementare una sommatoria Matrix molto più efficiente utilizzando un singolo passaggio:
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;
}
L'esempio precedente tuttavia ha i suoi svantaggi perché crea un'interfaccia molto più complessa per la classe Matrix (dovresti considerare metodi come Matrix::add2()
, Matrix::AddMultiply()
e così via).
Facciamo un passo indietro e vediamo come possiamo adattare l'overloading dell'operatore per eseguire in modo più efficiente
Il problema deriva dal fatto che l'espressione Matrix d = a + b + c
viene valutata troppo "appassionatamente" prima di aver avuto l'opportunità di costruire l'intero albero delle espressioni. In altre parole, ciò che si vuole veramente ottenere è di valutare a + b + c
in un solo passaggio e solo quando effettivamente si ha bisogno di assegnare l'espressione risultante a d
.
Questa è l'idea alla base dei modelli di espressione: invece di avere operator+()
valuta immediatamente il risultato dell'aggiunta di due istanze Matrix, restituirà un "modello di espressione" per la valutazione futura una volta che l'intero albero di espressioni è stato creato.
Ad esempio, ecco una possibile implementazione per un modello di espressione corrispondente alla somma di 2 tipi:
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;
};
Ed ecco la versione aggiornata di operator+()
template <typename LHS, typename RHS>
MatrixSum<LHS, RHS> operator+(const LHS& lhs, const LHS& rhs) {
return MatrixSum<LHS, RHS>(lhs, rhs);
}
Come si può vedere, l' operator+()
non restituisce più una "valutazione entusiasta" del risultato dell'aggiunta di 2 istanze Matrix (che sarebbe un'altra istanza Matrix), ma invece di un modello di espressione che rappresenta l'operazione di aggiunta. Il punto più importante da tenere a mente è che l'espressione non è stata ancora valutata. Mantiene semplicemente i riferimenti ai suoi operandi.
In effetti, nulla ti impedisce di MatrixSum<>
un'istanza del modello di espressione MatrixSum<>
come segue:
MatrixSum<Matrix<double>, Matrix<double> > SumAB(a, b);
Tuttavia, in una fase successiva, quando è effettivamente necessario il risultato della somma, valutare l'espressione d = a + b
come segue:
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);
}
}
Come si può vedere, un altro vantaggio di utilizzare un modello di espressione, è che si è praticamente riuscito a valutare la somma di a
e b
e assegnarlo a d
in un unico passaggio.
Inoltre, nulla ti impedisce di combinare più modelli di espressioni. Ad esempio, a + b + c
risulterebbe nel seguente modello di espressione:
MatrixSum<MatrixSum<Matrix<double>, Matrix<double> >, Matrix<double> > SumABC(SumAB, c);
E anche qui puoi valutare il risultato finale con un solo passaggio:
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);
}
}
Infine, l'ultimo pezzo del puzzle è quello di collegare effettivamente il modello di espressione alla classe Matrix
. Ciò si ottiene essenzialmente fornendo un'implementazione per Matrix::operator=()
, che prende il modello di espressione come argomento e lo valuta in un passaggio, come fatto "manualmente" prima:
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;
};