C++
Expressie sjablonen
Zoeken…
Basisuitdrukkingssjablonen op elementgewijze algebraïsche uitdrukkingen
Introductie en motivatie
Expressiesjablonen (hierna aangeduid als ET's ) zijn een krachtige meta-programmeertechniek voor sjablonen, die wordt gebruikt om berekeningen van soms vrij dure expressies te versnellen. Het wordt veel gebruikt in verschillende domeinen, bijvoorbeeld bij de implementatie van lineaire algebra-bibliotheken.
Overweeg voor dit voorbeeld de context van lineaire algebraïsche berekeningen. Meer specifiek, berekeningen die alleen elementgewijze bewerkingen omvatten. Dit soort berekeningen zijn de meest basistoepassingen van ET's en ze vormen een goede introductie voor de manier waarop ET 's intern werken.
Laten we eens kijken naar een motiverend voorbeeld. Overweeg de berekening van de uitdrukking:
Vector vec_1, vec_2, vec_3;
// Initializing vec_1, vec_2 and vec_3.
Vector result = vec_1 + vec_2*vec_3;
Hier zal ik omwille van de eenvoud aannemen dat de klasse Vector
en bewerking + (vector plus: elementgewijs plus bewerking) en bewerking * (hier betekent vector binnenproduct: ook elementgewijs bediening) beide correct zijn geïmplementeerd, zoals hoe ze zouden moeten zijn, wiskundig.
In een gebruikelijke implementatie zonder ET (of soortgelijke technieken), ten minste vijf constructies Vector
gevallen plaatsvinden teneinde het uiteindelijke te verkrijgen result
:
- Drie instanties die overeenkomen met
vec_1
,vec_2
envec_3
. - Een tijdelijke
Vector
instantie_tmp
, die het resultaat vertegenwoordigt van_tmp = vec_2*vec_3;
. - Eindelijk met correct gebruik van de optimalisatie van de retourwaarde , de constructie van het
result
inresult = vec_1 + _tmp;
.
Implementatie met behulp van ET's kan de creatie van tijdelijke Vector _tmp
in 2 Vector _tmp
, waardoor slechts vier constructies van Vector
exemplaren Vector _tmp
. Interessanter is dat de volgende uitdrukking complexer is:
Vector result = vec_1 + (vec_2*vec3 + vec_1)*(vec_2 + vec_3*vec_1);
Er zullen in totaal ook vier constructies van Vector
instanties zijn: vec_1, vec_2, vec_3
en result
. Met andere woorden, in dit voorbeeld, waar alleen elementaire bewerkingen zijn betrokken , is het gegarandeerd dat er geen tijdelijke objecten worden gemaakt op basis van tussentijdse berekeningen .
Hoe werken ET's?
Kort gezegd bestaan ET's voor alle algebraïsche berekeningen uit twee bouwstenen:
- Pure algebraïsche uitdrukkingen ( PAE ): dit zijn proxy's / abstracties van algebraïsche uitdrukkingen. Een pure algebraïsche berekent geen daadwerkelijke berekeningen, het zijn slechts abstracties / modellering van de berekeningswerkstroom. Een PAE kan een model zijn van de invoer of de uitvoer van alle algebraïsche uitdrukkingen . Gevallen van PAE 's worden vaak als goedkoop beschouwd om te kopiëren.
- Luie evaluaties : die implementatie van echte berekeningen zijn. In het volgende voorbeeld zullen we zien dat voor expressies die alleen element-gewijze bewerkingen betreffen, luie evaluaties daadwerkelijke berekeningen kunnen implementeren in de geïndexeerde toegang op het eindresultaat, waardoor een schema van evaluatie op aanvraag wordt gemaakt: er wordt geen berekening uitgevoerd alleen als het eindresultaat wordt geopend / gevraagd.
Dus, specifiek, hoe implementeren we ET's in dit voorbeeld? Laten we er nu doorheen lopen.
Overweeg altijd het volgende codefragment:
Vector vec_1, vec_2, vec_3;
// Initializing vec_1, vec_2 and vec_3.
Vector result = vec_1 + vec_2*vec_3;
De uitdrukking om het resultaat te berekenen kan verder worden opgedeeld in twee sub-uitdrukkingen:
- Een vector plus expressie (aangeduid als plus_expr )
- Een vector innerlijke productexpressie (aangeduid als innerprod_expr ).
Wat ET's doen is het volgende:
In plaats van elke sub-uitdrukking meteen te berekenen, modelleren ET's eerst de gehele uitdrukking met behulp van een grafische structuur. Elk knooppunt in de grafiek vertegenwoordigt een PAE . De randverbinding van de knooppunten vertegenwoordigt de feitelijke berekeningsstroom. Voor de bovenstaande uitdrukking verkrijgen we de volgende grafiek:
result = plus_expr( vec_1, innerprod_expr(vec_2, vec_3) ) / \ / \ / \ / innerprod_expr( vec_2, vec_3 ) / / \ / / \ / / \ vec_1 vec_2 vec_3
De uiteindelijke berekening wordt geïmplementeerd door de grafische hiërarchie te doorzoeken : omdat we hier alleen elementaire bewerkingen behandelen, kan de berekening van elke geïndexeerde waarde in het
result
onafhankelijk worden gedaan : de uiteindelijke evaluatie van hetresult
kan lui worden uitgesteld tot een element- wijze evaluatie van elkresult
. Met andere woorden, aangezien de berekening van eenresult
,elem_res
, kan worden uitgedrukt met behulp van overeenkomstige elementen invec_1
(elem_1
),vec_2
(elem_2
) envec_3
(elem_3
) als:elem_res = elem_1 + elem_2*elem_3;
het is daarom niet nodig om een tijdelijke Vector
te maken om het resultaat van een tussenliggend binnenproduct op te slaan: de hele berekening voor één element kan volledig worden uitgevoerd en worden gecodeerd in de geïndexeerde toegangsbewerking .
Hier zijn de voorbeeldcodes in actie.
Bestand vec.hh: wrapper voor std :: vector, gebruikt om het logboek te tonen wanneer een constructie wordt aangeroepen.
#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
Bestand expr.hh: implementatie van expressiesjablonen voor elementgewijze bewerkingen (vector plus en vector binnenproduct)
Laten we het in secties onderverdelen.
- Sectie 1 implementeert een basisklasse voor alle uitdrukkingen. Het maakt gebruik van het Curiously Recurring Template Pattern ( CRTP ).
- Sectie 2 implementeert de eerste PAE : een terminal , die slechts een wrapper (const-referentie) is van een invoergegevensstructuur die de werkelijke invoerwaarde voor berekening bevat.
- Sectie 3 implementeert de tweede PAE : binary_operation , een klassensjabloon die later wordt gebruikt voor vector_plus en vector_innerprod. Het wordt bepaald door het type bewerking , de PAE aan de linkerkant en de PAE aan de rechterkant . De daadwerkelijke berekening is gecodeerd in de geïndexeerde toegangsexploitant.
- Sectie 4 definieert vector_plus- en vector_innerprod-bewerkingen als elementgewijze bewerking . Het overbelast operator + en * ook voor PAE 's: zodat deze twee bewerkingen ook PAE retourneren.
#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
Bestand main.cc: test src-bestand
# 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;
}
Hier is een mogelijke uitvoer wanneer gecompileerd met -O3 -std=c++14
met 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> > > >
De waarnemingen zijn:
- Het gebruik van ET's levert in dit geval een vrij significante prestatieverbetering op (> 3x).
- Het maken van een tijdelijk Vector-object is geëlimineerd. Net als in het geval van ET's wordt ctor slechts eenmaal genoemd.
- Boost :: demangle werd gebruikt om het type ET's-rendement voor conversie te visualiseren: het construeerde duidelijk exact dezelfde expressiegrafiek die hierboven werd getoond.
Nadelen en voorbehouden
Een duidelijk nadeel van ET's is de leercurve, de complexiteit van de implementatie en de moeilijkheid van codeonderhoud . In het bovenstaande voorbeeld, waarin alleen elementaire bewerkingen worden overwogen, bevat de implementatie al een enorme hoeveelheid ketelplaten, laat staan in de echte wereld, waar complexere algebraïsche uitdrukkingen in elke berekening voorkomen en elementgewijze onafhankelijkheid niet langer geldt (bijvoorbeeld matrixvermenigvuldiging) ), de moeilijkheid zal exponentieel zijn.
Een ander nadeel van het gebruik van ET's is dat ze goed spelen met het
auto
trefwoord. Zoals hierboven vermeld, zijn PAE 's in wezen proxy's: en proxy's spelen in principe niet goed metauto
. Overweeg het volgende voorbeeld: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.
In elke iteratie van de for-lus wordt het resultaat opnieuw geëvalueerd , omdat de expressiegrafiek in plaats van de berekende waarde wordt doorgegeven aan de for-lus.
Bestaande bibliotheken die ET's implementeren
- boost :: proto is een krachtige bibliotheek waarmee u uw eigen regels en grammatica's voor uw eigen expressies kunt definiëren en kunt uitvoeren met behulp van ET's .
- Eigen is een bibliotheek voor lineaire algebra die verschillende algebraïsche berekeningen efficiënt implementeert met behulp van ET's .
Een basisvoorbeeld ter illustratie van expressiesjablonen
Een expressiesjabloon is een compilatie-optimalisatietechniek die meestal wordt gebruikt in wetenschappelijk computergebruik. Het belangrijkste doel is om onnodige tijdelijke functies te vermijden en lusberekeningen te optimaliseren met een enkele doorgang (meestal bij het uitvoeren van bewerkingen op numerieke aggregaten). Er werden aanvankelijk expressiesjablonen ontworpen om de inefficiënties van naïeve overbelasting van de operator te omzeilen bij het implementeren van numerieke Array
of Matrix
. Een vergelijkbare terminologie voor expressiesjablonen is geïntroduceerd door Bjarne Stroustrup, die ze "fused operations" noemt in de nieuwste versie van zijn boek, "The C ++ Programming Language".
Voordat u daadwerkelijk in expressiesjablonen duikt, moet u begrijpen waarom u ze in de eerste plaats nodig hebt. Om dit te illustreren, overweeg de zeer eenvoudige Matrix-klasse hieronder:
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;
}
Gezien de vorige klassedefinitie, kunt u nu Matrix-expressies schrijven zoals:
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
Zoals hierboven geïllustreerd, biedt de mogelijkheid om operator+()
te overbelasten een notatie die de natuurlijke wiskundige notatie voor matrices nabootst.
Helaas is de vorige implementatie ook zeer inefficiënt in vergelijking met een equivalente "handgemaakte" versie.
Om te begrijpen waarom, moet je overwegen wat er gebeurt als je een uitdrukking schrijft zoals Matrix d = a + b + c
. Dit wordt in feite uitgebreid naar ((a + b) + c)
of operator+(operator+(a, b), c)
. Met andere woorden, de lus binnen operator+()
wordt twee keer uitgevoerd, terwijl deze gemakkelijk in een enkele doorgang had kunnen worden uitgevoerd. Dit resulteert ook in het creëren van 2 tijdelijke functies, wat de prestaties verder verslechtert. Door de flexibiliteit toe te voegen om een notatie te gebruiken die dicht bij de wiskundige tegenhanger ervan ligt, hebt u de Matrix
klasse in feite ook zeer inefficiënt gemaakt.
U kunt bijvoorbeeld zonder overbelasting van de operator een veel efficiëntere Matrix-sommatie implementeren met een enkele doorgang:
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;
}
Het vorige voorbeeld heeft echter zijn eigen nadelen omdat het een veel ingewikkelder interface voor de klasse Matrix creëert (u zou methoden zoals Matrix::add2()
, Matrix::AddMultiply()
enzovoort moeten overwegen).
Laten we in plaats daarvan een stapje terug doen en kijken hoe we de overbelasting van de operator kunnen aanpassen om op een efficiëntere manier te presteren
Het probleem komt voort uit het feit dat de uitdrukking Matrix d = a + b + c
te "gretig" wordt geëvalueerd voordat u de gelegenheid hebt gehad om de hele boomstructuur van de uitdrukking te bouwen. Met andere woorden, wat je echt wilt bereiken is om a + b + c
in één keer te evalueren en slechts eenmaal je de resulterende expressie aan d
moet toewijzen.
Dit is het kernidee achter expressiesjablonen: in plaats van dat operator+()
onmiddellijk het resultaat evalueert van het toevoegen van twee Matrix-instanties, retourneert het een "expressiesjabloon" voor toekomstige evaluatie zodra de hele expressieboom is gebouwd.
Hier is bijvoorbeeld een mogelijke implementatie voor een expressiesjabloon die overeenkomt met de optelling van 2 typen:
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;
};
En hier is de bijgewerkte versie van operator+()
template <typename LHS, typename RHS>
MatrixSum<LHS, RHS> operator+(const LHS& lhs, const LHS& rhs) {
return MatrixSum<LHS, RHS>(lhs, rhs);
}
Zoals u ziet, retourneert operator+()
niet langer een "enthousiaste evaluatie" van het resultaat van het toevoegen van 2 Matrix-instanties (wat een andere Matrix-instantie zou zijn), maar in plaats daarvan een expressiesjabloon die de toevoegingsbewerking vertegenwoordigt. Het belangrijkste om te onthouden is dat de uitdrukking nog niet is geëvalueerd. Het bevat alleen verwijzingen naar zijn operanden.
Niets MatrixSum<>
als volgt van om de MatrixSum<>
expressiesjabloon als volgt te instantiëren:
MatrixSum<Matrix<double>, Matrix<double> > SumAB(a, b);
U kunt echter in een later stadium, wanneer u het resultaat van de sommatie echt nodig hebt, de uitdrukking d = a + b
als volgt evalueren:
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);
}
}
Zoals u kunt zien, is een ander voordeel van het gebruik van een expressiesjabloon dat u er in feite in bent geslaagd om de som van a
en b
te evalueren en deze in één keer aan d
toe te wijzen.
Ook houdt niets u tegen om meerdere expressiesjablonen te combineren. Een a + b + c
zou bijvoorbeeld resulteren in de volgende expressiesjabloon:
MatrixSum<MatrixSum<Matrix<double>, Matrix<double> >, Matrix<double> > SumABC(SumAB, c);
En ook hier kunt u het eindresultaat evalueren met een enkele 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);
}
}
Ten slotte is het laatste stukje van de puzzel om je expressiesjabloon daadwerkelijk in de Matrix
klasse te Matrix
. Dit wordt in wezen bereikt door een implementatie te bieden voor Matrix::operator=()
, die de expressiesjabloon als argument neemt en in één keer evalueert, zoals u eerder "handmatig" deed:
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;
};