수색…


요소 와이즈 대수 표현식에 대한 기본 표현식 템플릿


소개 및 동기 부여


표현 템플릿 ( ET )은 강력한 템플릿 메타 프로그래밍 기법으로 때로는 상당히 비싼 표현의 계산 속도를 높이는 데 사용됩니다. 선형 대수학 라이브러리의 구현과 같이 다른 영역에서 널리 사용됩니다.

이 예제의 경우 선형 대수 연산의 컨텍스트를 고려하십시오. 보다 구체적으로, 요소 별 연산만을 포함하는 계산입니다. 계산의이 종류는 외계인의 가장 기본적인 응용 프로그램입니다, 그들은 외계인이 내부적으로 어떻게 작동하는지에 대한 좋은 소개 역할을합니다.

동기 부여 예제를 살펴 보겠습니다. 표현식의 계산을 고려하십시오.

Vector vec_1, vec_2, vec_3;

// Initializing vec_1, vec_2 and vec_3.

Vector result = vec_1 + vec_2*vec_3;

여기서 간단히하기 위해 Vector 와 operation 클래스 (vector plus : element-wise plus operation)와 operation * (여기서 vector inner product : element-wise operation)은 모두 올바르게 구현되었다고 가정합니다. 그들이 어떻게해야하는지, 수학적으로.

ET (또는 다른 유사한 기술)를 사용하지 않는 종래의 구현 에서 , 최종 result 를 얻으려면 Vector 인스턴스의 적어도 다섯 가지 구조가 result .

  1. vec_1 , vec_2vec_3 해당하는 세 개의 인스턴스.
  2. 임시 Vector 인스턴스 _tmp 의 결과를 나타내는, _tmp = vec_2*vec_3; .
  3. 마지막으로 반환 값 최적화 를 적절히 사용 result = vec_1 + _tmp; 의 최종 resultresult = vec_1 + _tmp; .

ET 를 사용하여 구현하면 2에서 임시 Vector _tmp 생성을 제거 할 수 있으므로 Vector 인스턴스의 구조가 4 개만 남습니다. 더 흥미롭게도 다음과 같은 식은 더 복잡합니다.

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

vec_1, vec_2, vec_3result 총 4 가지 Vector 인스턴스가 result 됩니다. 즉, 요소 별 작업 만 관련된 이 예제에서는 중간 계산에서 임시 개체를 만들지 않습니다 .


ET는 어떻게 작동합니까?


기본적으로 말하자면, 대수적 계산을위한 ET 는 두 개의 빌딩 블록으로 구성됩니다.

  1. 순수 대수 표현 ( Pure Algebraic Express , PAE ) : 대수 표현의 프록시 / 추상입니다. 순수 대수학은 실제 계산을 수행하지 않으며, 단지 계산 워크 플로우의 추상화 / 모델링 일뿐입니다. PAE는 대수 식의 입력 또는 출력 모델 일 수 있습니다. PAE 의 인스턴스는 종종 저렴한 복사로 간주됩니다.
  2. 게으른 평가 : 실제 계산을 구현합니다. 다음 예에서는 요소 별 작업 만 포함하는 식에서 지연 테스트는 최종 결과에 대한 인덱싱 된 액세스 연산 내에서 실제 계산을 구현할 수 있으므로 주문형 평가 체계를 만듭니다. 즉 계산이 수행되지 않습니다 최종 결과에 액세스 / 요청한 경우에만

그래서, 구체적으로이 예제에서 ET 를 어떻게 구현합니까? 이제 살펴 보겠습니다.

항상 다음 코드 스 니펫을 고려하십시오.

Vector vec_1, vec_2, vec_3;

// Initializing vec_1, vec_2 and vec_3.

Vector result = vec_1 + vec_2*vec_3;

결과를 계산하는 표현식은 두 개의 하위 표현식으로 더 분해 될 수 있습니다.

  1. 벡터 더하기 표현식 ( plus_expr으로 표시)
  2. 벡터 내부 제품 식 ( innerprod_expr표시됨 ).

ET 가 수행하는 작업은 다음과 같습니다.

  • 각 하위 표현을 바로 계산하는 대신 ET는 먼저 그래픽 구조를 사용하여 전체 표현을 모델링합니다. 그래프의 각 노드는 PAE를 나타냅니다. 노드의 에지 연결은 실제 계산 흐름을 나타냅니다. 위의 식에 대해 다음 그래프를 얻습니다.

           result = plus_expr( vec_1, innerprod_expr(vec_2, vec_3) )
              /   \
             /     \
            /       \
           /   innerprod_expr( vec_2, vec_3 )
          /         /  \
         /         /    \
        /         /      \
     vec_1     vec_2    vec_3
    
  • 최종 계산은 그래프 계층을 찾고 의해 구현된다 : 여기에만 소자 연산의 취급하고 있기 때문에, 각각의 인덱스 값의 계산 result 독립적으로 수행 할 수있다 : 최종 평가 result 느리게 엘리먼트 - 연기 될 수있다 각 요소의 현명한 평가 result . 즉, result 요소 elem_res 의 계산은 vec_1 ( elem_1 ), vec_2 ( elem_2 ) 및 vec_3 ( elem_3 )의 해당 요소를 사용하여 다음과 같이 표현할 수 있습니다.

    elem_res = elem_1 + elem_2*elem_3;
    

