Szukaj…


Podstawowe szablony wyrażeń w elementarnych wyrażeniach algebraicznych


Wprowadzenie i motywacja


Szablony wyrażeń (oznaczone poniżej jako ET ) to potężna technika metaprogramowania szablonów, używana do przyspieszenia obliczeń czasami dość drogich wyrażeń. Jest szeroko stosowany w różnych domenach, na przykład przy implementacji bibliotek algebry liniowej.

W tym przykładzie rozważmy kontekst liniowych obliczeń algebraicznych. Mówiąc dokładniej, obliczenia obejmujące tylko operacje elementowe . Tego rodzaju obliczenia są najbardziej podstawowymi zastosowaniami ET i służą jako dobre wprowadzenie do tego, jak ET działają wewnętrznie.

Spójrzmy na motywujący przykład. Rozważ obliczenie wyrażenia:

Vector vec_1, vec_2, vec_3;

// Initializing vec_1, vec_2 and vec_3.

Vector result = vec_1 + vec_2*vec_3;

Dla uproszczenia założę, że klasa Vector i operacja + (wektor plus: operacja elementarna plus) i operacja * (tutaj oznacza produkt wewnętrzny wektora: również operacja elementarna) są poprawnie zaimplementowane, ponieważ jakie powinny być, matematycznie.

W konwencjonalnej implementacji bez użycia ET (lub innych podobnych technik), co najmniej pięć konstrukcji instancji Vector ma miejsce w celu uzyskania końcowego result :

  1. Trzy wystąpienia odpowiadające vec_1 , vec_2 i vec_3 .
  2. Tymczasowa instancja Vector _tmp , reprezentująca wynik _tmp = vec_2*vec_3; .
  3. Wreszcie przy odpowiednim zastosowaniu optymalizacji wartości zwracanej konstrukcja result końcowego w result = vec_1 + _tmp; .

Implementacja przy użyciu ET może wyeliminować tworzenie tymczasowego Vector _tmp w 2, pozostawiając w ten sposób tylko cztery konstrukcje instancji Vector . Co ciekawsze, rozważ następujące wyrażenie, które jest bardziej złożone:

Vector result = vec_1 + (vec_2*vec3 + vec_1)*(vec_2 + vec_3*vec_1);

W sumie będą też cztery konstrukcje instancji Vector : vec_1, vec_2, vec_3 i result . Innymi słowy, w tym przykładzie, w którym uczestniczą tylko operacje elementarne , gwarantuje się, że z obliczeń pośrednich nie zostaną utworzone żadne obiekty tymczasowe .


Jak działają ET


Zasadniczo, ET dla wszelkich obliczeń algebraicznych składają się z dwóch elementów:

  1. Czyste wyrażenia algebraiczne ( PAE ): są to proxy / abstrakcje wyrażeń algebraicznych. Czysta algebraiczna nie wykonuje rzeczywistych obliczeń, jest jedynie abstrakcją / modelowaniem przepływu pracy obliczeń. PAE może być modelem danych wejściowych lub wyjściowych dowolnych wyrażeń algebraicznych . Wystąpienia PAE są często uważane za tanie do skopiowania.
  2. Leniwe oceny : które są implementacją prawdziwych obliczeń. W poniższym przykładzie zobaczymy, że dla wyrażeń obejmujących tylko operacje elementowe, leniwe oceny mogą implementować rzeczywiste obliczenia wewnątrz operacji dostępu indeksowanego do wyniku końcowego, tworząc w ten sposób schemat oceny na żądanie: obliczenia nie są wykonywane tylko jeśli dostęp do wyniku końcowego jest wymagany / wymagany.

A konkretnie, w jaki sposób wdrażamy ET w tym przykładzie? Przejdźmy teraz przez to.

Zawsze bierz pod uwagę następujący fragment kodu:

Vector vec_1, vec_2, vec_3;

// Initializing vec_1, vec_2 and vec_3.

Vector result = vec_1 + vec_2*vec_3;

Wyrażenie do obliczenia wyniku można dodatkowo podzielić na dwa podwyrażenia:

  1. Wektor plus wyrażenie (oznaczone jako plus_expr )
  2. Wektorowe wyrażenie produktu wewnętrznego (oznaczone jako wewnętrzna_produkt_proc ).

To, co robią ET , to:

  • Zamiast obliczać od razu każde podwyrażenie , ET najpierw modelują całe wyrażenie za pomocą struktury graficznej. Każdy węzeł na wykresie reprezentuje PAE . Połączenie brzegowe węzłów reprezentuje rzeczywisty przepływ obliczeń. Tak więc dla powyższego wyrażenia otrzymujemy następujący wykres:

           result = plus_expr( vec_1, innerprod_expr(vec_2, vec_3) )
              /   \
             /     \
            /       \
           /   innerprod_expr( vec_2, vec_3 )
          /         /  \
         /         /    \
        /         /      \
     vec_1     vec_2    vec_3
    
  • Ostateczne obliczenia są realizowane przez przeglądanie hierarchii wykresów : ponieważ tutaj mamy do czynienia tylko z operacjami opartymi na elementach , obliczenia każdej zindeksowanej wartości w result można wykonać niezależnie : ostateczna ocena result może zostać leniwie przełożona na element- mądra ocena każdego elementu result . Innymi słowy, ponieważ obliczenia elementu result , elem_res , można wyrazić za pomocą odpowiednich elementów w vec_1 ( elem_1 ), vec_2 ( elem_2 ) i vec_3 ( elem_3 ) jako:

    elem_res = elem_1 + elem_2*elem_3;
    

