수색…
요소 와이즈 대수 표현식에 대한 기본 표현식 템플릿
소개 및 동기 부여
표현 템플릿 ( 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
.
-
vec_1
,vec_2
및vec_3
해당하는 세 개의 인스턴스. - 임시
Vector
인스턴스_tmp
의 결과를 나타내는,_tmp = vec_2*vec_3;
. - 마지막으로 반환 값 최적화 를 적절히 사용
result = vec_1 + _tmp;
의 최종result
가result = 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_3
및 result
총 4 가지 Vector
인스턴스가 result
됩니다. 즉, 요소 별 작업 만 관련된 이 예제에서는 중간 계산에서 임시 개체를 만들지 않습니다 .
ET는 어떻게 작동합니까?
기본적으로 말하자면, 대수적 계산을위한 ET 는 두 개의 빌딩 블록으로 구성됩니다.
- 순수 대수 표현 ( Pure Algebraic Express , PAE ) : 대수 표현의 프록시 / 추상입니다. 순수 대수학은 실제 계산을 수행하지 않으며, 단지 계산 워크 플로우의 추상화 / 모델링 일뿐입니다. PAE는 대수 식의 입력 또는 출력 모델 일 수 있습니다. PAE 의 인스턴스는 종종 저렴한 복사로 간주됩니다.
- 게으른 평가 : 실제 계산을 구현합니다. 다음 예에서는 요소 별 작업 만 포함하는 식에서 지연 테스트는 최종 결과에 대한 인덱싱 된 액세스 연산 내에서 실제 계산을 구현할 수 있으므로 주문형 평가 체계를 만듭니다. 즉 계산이 수행되지 않습니다 최종 결과에 액세스 / 요청한 경우에만
그래서, 구체적으로이 예제에서 ET 를 어떻게 구현합니까? 이제 살펴 보겠습니다.
항상 다음 코드 스 니펫을 고려하십시오.
Vector vec_1, vec_2, vec_3;
// Initializing vec_1, vec_2 and vec_3.
Vector result = vec_1 + vec_2*vec_3;
결과를 계산하는 표현식은 두 개의 하위 표현식으로 더 분해 될 수 있습니다.
- 벡터 더하기 표현식 ( plus_expr으로 표시)
- 벡터 내부 제품 식 ( 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은 모든 표현식에 대한 기본 클래스를 구현합니다. Curiously Recurring Template Pattern ( CRTP )을 사용합니다.
- 섹션 2는 첫 번째 PAE를 구현합니다. 터미널 은 계산을위한 실제 입력 값을 포함하는 입력 데이터 구조의 래퍼 (const 참조)입니다.
- 3 절에서는 두 번째 PAE 인 binary_operation을 구현합니다.이 작업은 나중에 vector_plus 및 vector_innerprod에 사용되는 클래스 템플릿입니다. 작동 유형 , 왼쪽 측면 PAE 및 오른쪽 측면 PAE로 매개 변수화됩니다. 실제 계산은 인덱싱 된 액세스 연산자에서 인코딩됩니다.
- 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를 사용하여 실행할 수있게 해주는 강력한 라이브러리입니다.
- Eigen 은 ET를 사용하여 다양한 대수 연산을 효율적으로 구현하는 선형 대수학 라이브러리입니다.
표현 템플릿을 보여주는 기본 예제
표현 템플릿은 주로 과학 계산에 사용되는 컴파일 타임 최적화 기법입니다. 주 목적은 불필요한 임시 변수를 피하고 단일 패스를 사용하여 루프 계산을 최적화하는 것입니다 (일반적으로 수치 집계에 대한 연산을 수행 할 때). 표현식 템플릿은 초기에 숫자 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);
}
}
보시다시피, 표현 템플릿을 사용하는 또 다른 이점은 기본적으로 a
와 b
의 합을 평가하고이를 한 번에 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;
};