따라서 중간 중간 생성물의 결과를 저장하기 위해 임시 Vector 를 만들 필요가 없습니다. 한 요소에 대한 전체 계산을 모두 수행하고 인덱싱 된 액세스 연산 내에서 인코딩 할 수 있습니다 .


다음은 작동중인 예제 코드입니다.


파일 vec.hh : std :: vector의 래퍼 (wrapper for std :: vector). 생성이 호출 될 때 로그를 보여주기 위해 사용됩니다.


#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

파일 expr.hh : 요소 와이즈 연산을위한 표현 템플릿 구현 (벡터 플러스 및 벡터 내부 제품)


섹션으로 나누십시오.

  1. 섹션 1은 모든 표현식에 대한 기본 클래스를 구현합니다. Curiously Recurring Template Pattern ( CRTP )을 사용합니다.
  2. 섹션 2는 첫 번째 PAE를 구현합니다. 터미널 은 계산을위한 실제 입력 값을 포함하는 입력 데이터 구조의 래퍼 (const 참조)입니다.
  3. 3 절에서는 두 번째 PAE 인 binary_operation을 구현합니다.이 작업은 나중에 vector_plus 및 vector_innerprod에 사용되는 클래스 템플릿입니다. 작동 유형 , 왼쪽 측면 PAE오른쪽 측면 PAE로 매개 변수화됩니다. 실제 계산은 인덱싱 된 액세스 연산자에서 인코딩됩니다.
  4. 4 절에서는 vector_plus 및 vector_innerprod 연산을 요소 별 연산으로 정의 합니다 . 또한 PAE에 대해 연산자 + 및 *를 오버로드합니다. 이러한 두 연산도 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

main.cc 파일 : 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;
}

다음은 GCC 5.3을 사용하여 -O3 -std=c++14 컴파일했을 때 가능한 출력입니다.

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

관찰 내용은 다음과 같습니다.

  • ET를 사용하면 이 경우 상당한 성능 향상 얻을 수 있습니다 (3 배 초과).
  • 임시 Vector 객체의 생성이 제거되었습니다. ET의 경우와 마찬가지로 ctor는 한 번만 호출됩니다.
  • Boost :: demangle은 변환 전에 반환되는 ET의 유형을 시각화하는 데 사용되었습니다. 이는 위에서 설명한 동일한 표현식 그래프를 명확하게 구성한 것입니다.

드로우 백 및주의 사항


  • ET 의 명백한 단점은 학습 곡선, 구현의 복잡성 및 코드 유지 관리의 어려움이다. 요소 별 연산 만 고려되는 위의 예제에서 구현은 모든 계산에서보다 복잡한 대수 표현이 발생하고 요소 별 독립성이 더 이상 유지되지 않는 실세계에서 보자면 이미 막대한 양의 상용구를 포함합니다 (예 : 행렬 곱셈 ), 어려움은 기하 급수적 일 것입니다.

  • ET 를 사용하는 또 다른 경고는 auto 키워드로 잘 작동한다는 것입니다. 위에서 언급했듯이 PAE 는 본질적으로 프록시입니다. 프록시는 기본적으로 auto 잘 작동하지 않습니다. 다음 예제를 고려하십시오.

     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.
    

여기 에서 for 루프를 반복 할 때마다 계산 된 값 대신 식 그래프가 for 루프에 전달되므로 결과가 다시 계산됩니다.


ET를 구현하는 기존 라이브러리


  • boost :: proto 는 자신 만의 표현식에 대한 자신 만의 규칙과 문법을 정의하고 ET를 사용하여 실행할 수있게 해주는 강력한 라이브러리입니다.
  • EigenET를 사용하여 다양한 대수 연산을 효율적으로 구현하는 선형 대수학 라이브러리입니다.

표현 템플릿을 보여주는 기본 예제

표현 템플릿은 주로 과학 계산에 사용되는 컴파일 타임 최적화 기법입니다. 주 목적은 불필요한 임시 변수를 피하고 단일 패스를 사용하여 루프 계산을 최적화하는 것입니다 (일반적으로 수치 집계에 대한 연산을 수행 할 때). 표현식 템플릿은 초기에 숫자 Array 또는 Matrix 유형을 구현할 때 순진한 연산자 오버로딩의 비효율을 피하기 위해 고안되었습니다. Bjarne Stroustrup은 "C ++ 프로그래밍 언어"라는 책의 최신 버전에서 "융합 작업"이라고 부르는 표현 템플릿에 대한 동일한 용어를 도입했습니다.