dlatego nie ma potrzeby tworzenia tymczasowego Vector do przechowywania wyniku pośredniego produktu wewnętrznego: całe obliczenia dla jednego elementu można wykonać w całości i zakodować w operacji dostępu indeksowanego .


Oto przykładowe kody w akcji.


Plik vec.hh: wrapper dla std :: vector, używany do wyświetlania dziennika, gdy wywoływana jest konstrukcja.


#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

Plik expr.hh: implementacja szablonów wyrażeń dla operacji elementarnych (wektor plus i wewnętrzny produkt wektor)


Podzielmy to na sekcje.

  1. Sekcja 1 implementuje klasę podstawową dla wszystkich wyrażeń. Wykorzystuje ciekawie powtarzający się wzorzec szablonu ( CRTP ).
  2. W sekcji 2 zaimplementowano pierwszą PAE : terminal , który jest tylko opakowaniem (const referen) struktury danych wejściowych zawierającej rzeczywistą wartość wejściową do obliczeń.
  3. W sekcji 3 zaimplementowano drugą operację PAE : operacja binarna , która jest szablonem klasy używanym później dla vector_plus i vector_innerprod. Jest on sparametryzowany przez rodzaj operacji , PAE po lewej stronie i PAE po prawej stronie . Rzeczywiste obliczenia są kodowane w operatorze z dostępem indeksowanym.
  4. Rozdział 4 definiuje operacje vector_plus i vector_innerprod jako operację elementarną . Przeciąża także operatora + i * dla PAE : tak, że te dwie operacje również zwracają 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

Plik main.cc: testuj plik 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;
}

Oto jedno z możliwych danych wyjściowych po skompilowaniu z -O3 -std=c++14 przy użyciu 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> > > >

Obserwacje to:

  • Użycie ET zapewnia w tym przypadku raczej znaczny wzrost wydajności (> 3x).
  • Tworzenie tymczasowego obiektu Vector zostało wyeliminowane. Podobnie jak w przypadku ET , ctor jest wywoływany tylko raz.
  • Boost :: demangle został użyty do wizualizacji rodzaju zwrotu ET przed konwersją: wyraźnie skonstruował dokładnie ten sam wykres wyrażeń, co pokazano powyżej.

Wady i zastrzeżenia


  • Oczywistą wadą ET jest krzywa uczenia się, złożoność implementacji i trudności z utrzymaniem kodu. W powyższym przykładzie, w którym brane są pod uwagę tylko operacje elementowe, implementacja zawiera już ogromną liczbę plansz, nie mówiąc już o prawdziwym świecie, w którym przy każdym obliczeniu występują bardziej złożone wyrażenia algebraiczne i nie zachodzi już niezależność elementarna (na przykład mnożenie macierzy ), trudność będzie wykładnicza.

  • Innym zastrzeżeniem związanym z używaniem ET jest to, że dobrze się bawią słowem kluczowym auto . Jak wspomniano powyżej, PAE są zasadniczo proxy: i proxy zasadniczo nie działają dobrze z auto . Rozważ następujący przykład:

     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.
    

Tutaj w każdej iteracji pętli for wynik zostanie ponownie oceniony , ponieważ wykres wyrażeń zamiast obliczonej wartości jest przekazywany do pętli for.


Istniejące biblioteki implementujące ET


  • boost :: proto to potężna biblioteka pozwalająca definiować własne reguły i gramatykę dla własnych wyrażeń i wykonywać je przy użyciu ET .
  • Eigen to biblioteka algebry liniowej, która skutecznie realizuje różne obliczenia algebraiczne przy użyciu ET .

Podstawowy przykład ilustrujący szablony wyrażeń

Szablon wyrażenia to technika optymalizacji czasu kompilacji stosowana głównie w obliczeniach naukowych. Jego głównym celem jest uniknięcie niepotrzebnych tymczasowości i optymalizacja obliczeń pętli za pomocą pojedynczego przejścia (zazwyczaj podczas wykonywania operacji na agregatach numerycznych). Początkowo opracowano szablony wyrażeń, aby ominąć nieefektywność przeciążania operatora naiwnego podczas wdrażania liczbowych typów Array lub Matrix . Równoważną terminologię dotyczącą szablonów wyrażeń wprowadził Bjarne Stroustrup, który nazywa je „połączonymi operacjami” w najnowszej wersji swojej książki „The C ++ Programming Language”.

