C++
Modèles d'expression
Recherche…
Modèles d'expression de base sur des expressions algébriques élémentaires
Introduction et motivation
Les modèles d'expression ( appelés ET dans la suite) sont une technique puissante de méta-programmation de modèles, utilisée pour accélérer les calculs d'expressions parfois très coûteuses. Il est largement utilisé dans différents domaines, par exemple dans la mise en œuvre de bibliothèques d'algèbre linéaire.
Pour cet exemple, considérons le contexte des calculs algébriques linéaires. Plus précisément, les calculs impliquant uniquement des opérations par éléments . Ce type de calcul est l’application la plus élémentaire des ET et constitue une bonne introduction à la manière dont les ET fonctionnent en interne.
Regardons un exemple motivant. Considérons le calcul de l'expression:
Vector vec_1, vec_2, vec_3;
// Initializing vec_1, vec_2 and vec_3.
Vector result = vec_1 + vec_2*vec_3;
Pour simplifier, je suppose que les classes Vector
et operation + (vector plus: element-wise plus operation) et operation * (ici: produit interne vectoriel: opération par élément) sont toutes deux correctement implémentées, comment ils devraient être, mathématiquement.
Dans une implémentation conventionnelle sans utiliser d' ET (ou d'autres techniques similaires), au moins cinq constructions d'instances Vector
ont lieu afin d'obtenir le result
final:
- Trois instances correspondant à
vec_1
,vec_2
etvec_3
. - Une instance de
Vector
temporaire_tmp
, représentant le résultat de_tmp = vec_2*vec_3;
. - Enfin, avec une utilisation correcte de l' optimisation de la valeur de retour , la construction du
result
final dansresult = vec_1 + _tmp;
.
L'implémentation à l'aide d' ET peut éliminer la création d'un Vector _tmp
temporaire Vector _tmp
en 2, ne laissant ainsi que quatre constructions d'instances Vector
. Plus intéressant encore, considérons l’expression suivante qui est plus complexe:
Vector result = vec_1 + (vec_2*vec3 + vec_1)*(vec_2 + vec_3*vec_1);
Il y aura également quatre constructions d'instances Vector
au total: vec_1, vec_2, vec_3
et result
. En d'autres termes, dans cet exemple, où seules les opérations élémentaires sont impliquées , il est garanti qu'aucun objet temporaire ne sera créé à partir de calculs intermédiaires .
Comment fonctionnent les ET
Fondamentalement, les ET pour tous les calculs algébriques sont constitués de deux blocs de construction:
- Expressions algébriques pures ( PAE ): ce sont des proxies / abstractions d'expressions algébriques. Une algébrique pure ne fait pas de calculs réels, ce ne sont que des abstractions / modélisation du flux de travail de calcul. Un PAE peut être un modèle de l'entrée ou de la sortie de toutes les expressions algébriques . Les instances de PAE sont souvent considérées comme peu coûteuses à copier.
- Évaluations paresseuses : qui sont des implémentations de calculs réels. Dans l'exemple suivant, nous verrons que pour les expressions n'impliquant que des opérations élémentaires, les évaluations paresseuses peuvent implémenter des calculs réels à l'intérieur de l'opération indexée sur le résultat final, créant ainsi un schéma d'évaluation à la demande: seulement si le résultat final est accédé / demandé.
Donc, spécifiquement, comment implémentons-nous les ET dans cet exemple? Passons à travers maintenant.
Considérez toujours l'extrait de code suivant:
Vector vec_1, vec_2, vec_3;
// Initializing vec_1, vec_2 and vec_3.
Vector result = vec_1 + vec_2*vec_3;
L'expression pour calculer le résultat peut être décomposée en deux sous-expressions:
- Un vecteur plus une expression (notée plus_expr )
- Une expression de produit interne vectoriel (dénommée innerprod_expr ).
Ce que font les ET est le suivant:
Au lieu de calculer tout de suite chaque sous-expression, les ET modélisent d'abord toute l'expression à l'aide d'une structure graphique. Chaque nœud du graphique représente un PAE . La connexion de périphérie des nœuds représente le flux de calcul réel. Donc, pour l'expression ci-dessus, nous obtenons le graphique suivant:
result = plus_expr( vec_1, innerprod_expr(vec_2, vec_3) ) / \ / \ / \ / innerprod_expr( vec_2, vec_3 ) / / \ / / \ / / \ vec_1 vec_2 vec_3
Le calcul final est réalisé en parcourant la hiérarchie graphique : puisque nous ne traitons ici que d’opérations élémentaires , le calcul de chaque valeur indexée dans le
result
peut être effectué de manière indépendante : l’évaluation finale duresult
peut être différée à un élément. évaluation sage de chaque élément deresult
. En d'autres termes, puisque le calcul d'un élément deresult
,elem_res
, peut être exprimé en utilisant les éléments correspondants dansvec_1
(elem_1
),vec_2
(elem_2
) etvec_3
(elem_3
) comme:elem_res = elem_1 + elem_2*elem_3;
il n'est donc pas nécessaire de créer un Vector
temporaire pour stocker le résultat du produit interne intermédiaire: tout le calcul pour un élément peut être fait entièrement et être codé dans l'opération d'accès indexé .
Voici les exemples de codes en action.
Fichier vec.hh: wrapper pour std :: vector, utilisé pour afficher le journal lorsqu'une construction est appelée.
#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
Fichier expr.hh: implémentation de modèles d'expression pour les opérations élémentaires (vecteur plus et produit interne vectoriel)
Divisons le en sections.
- La section 1 implémente une classe de base pour toutes les expressions. Il utilise le modèle de modèle curieusement récurrent ( CRTP ).
- La section 2 implémente le premier PAE : un terminal , qui n'est qu'un wrapper (référence const) d'une structure de données d'entrée contenant une valeur d'entrée réelle pour le calcul.
- La section 3 implémente le second PAE : binary_operation , qui est un modèle de classe utilisé par la suite pour vector_plus et vector_innerprod. Il est paramétré par le type d'opération , le PAE côté gauche et le PAE côté droit . Le calcul réel est codé dans l'opérateur d'accès indexé.
- La section 4 définit les opérations vector_plus et vector_innerprod comme des opérations élémentaires . Il surcharge également l'opérateur + et * pour les PAE : ces deux opérations renvoient également 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
Fichier main.cc: test du fichier 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;
}
Voici une sortie possible lors de la compilation avec -O3 -std=c++14
utilisant 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> > > >
Les observations sont les suivantes:
- Dans ce cas, l' utilisation des ET permet une amélioration significative des performances (> 3x).
- La création d'un objet vectoriel temporaire est éliminée. Comme dans le cas de ETs, cteur est appelée une seule fois.
- Boost :: demangle a été utilisé pour visualiser le type de retour d'ET avant la conversion: il a clairement construit exactement le même graphique d'expression que celui présenté ci-dessus.
Reculs et mises en garde
Un inconvénient évident des ET est la courbe d'apprentissage, la complexité de la mise en œuvre et la difficulté de maintenance du code. Dans l'exemple ci-dessus où seules les opérations par éléments sont prises en compte, l'implémentation contient déjà énormément de points d'exclusion, sans parler du monde réel, où des expressions algébriques plus complexes apparaissent. ), la difficulté sera exponentielle.
Un autre inconvénient de l'utilisation des ET est qu'ils jouent bien avec le mot
auto
cléauto
. Comme mentionné ci-dessus, les PAE sont essentiellement des proxies: et les proxys ne jouent pas bien avec l’auto
. Prenons l'exemple suivant: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.
Dans chaque itération de la boucle for, le résultat sera réévalué , car le graphique de l'expression au lieu de la valeur calculée est transmis à la boucle for.
Bibliothèques existantes implémentant des ET
- boost :: proto est une puissante bibliothèque qui vous permet de définir vos propres règles et grammaires pour vos propres expressions et de les exécuter à l'aide d' ET .
- Eigen est une bibliothèque d'algèbre linéaire qui implémente différents calculs algébriques avec des ET .
Un exemple de base illustrant des modèles d'expression
Un modèle d'expression est une technique d'optimisation à la compilation utilisée principalement dans le calcul scientifique. Son objectif principal est d'éviter les tâches temporaires inutiles et d'optimiser les calculs de boucle en un seul passage (généralement lors d'opérations sur des agrégats numériques). Les modèles d'expression ont été initialement conçus pour contourner l'inefficacité de la surcharge des opérateurs naïfs lors de l' implémentation numérique Array
ou Matrix
types. Une terminologie équivalente pour les modèles d’expression a été introduite par Bjarne Stroustrup, qui les appelle "opérations fusionnées" dans la dernière version de son livre, "Le langage de programmation C ++".
Avant de plonger dans des modèles d’expression, vous devez comprendre pourquoi vous en avez besoin au départ. Pour illustrer ceci, considérons la classe de matrice très simple donnée ci-dessous:
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;
}
Étant donné la définition de classe précédente, vous pouvez maintenant écrire des expressions Matrix telles que:
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
Comme illustré ci-dessus, le fait de pouvoir surcharger operator+()
vous fournit une notation qui imite la notation mathématique naturelle des matrices.
Malheureusement, l'implémentation précédente est également très inefficace par rapport à une version «artisanale» équivalente.
Pour comprendre pourquoi, vous devez considérer ce qui se passe lorsque vous écrivez une expression telle que Matrix d = a + b + c
. Cela se développe en fait en ((a + b) + c)
ou en operator+(operator+(a, b), c)
. En d'autres termes, la boucle à l'intérieur de l' operator+()
est exécutée deux fois, alors qu'elle aurait pu être facilement exécutée en un seul passage. Cela se traduit également par la création de 2 versions temporaires, ce qui dégrade davantage les performances. Essentiellement, en ajoutant la flexibilité d’utiliser une notation proche de son équivalent mathématique, vous avez également rendu la classe Matrix
très inefficace.
Par exemple, sans surcharge de l'opérateur, vous pouvez implémenter une somme de matrice beaucoup plus efficace en utilisant un seul passage:
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'exemple précédent a cependant ses propres inconvénients, car il crée une interface beaucoup plus compliquée pour la classe Matrix (vous devrez envisager des méthodes telles que Matrix::add2()
, Matrix::AddMultiply()
, etc.).
Au lieu de cela, prenons un peu de recul et voyons comment nous pouvons adapter la surcharge de l'opérateur pour une performance plus efficace.
Le problème provient du fait que l'expression Matrix d = a + b + c
est évaluée trop "avidement" avant que vous ayez eu l'occasion de construire l'arborescence complète de l'expression. En d’autres termes, ce que vous voulez vraiment réaliser, c’est d’évaluer a + b + c
en une seule fois et seulement une fois que vous avez réellement besoin d’affecter l’expression résultante à d
.
C'est l'idée de base des modèles d'expression: au lieu de l' operator+()
évalue immédiatement le résultat de l'ajout de deux instances Matrix, il renverra un "modèle d'expression" pour une évaluation ultérieure une fois que toute l'arborescence aura été construite.
Par exemple, voici une implémentation possible pour un modèle d'expression correspondant à la somme de deux types:
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;
};
Et voici la version mise à jour de l' operator+()
template <typename LHS, typename RHS>
MatrixSum<LHS, RHS> operator+(const LHS& lhs, const LHS& rhs) {
return MatrixSum<LHS, RHS>(lhs, rhs);
}
Comme vous pouvez le voir, operator+()
ne renvoie plus une "évaluation enthousiaste" du résultat de l'ajout de 2 instances Matrix (ce qui serait une autre instance Matrix), mais plutôt un modèle d'expression représentant l'opération d'addition. Le point le plus important à garder à l'esprit est que l'expression n'a pas encore été évaluée. Il ne contient que des références à ses opérandes.
En fait, rien ne vous empêche d’instancier le modèle d’expression MatrixSum<>
comme suit:
MatrixSum<Matrix<double>, Matrix<double> > SumAB(a, b);
Vous pouvez cependant à un stade ultérieur, lorsque vous avez réellement besoin du résultat de la sommation, évaluer l'expression d = a + b
comme suit:
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);
}
}
Comme vous pouvez le voir, un autre avantage de l'utilisation d'un modèle d'expression est que vous avez essentiellement réussi à évaluer la somme de a
et b
et à l'assigner à d
en un seul passage.
De plus, rien ne vous empêche de combiner plusieurs modèles d’expression. Par exemple, a + b + c
entraînerait le modèle d'expression suivant:
MatrixSum<MatrixSum<Matrix<double>, Matrix<double> >, Matrix<double> > SumABC(SumAB, c);
Et là encore, vous pouvez évaluer le résultat final en un seul passage:
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);
}
}
Enfin, la dernière pièce du puzzle consiste à brancher votre modèle d’expression dans la classe Matrix
. Ceci est essentiellement réalisé en fournissant une implémentation pour Matrix::operator=()
, qui prend le modèle d'expression comme argument et l'évalue en une seule fois, comme vous l'avez fait "manuellement" avant:
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;
};