실제로 표현 템플릿으로 들어가기 전에 먼저 왜 표현 템플릿이 필요한지 이해해야합니다. 이를 설명하기 위해 아래에 주어진 매우 간단한 Matrix 클래스를 고려하십시오.

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

이전 클래스 정의가 주어지면 다음과 같은 Matrix 표현식을 작성할 수 있습니다.

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 

위에서 설명한 바와 같이, operator+() 를 오버로드 할 수 있다는 것은 행렬에 대한 자연 수학 표기법을 모방 한 표기법을 제공합니다.

불행하게도, 이전의 구현은 동등한 "수작업"버전에 비해 매우 비효율적입니다.

이유를 이해하려면 Matrix d = a + b + c 와 같은 표현식을 작성할 때 어떤 일이 발생하는지 고려해야합니다. 이것은 사실 ((a + b) + c) 또는 operator+(operator+(a, b), c) 됩니다. 다시 말해, operator+() 내부의 루프는 두 번 실행되는 반면 단일 패스에서 쉽게 수행 될 수 있습니다. 또한 이로 인해 2 개의 임시 생성이 생성되어 성능을 더욱 저하시킵니다. 본질적으로, 수학적 대응 물과 가까운 표기법을 사용할 수있는 유연성을 추가함으로써 Matrix 클래스를 매우 비효율적으로 만들었습니다.

예를 들어 연산자 오버로딩이 없으면 단일 패스를 사용하여 훨씬 더 효율적인 행렬 합계를 구현할 수 있습니다.

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

그러나 앞의 예제는 Matrix 클래스에 대해 훨씬 복잡한 Matrix::add2() 인터페이스를 만들기 때문에 ( Matrix::add2() , Matrix::AddMultiply() 등과 같은 메서드를 고려해야하기 때문에 자체 단점이 있습니다).

대신 우리가 한 발 물러서서보다 효율적인 방법으로 수행하기 위해 연산자 오버로딩을 조정할 수있는 방법을 살펴 보겠습니다.

문제는 전체 식 트리를 만들 기회가 있기 전에 Matrix d = a + b + c 라는식이 너무 "간절히"평가된다는 사실에서 기인합니다. 즉, 실제로 달성하고자 a + b + c 것은 하나의 패스에서 a + b + c 를 평가 a + b + c 실제로 표현 된 결과를 d 할당해야하는 경우입니다.

이것은 표현 템플릿 뒤에있는 핵심 아이디어입니다. operator+() 두 개의 Matrix 인스턴스를 추가 한 결과를 즉시 평가하는 대신, 전체 표현 트리를 작성한 후에 향후 평가를 위해 "표현 템플릿" 을 반환합니다.

예를 들어, 다음은 2 가지 유형의 합계에 해당하는 표현식 템플리트의 가능한 구현입니다.

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

그리고 operator+() 의 업데이트 된 버전이 있습니다.

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

보시다시피 operator+() 는 더 이상 2 개의 Matrix 인스턴스 (다른 Matrix 인스턴스)를 추가 한 결과에 대한 "열정적 인 평가"가 아니라 더하기 연산을 나타내는 표현 템플릿을 반환합니다. 명심해야 할 가장 중요한 점은 표현이 아직 평가되지 않았다는 것입니다. 피연산자에 대한 참조 만 보유합니다.

사실 MatrixSum<> 표현식 템플릿을 다음과 같이 인스턴스화하는 것을 MatrixSum<> 하는 MatrixSum<>MatrixSum<> .

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

그러나 나중에 합계 결과가 필요할 때 다음과 같이 식 d = a + 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);
    }
}

보시다시피, 표현 템플릿을 사용하는 또 다른 이점은 기본적으로 ab 의 합을 평가하고이를 한 번에 d 에 할당하는 것입니다.

또한 여러 표현 템플릿을 결합하는 것을 방해하는 요소는 없습니다. 예를 들어, a + b + c 는 다음 표현식 템플리트를 생성합니다.

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

여기에서 다시 한 번 패스를 사용하여 최종 결과를 평가할 수 있습니다.

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

마지막으로, 퍼즐의 마지막 부분은 실제로 표현 템플릿을 Matrix 클래스에 연결하는 것입니다. 이는 Matrix::operator=() 대한 구현을 제공함으로써 달성됩니다.이 구현은 표현 템플릿을 인수로 사용하고 이전에 수동으로 수행 한 것처럼 한 번에 계산하여 평가합니다.

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
아래 라이선스 CC BY-SA 3.0
와 제휴하지 않음 Stack Overflow