Zanim zaczniesz nurkować w szablonach wyrażeń, powinieneś zrozumieć, dlaczego ich potrzebujesz. Aby to zilustrować, rozważ bardzo prostą klasę Matrix podaną poniżej:

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;
}

Biorąc pod uwagę poprzednią definicję klasy, możesz teraz pisać wyrażenia Matrix, takie jak:

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 

Jak pokazano powyżej, możliwość przeciążenia operator+() zapewnia notację, która naśladuje naturalny zapis matematyczny dla macierzy.

Niestety poprzednie wdrożenie jest również bardzo nieefektywne w porównaniu z równoważną wersją „ręcznie wykonaną”.

Aby zrozumieć, dlaczego, musisz rozważyć, co dzieje się, gdy piszesz wyrażenie takie jak Matrix d = a + b + c . To w rzeczywistości rozwija się do ((a + b) + c) lub operator+(operator+(a, b), c) . Innymi słowy, pętla wewnątrz operator+() jest wykonywana dwukrotnie, podczas gdy można ją łatwo wykonać w jednym przejściu. Powoduje to również utworzenie 2 tymczasowych elementów, co dodatkowo obniża wydajność. Zasadniczo, dodając elastyczność korzystania z notacji zbliżonej do jej matematycznego odpowiednika, sprawiłeś, że klasa Matrix jest bardzo nieefektywna.

Na przykład bez przeciążania operatora można zaimplementować znacznie bardziej wydajne sumowanie macierzy za pomocą jednego przejścia:

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;
}

Poprzedni przykład ma jednak swoje wady, ponieważ tworzy znacznie bardziej skomplikowany interfejs dla klasy Matrix (należy rozważyć takie metody, jak Matrix::add2() , Matrix::AddMultiply() i tak dalej).

Zamiast tego cofnijmy się o krok i zobaczmy, jak możemy dostosować przeciążenie operatora, aby działał w bardziej wydajny sposób

Problem wynika z faktu, że wyrażenie Matrix d = a + b + c jest oceniane zbyt „chętnie”, zanim miałeś okazję zbudować całe drzewo wyrażeń. Innymi słowy, to, co naprawdę chcesz osiągnąć, to ocenić a + b + c w jednym przebiegu i tylko wtedy, gdy faktycznie musisz przypisać wynikowe wyrażenie do d .

Jest to podstawowa idea szablonów wyrażeń: zamiast tego, aby operator+() natychmiast ocenił wynik dodania dwóch instancji Matrix, zwróci „szablon wyrażenia” do przyszłej oceny po zbudowaniu całego drzewa wyrażeń.

Na przykład tutaj jest możliwa implementacja szablonu wyrażenia odpowiadającego sumowaniu 2 typów:

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;
};

A oto zaktualizowana wersja operator+()

template <typename LHS, typename RHS>
MatrixSum<LHS, RHS> operator+(const LHS& lhs, const LHS& rhs) {
    return MatrixSum<LHS, RHS>(lhs, rhs);
}

Jak widać, operator+() nie zwraca już „chętnej oceny” wyniku dodania 2 instancji Matrix (co byłoby inną instancją Matrix), ale szablon wyrażenia reprezentujący operację dodawania. Najważniejsze, aby pamiętać, że wyrażenie nie zostało jeszcze ocenione. Zawiera jedynie odniesienia do swoich operandów.

W rzeczywistości nic nie MatrixSum<> na MatrixSum<> aby utworzyć instancję szablonu wyrażenia MatrixSum<> w następujący sposób:

MatrixSum<Matrix<double>, Matrix<double> > SumAB(a, b);

Możesz jednak na późniejszym etapie, kiedy rzeczywiście potrzebujesz wyniku sumowania, oceń wyrażenie d = a + b w następujący sposób:

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);
    }
}

Jak widać, kolejna zaleta korzystania z szablonu ekspresji, jest to, że masz w zasadzie udało się oszacować sumę i a b i przypisać go do d w jednym przejściu.

Ponadto nic nie stoi na przeszkodzie, aby łączyć wiele szablonów wyrażeń. Na przykład a + b + c dałoby następujący szablon wyrażenia:

MatrixSum<MatrixSum<Matrix<double>, Matrix<double> >, Matrix<double> > SumABC(SumAB, c);

I tutaj ponownie możesz ocenić wynik końcowy za pomocą jednego przejścia:

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);
    }
}

Wreszcie ostatnim elementem układanki jest podłączenie szablonu wyrażenia do klasy Matrix . Zasadniczo osiąga się to przez zapewnienie implementacji Matrix::operator=() , która bierze szablon wyrażenia jako argument i ocenia go w jednym przebiegu, tak jak wcześniej „ręcznie”:

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;
};


Modified text is an extract of the original Stack Overflow Documentation
Licencjonowany na podstawie CC BY-SA 3.0
Nie związany z Stack Overflow