versiones compiler c++ c++11 expression-templates

compiler - c++17



Plantillas de expresiones y C++ 11 (2)

Me preguntaba si las lambdas junto con la semántica de movimiento o cualquier otra característica nueva pueden funcionar tan bien como los extraterrestres. ¿Alguna idea?

Respuesta rápida

La semántica de movimiento no es una panacea total en sí misma. ¡Las técnicas como las plantillas de expresión (ET) aún se necesitan en C ++ 11 para eliminar los gastos generales, como mover datos! Por lo tanto, para responder a su pregunta rápidamente antes de sumergirme en el resto de mi respuesta, mover la semántica, etc. no reemplaza por completo a los ET, como lo ilustra mi respuesta a continuación.

Respuesta detallada

Los ET normalmente devuelven objetos proxy para diferir la evaluación hasta más tarde, por lo que no hay un beneficio aparente inmediato de las características del lenguaje C ++ 11 hasta que el código que desencadena el cálculo. Dicho esto, uno no querría escribir el código ET, sin embargo, que desencadena la generación de código de tiempo de ejecución durante la construcción del árbol de expresión con los proxies. Muy bien, la semántica de movimiento de C ++ 11 y el reenvío perfecto pueden ayudar a evitar esos gastos generales en caso de que ocurra. (Tal no hubiera sido posible en C ++ 03).

Básicamente, cuando se escriben ET, uno quiere explotar las características del lenguaje de una manera para generar código óptimo una vez que se invocan las funciones miembro de los objetos proxy involucrados. En C ++ 11 esto incluirá el uso de reenvío perfecto, semántica de movimiento sobre copia, etc., si es que aún se necesita más allá de lo que el compilador ya puede hacer. El nombre del juego es minimizar el código de tiempo de ejecución generado y / o maximizar la velocidad de tiempo de ejecución y / o minimizar la sobrecarga en tiempo de ejecución.

Quería probar algunos ET con características C ++ 11 para ver si podía eludir TODOS los tipos intermedios de instancias temporales con a = b + c + d; expresión. (Como esto fue solo un descanso divertido de mis actividades normales, así que no lo comparé ni escribí el código ET simplemente usando C ++ 03. Tampoco me preocupé por todos los aspectos del pulido de código que aparecen a continuación).

Para empezar, no usé lambdas, ya que preferí usar tipos y funciones explícitos, así que no discutiré en contra de lambdas con respecto a su pregunta. Supongo que serían similares a usar funtores y tener un rendimiento no superior al código no ET a continuación (es decir, se necesitarían movimientos), al menos hasta que los compiladores puedan optimizar automáticamente las lambdas utilizando sus propios extraterrestres internos para tal fin. El código que escribí, sin embargo, explota la semántica de movimiento y el reenvío perfecto. Esto es lo que hice comenzando con los resultados y finalmente presentando el código.

math_vector<N> una math_vector<N> donde N==3 y define una instancia privada interna de std::array<long double, N> . Los miembros son un constructor predeterminado, copian y mueven constructores y asignaciones, un constructor de listas de inicializadores, un destructor, un miembro swap (), operator [] para acceder a elementos del vector y operator + =. Usado sin ninguna plantilla de expresión, este código:

{ cout << "CASE 1:/n"; math_vector<3> a{1.0, 1.1, 1.2}; math_vector<3> b{2.0, 2.1, 2.2}; math_vector<3> c{3.0, 3.1, 3.2}; math_vector<3> d{4.0, 4.1, 4.2}; math_vector<3> result = a + b + c + d; cout << ''['' << &result << "]: " << result << "/n"; }

salidas (cuando se compila con clang++ 3.1 o g++ 4.8 con - std=c++11 -O3 ):

CASE 1: 0x7fff8d6edf50: math_vector(initlist) 0x7fff8d6edef0: math_vector(initlist) 0x7fff8d6ede90: math_vector(initlist) 0x7fff8d6ede30: math_vector(initlist) 0x7fff8d6edd70: math_vector(copy: 0x7fff8d6edf50) 0x7fff8d6edda0: math_vector(move: 0x7fff8d6edd70) 0x7fff8d6eddd0: math_vector(move: 0x7fff8d6edda0) 0x7fff8d6edda0: ~math_vector() 0x7fff8d6edd70: ~math_vector() [0x7fff8d6eddd0]: (10,10.4,10.8) 0x7fff8d6eddd0: ~math_vector() 0x7fff8d6ede30: ~math_vector() 0x7fff8d6ede90: ~math_vector() 0x7fff8d6edef0: ~math_vector() 0x7fff8d6edf50: ~math_vector()

es decir, las cuatro instancias explícitamente construidas que usan listas de inicializadores (es decir, los elementos initlist ), la variable result (es decir, 0x7fff8d6eddd0 ), y también hace que tres objetos adicionales se copien y se muevan.

Para centrarme únicamente en los temporales y moverlos, creé un segundo caso que solo crea el result como una variable nombrada, todos los demás son valores r:

{ cout << "CASE 2:/n"; math_vector<3> result = math_vector<3>{1.0, 1.1, 1.2} + math_vector<3>{2.0, 2.1, 2.2} + math_vector<3>{3.0, 3.1, 3.2} + math_vector<3>{4.0, 4.1, 4.2} ; cout << ''['' << &result << "]: " << result << "/n"; }

que produce esto (nuevamente cuando no se usan ETs):

CASE 2: 0x7fff8d6edcb0: math_vector(initlist) 0x7fff8d6edc50: math_vector(initlist) 0x7fff8d6edce0: math_vector(move: 0x7fff8d6edcb0) 0x7fff8d6edbf0: math_vector(initlist) 0x7fff8d6edd10: math_vector(move: 0x7fff8d6edce0) 0x7fff8d6edb90: math_vector(initlist) 0x7fff8d6edd40: math_vector(move: 0x7fff8d6edd10) 0x7fff8d6edb90: ~math_vector() 0x7fff8d6edd10: ~math_vector() 0x7fff8d6edbf0: ~math_vector() 0x7fff8d6edce0: ~math_vector() 0x7fff8d6edc50: ~math_vector() 0x7fff8d6edcb0: ~math_vector() [0x7fff8d6edd40]: (10,10.4,10.8) 0x7fff8d6edd40: ~math_vector()

lo cual es mejor: solo se crean objetos de movimiento extra.

Pero quería algo mejor: quería cero temporales adicionales y tener el código como si lo codificara con una advertencia de codificación normal: todos los tipos de instancia explícita se crearían (es decir, los cuatro constructores de la initlist instalación y el result ). Para lograr esto, agregué el código de la plantilla de expresión de la siguiente manera:

  1. una math_vector_expr<LeftExpr,BinaryOp,RightExpr> proxy math_vector_expr<LeftExpr,BinaryOp,RightExpr> fue creada para contener una expresión aún no computada,
  2. se creó una clase proxy plus_op para contener la operación de adición,
  3. se agregó un constructor a math_vector para aceptar un objeto math_vector_expr y,
  4. Se agregaron funciones de miembro "iniciador" para activar la creación de la plantilla de expresión.

Los resultados con ETs son maravillosos: ¡sin temporales adicionales en ninguno de los casos! Los dos casos anteriores arriba ahora salen:

CASE 1: 0x7fffe7180c60: math_vector(initlist) 0x7fffe7180c90: math_vector(initlist) 0x7fffe7180cc0: math_vector(initlist) 0x7fffe7180cf0: math_vector(initlist) 0x7fffe7180d20: math_vector(expr: 0x7fffe7180d90) [0x7fffe7180d20]: (10,10.4,10.8) 0x7fffe7180d20: ~math_vector() 0x7fffe7180cf0: ~math_vector() 0x7fffe7180cc0: ~math_vector() 0x7fffe7180c90: ~math_vector() 0x7fffe7180c60: ~math_vector() CASE 2: 0x7fffe7180dd0: math_vector(initlist) 0x7fffe7180e20: math_vector(initlist) 0x7fffe7180e70: math_vector(initlist) 0x7fffe7180eb0: math_vector(initlist) 0x7fffe7180d20: math_vector(expr: 0x7fffe7180dc0) 0x7fffe7180eb0: ~math_vector() 0x7fffe7180e70: ~math_vector() 0x7fffe7180e20: ~math_vector() 0x7fffe7180dd0: ~math_vector() [0x7fffe7180d20]: (10,10.4,10.8) 0x7fffe7180d20: ~math_vector()

es decir, exactamente 5 llamadas de constructor y 5 llamadas de destructor en cada caso. De hecho, si le pide al compilador que genere el código del ensamblador entre las 4 initlist constructor de la initlist de initlist y la salida del result obtendrá esta hermosa cadena de código ensamblador:

fldt 128(%rsp) leaq 128(%rsp), %rdi leaq 80(%rsp), %rbp fldt 176(%rsp) faddp %st, %st(1) fldt 224(%rsp) faddp %st, %st(1) fldt 272(%rsp) faddp %st, %st(1) fstpt 80(%rsp) fldt 144(%rsp) fldt 192(%rsp) faddp %st, %st(1) fldt 240(%rsp) faddp %st, %st(1) fldt 288(%rsp) faddp %st, %st(1) fstpt 96(%rsp) fldt 160(%rsp) fldt 208(%rsp) faddp %st, %st(1) fldt 256(%rsp) faddp %st, %st(1) fldt 304(%rsp) faddp %st, %st(1) fstpt 112(%rsp)

con g++ y clang++ genera un código similar (incluso más pequeño). No hay llamadas de función, etc., ¡simplemente un montón de adiciones que es EXACTAMENTE lo que uno quiere!

El código C ++ 11 para lograr esto sigue. Simplemente #define DONT_USE_EXPR_TEMPL para que no use ETs o no lo defina para usar ETs.

#include <array> #include <algorithm> #include <initializer_list> #include <type_traits> #include <iostream> //#define DONT_USE_EXPR_TEMPL //=========================================================================== template <std::size_t N> class math_vector; template < typename LeftExpr, typename BinaryOp, typename RightExpr > class math_vector_expr { public: math_vector_expr() = delete; math_vector_expr(LeftExpr l, RightExpr r) : l_(std::forward<LeftExpr>(l)), r_(std::forward<RightExpr>(r)) { } // Prohibit copying... math_vector_expr(math_vector_expr const&) = delete; math_vector_expr& operator =(math_vector_expr const&) = delete; // Allow moves... math_vector_expr(math_vector_expr&&) = default; math_vector_expr& operator =(math_vector_expr&&) = default; template <typename RE> auto operator +(RE&& re) const -> math_vector_expr< math_vector_expr<LeftExpr,BinaryOp,RightExpr> const&, BinaryOp, decltype(std::forward<RE>(re)) > { return math_vector_expr< math_vector_expr<LeftExpr,BinaryOp,RightExpr> const&, BinaryOp, decltype(std::forward<RE>(re)) >(*this, std::forward<RE>(re)) ; } auto le() -> typename std::add_lvalue_reference<LeftExpr>::type { return l_; } auto le() const -> typename std::add_lvalue_reference< typename std::add_const<LeftExpr>::type >::type { return l_; } auto re() -> typename std::add_lvalue_reference<RightExpr>::type { return r_; } auto re() const -> typename std::add_lvalue_reference< typename std::add_const<RightExpr>::type >::type { return r_; } auto operator [](std::size_t index) const -> decltype( BinaryOp::apply(this->le()[index], this->re()[index]) ) { return BinaryOp::apply(le()[index], re()[index]); } private: LeftExpr l_; RightExpr r_; }; //=========================================================================== template <typename T> struct plus_op { static T apply(T const& a, T const& b) { return a + b; } static T apply(T&& a, T const& b) { a += b; return std::move(a); } static T apply(T const& a, T&& b) { b += a; return std::move(b); } static T apply(T&& a, T&& b) { a += b; return std::move(a); } }; //=========================================================================== template <std::size_t N> class math_vector { using impl_type = std::array<long double, N>; public: math_vector() { using namespace std; fill(begin(v_), end(v_), impl_type{}); std::cout << this << ": math_vector()" << endl; } math_vector(math_vector const& mv) noexcept { using namespace std; copy(begin(mv.v_), end(mv.v_), begin(v_)); std::cout << this << ": math_vector(copy: " << &mv << ")" << endl; } math_vector(math_vector&& mv) noexcept { using namespace std; move(begin(mv.v_), end(mv.v_), begin(v_)); std::cout << this << ": math_vector(move: " << &mv << ")" << endl; } math_vector(std::initializer_list<typename impl_type::value_type> l) { using namespace std; copy(begin(l), end(l), begin(v_)); std::cout << this << ": math_vector(initlist)" << endl; } math_vector& operator =(math_vector const& mv) noexcept { using namespace std; copy(begin(mv.v_), end(mv.v_), begin(v_)); std::cout << this << ": math_vector op =(copy: " << &mv << ")" << endl; return *this; } math_vector& operator =(math_vector&& mv) noexcept { using namespace std; move(begin(mv.v_), end(mv.v_), begin(v_)); std::cout << this << ": math_vector op =(move: " << &mv << ")" << endl; return *this; } ~math_vector() { using namespace std; std::cout << this << ": ~math_vector()" << endl; } void swap(math_vector& mv) { using namespace std; for (std::size_t i = 0; i<N; ++i) swap(v_[i], mv[i]); } auto operator [](std::size_t index) const -> typename impl_type::value_type const& { return v_[index]; } auto operator [](std::size_t index) -> typename impl_type::value_type& { return v_[index]; } math_vector& operator +=(math_vector const& b) { for (std::size_t i = 0; i<N; ++i) v_[i] += b[i]; return *this; } #ifndef DONT_USE_EXPR_TEMPL template <typename LE, typename Op, typename RE> math_vector(math_vector_expr<LE,Op,RE>&& mve) { for (std::size_t i = 0; i < N; ++i) v_[i] = mve[i]; std::cout << this << ": math_vector(expr: " << &mve << ")" << std::endl; } template <typename RightExpr> math_vector& operator =(RightExpr&& re) { for (std::size_t i = 0; i<N; ++i) v_[i] = re[i]; return *this; } template <typename RightExpr> math_vector& operator +=(RightExpr&& re) { for (std::size_t i = 0; i<N; ++i) v_[i] += re[i]; return *this; } template <typename RightExpr> auto operator +(RightExpr&& re) const -> math_vector_expr< math_vector const&, plus_op<typename impl_type::value_type>, decltype(std::forward<RightExpr>(re)) > { return math_vector_expr< math_vector const&, plus_op<typename impl_type::value_type>, decltype(std::forward<RightExpr>(re)) >( *this, std::forward<RightExpr>(re) ) ; } #endif // #ifndef DONT_USE_EXPR_TEMPL private: impl_type v_; }; //=========================================================================== template <std::size_t N> inline void swap(math_vector<N>& a, math_vector<N>& b) { a.swap(b); } //=========================================================================== #ifdef DONT_USE_EXPR_TEMPL template <std::size_t N> inline math_vector<N> operator +( math_vector<N> const& a, math_vector<N> const& b ) { math_vector<N> retval(a); retval += b; return retval; } template <std::size_t N> inline math_vector<N> operator +( math_vector<N>&& a, math_vector<N> const& b ) { a += b; return std::move(a); } template <std::size_t N> inline math_vector<N> operator +( math_vector<N> const& a, math_vector<N>&& b ) { b += a; return std::move(b); } template <std::size_t N> inline math_vector<N> operator +( math_vector<N>&& a, math_vector<N>&& b ) { a += std::move(b); return std::move(a); } #endif // #ifdef DONT_USE_EXPR_TEMPL //=========================================================================== template <std::size_t N> std::ostream& operator <<(std::ostream& os, math_vector<N> const& mv) { os << ''(''; for (std::size_t i = 0; i < N; ++i) os << mv[i] << ((i+1 != N) ? '','' : '')''); return os; } //=========================================================================== int main() { using namespace std; try { { cout << "CASE 1:/n"; math_vector<3> a{1.0, 1.1, 1.2}; math_vector<3> b{2.0, 2.1, 2.2}; math_vector<3> c{3.0, 3.1, 3.2}; math_vector<3> d{4.0, 4.1, 4.2}; math_vector<3> result = a + b + c + d; cout << ''['' << &result << "]: " << result << "/n"; } cout << endl; { cout << "CASE 2:/n"; math_vector<3> result = math_vector<3>{1.0, 1.1, 1.2} + math_vector<3>{2.0, 2.1, 2.2} + math_vector<3>{3.0, 3.1, 3.2} + math_vector<3>{4.0, 4.1, 4.2} ; cout << ''['' << &result << "]: " << result << "/n"; } } catch (...) { return 1; } } //===========================================================================

Veamos un beneficio particular de las plantillas de expresión: los ET pueden usarse para evitar temporales temporales de tamaño de vector en la memoria que ocurren en operadores sobrecargados como:

template<typename T> std::vector<T> operator+(const std::vector<T>& a, const std::vector<T>& b) { std::vector<T> tmp; // vector-sized temporary for_each(...); return tmp; }

En C ++ 11, la declaración de retorno de esta función aplica semántica de movimiento. Sin copia del vector. Esa es una victoria.

Sin embargo, si miro una expresión simple como

d = a + b + c;

Veo que la función anterior se llama dos veces (para el operator+ ) mientras que la asignación final se puede hacer con la semántica de movimiento.

En total se ejecutan 2 bucles. Significa que apagué un temporal y lo leí inmediatamente después. Para vectores grandes, esto carece de caché. Eso es peor que las plantillas de expresión. Pueden hacer todo en solo 1 ciclo. Los ET pueden ejecutar el código anterior equivalente a:

for(int i=0 ; i < vec_length ; ++i) d[i] = a[i] + b[i] + c[i];

Me preguntaba si las lambdas junto con la semántica de movimiento o cualquier otra característica nueva pueden funcionar tan bien como los extraterrestres. ¿Alguna idea?

Editar:

Básicamente, utilizando la técnica ET, el compilador construye un árbol de análisis que se asemeja a la expresión algebraica con su sistema de tipos. Este árbol se compone de nodos internos y nodos de hojas. Los nodos internos representan operaciones (suma, multiplicación, etc.) y los nodos hoja representan referencias a los objetos de datos.

Traté de pensar en todo el proceso de cómputo a la manera de una máquina de pila: tomar una operación de una pila de operaciones y extraer los siguientes argumentos de la pila de argumentos y evaluar la operación. Pon el resultado nuevamente en la pila esperando la operación.

Para representar estos dos objetos diferentes (pila de operaciones y pila de hojas de datos) integé un std::tuple para las operaciones y un std::tuple para los datos parte a un std::pair<> . Inicialmente utilicé un std:vector pero eso dio como resultado una sobrecarga de tiempo de ejecución.

Todo el proceso se divide en dos fases: inicialización de la máquina apilada donde se inicializan la pila de operaciones y argumentos. Y la fase de evaluación que se desencadena al asignar los contenedores vinculados al vector.

Vec una clase Vec que contiene una array<int,5> privada array<int,5> (la carga útil) y que presenta un operador de asignación sobrecargado que toma la "expresión".

El operator* global operator* está sobrecargado para todas las combinaciones de tomar Vec y "expresión" para permitir el manejo correcto también en el caso en que tengamos más que a a*b . (Aviso, cambié para este ejemplo educativo a la multiplicación, básicamente para detectar rápidamente el imull en el ensamblador).

Lo que se hace primero antes de que comience la evaluación es "extraer" los valores de los objetos Vec involucrados e inicializar la pila de argumentos. Eso era necesario para no tener diferentes tipos de objetos: vectores indexables y resultados no indexables. Esto es para lo que es el Extractor . Algo bueno otra vez: se usan plantillas variables, que en este caso no generan sobrecarga en tiempo de ejecución (todo esto se hace en tiempo de compilación).

Todo funciona. La expresión está muy bien evaluada (también agregué la adición, pero se deja aquí para que se ajuste al código). A continuación puede ver la salida del ensamblador. Solo compuación bruta, exactamente como usted quiere que sea: En par con la técnica ET.

Resultado. Las nuevas funciones de lenguaje de C ++ 11 ofrecen las plantillas variadic que (junto con la meta-programación de plantillas) abren el área de computación en tiempo de compilación. Mostré aquí cómo se pueden usar los beneficios de las plantillas variadic para producir código tan bueno como con la técnica ET tradicional.

#include<algorithm> #include<iostream> #include<vector> #include<tuple> #include<utility> #include<array> template<typename Target,typename Tuple, int N, bool end> struct Extractor { template < typename ... Args > static Target index(int i,const Tuple& t, Args && ... args) { return Extractor<Target, Tuple, N+1, std::tuple_size<Tuple>::value == N+1>:: index(i, t , std::forward<Args>(args)..., std::get<N>(t).vec[i] ); } }; template < typename Target, typename Tuple, int N > struct Extractor<Target,Tuple,N,true> { template < typename ... Args > static Target index(int i,Tuple const& t, Args && ... args) { return Target(std::forward<Args>(args)...); } }; template < typename ... Vs > std::tuple<typename std::remove_reference<Vs>::type::type_t...> extract(int i , const std::tuple<Vs...>& tpl) { return Extractor<std::tuple<typename std::remove_reference<Vs>::type::type_t...>, std::tuple<Vs...>, 0, std::tuple_size<std::tuple<Vs...> >::value == 0>::index(i,tpl); } struct Vec { std::array<int,5> vec; typedef int type_t; template<typename... OPs,typename... VALs> Vec& operator=(const std::pair< std::tuple<VALs...> , std::tuple<OPs...> >& e) { for( int i = 0 ; i < vec.size() ; ++i ) { vec[i] = eval( extract(i,e.first) , e.second ); } } }; template<int OpPos,int ValPos, bool end> struct StackMachine { template<typename... OPs,typename... VALs> static void eval_pos( std::tuple<VALs...>& vals , const std::tuple<OPs...> & ops ) { std::get<ValPos+1>( vals ) = std::get<OpPos>(ops).apply( std::get<ValPos>( vals ) , std::get<ValPos+1>( vals ) ); StackMachine<OpPos+1,ValPos+1,sizeof...(OPs) == OpPos+1>::eval_pos(vals,ops); } }; template<int OpPos,int ValPos> struct StackMachine<OpPos,ValPos,true> { template<typename... OPs,typename... VALs> static void eval_pos( std::tuple<VALs...>& vals , const std::tuple<OPs...> & ops ) {} }; template<typename... OPs,typename... VALs> int eval( const std::tuple<VALs...>& vals , const std::tuple<OPs...> & ops ) { StackMachine<0,0,false>::eval_pos(const_cast<std::tuple<VALs...>&>(vals),ops); return std::get<sizeof...(OPs)>(vals); } struct OpMul { static int apply(const int& lhs,const int& rhs) { return lhs*rhs; } }; std::pair< std::tuple< const Vec&, const Vec& > , std::tuple<OpMul> > operator*(const Vec& lhs,const Vec& rhs) { return std::make_pair( std::tuple< const Vec&, const Vec& >( lhs , rhs ) , std::tuple<OpMul>( OpMul() ) ); } template<typename... OPs,typename... VALs> std::pair< std::tuple< const Vec&, VALs... > , std::tuple<OPs...,OpMul> > operator*(const Vec& lhs,const std::pair< std::tuple< VALs... > , std::tuple<OPs...> >& rhs) { return std::make_pair( std::tuple_cat( rhs.first , std::tuple< const Vec& >(lhs) ) , std::tuple_cat( rhs.second , std::tuple<OpMul>( OpMul() ) ) ); } template<typename... OPs,typename... VALs> std::pair< std::tuple< const Vec&, VALs... > , std::tuple<OPs...,OpMul> > operator*(const std::pair< std::tuple< VALs... > , std::tuple<OPs...> >& lhs, const Vec& rhs) { return std::make_pair( std::tuple_cat( lhs.first , std::tuple< const Vec& >(rhs) ) , std::tuple_cat( lhs.second , std::tuple<OpMul>( OpMul() ) ) ); } int main() { Vec d,c,b,a; for( int i = 0 ; i < d.vec.size() ; ++i ) { a.vec[i] = 10+i; b.vec[i] = 20+i; c.vec[i] = 30+i; d.vec[i] = 0; } d = a * b * c * a; for( int i = 0 ; i < d.vec.size() ; ++i ) std::cout << d.vec[i] << std::endl; }

Ensamblador generado con g++-4.6 -O3 (Tuve que poner cierta dependencia de tiempo de ejecución en la inicialización del vector para que el compilador no calcule todo en tiempo de compilación y realmente vea las imull imulsas).

imull %esi, %edx imull 32(%rsp), %edx imull %edx, %esi movl 68(%rsp), %edx imull %ecx, %edx movl %esi, (%rsp) imull 36(%rsp), %edx imull %ecx, %edx movl 104(%rsp), %ecx movl %edx, 4(%rsp) movl 72(%rsp), %edx imull %ecx, %edx imull 40(%rsp), %edx imull %ecx, %edx movl 108(%rsp), %ecx movl %edx, 8(%rsp) movl 76(%rsp), %edx imull %ecx, %edx imull 44(%rsp), %edx imull %ecx, %edx movl 112(%rsp), %ecx movl %edx, 12(%rsp) movl 80(%rsp), %edx imull %ecx, %edx imull %eax, %edx imull %ecx, %edx movl %edx, 16(%rsp)


Aquí está la versión corregida del código de Paul Preney. He informado al autor por correo electrónico y en comentarios; He escrito una edición, pero ha sido rechazada por revisores no calificados.

El error en el código original es que el parámetro de plantilla BinaryOp de math_vector_expr :: operator + es fijo.

#include <array> #include <algorithm> #include <initializer_list> #include <type_traits> #include <iostream> //#define DONT_USE_EXPR_TEMPL //=========================================================================== template <std::size_t N> class math_vector; template <typename T> struct plus_op; template < typename LeftExpr, typename BinaryOp, typename RightExpr > class math_vector_expr { public: typedef typename std::remove_reference<LeftExpr>::type::value_type value_type; math_vector_expr() = delete; math_vector_expr(LeftExpr l, RightExpr r) : l_(std::forward<LeftExpr>(l)), r_(std::forward<RightExpr>(r)) { } // Prohibit copying... math_vector_expr(math_vector_expr const&) = delete; math_vector_expr& operator =(math_vector_expr const&) = delete; // Allow moves... math_vector_expr(math_vector_expr&&) = default; math_vector_expr& operator =(math_vector_expr&&) = default; template <typename RE> auto operator +(RE&& re) const -> math_vector_expr< math_vector_expr<LeftExpr,BinaryOp,RightExpr> const&, plus_op<value_type>, decltype(std::forward<RE>(re)) > { return math_vector_expr< math_vector_expr<LeftExpr,BinaryOp,RightExpr> const&, plus_op<value_type>, decltype(std::forward<RE>(re)) >(*this, std::forward<RE>(re)) ; } auto le() -> typename std::add_lvalue_reference<LeftExpr>::type { return l_; } auto le() const -> typename std::add_lvalue_reference< typename std::add_const<LeftExpr>::type >::type { return l_; } auto re() -> typename std::add_lvalue_reference<RightExpr>::type { return r_; } auto re() const -> typename std::add_lvalue_reference< typename std::add_const<RightExpr>::type >::type { return r_; } auto operator [](std::size_t index) const -> value_type { return BinaryOp::apply(le()[index], re()[index]); } private: LeftExpr l_; RightExpr r_; }; //=========================================================================== template <typename T> struct plus_op { static T apply(T const& a, T const& b) { return a + b; } static T apply(T&& a, T const& b) { a += b; return std::move(a); } static T apply(T const& a, T&& b) { b += a; return std::move(b); } static T apply(T&& a, T&& b) { a += b; return std::move(a); } }; //=========================================================================== template <std::size_t N> class math_vector { using impl_type = std::array<long double, N>; public: typedef typename impl_type::value_type value_type; math_vector() { using namespace std; fill(begin(v_), end(v_), impl_type{}); std::cout << this << ": math_vector()" << endl; } math_vector(math_vector const& mv) noexcept { using namespace std; copy(begin(mv.v_), end(mv.v_), begin(v_)); std::cout << this << ": math_vector(copy: " << &mv << ")" << endl; } math_vector(math_vector&& mv) noexcept { using namespace std; move(begin(mv.v_), end(mv.v_), begin(v_)); std::cout << this << ": math_vector(move: " << &mv << ")" << endl; } math_vector(std::initializer_list<value_type> l) { using namespace std; copy(begin(l), end(l), begin(v_)); std::cout << this << ": math_vector(initlist)" << endl; } math_vector& operator =(math_vector const& mv) noexcept { using namespace std; copy(begin(mv.v_), end(mv.v_), begin(v_)); std::cout << this << ": math_vector op =(copy: " << &mv << ")" << endl; return *this; } math_vector& operator =(math_vector&& mv) noexcept { using namespace std; move(begin(mv.v_), end(mv.v_), begin(v_)); std::cout << this << ": math_vector op =(move: " << &mv << ")" << endl; return *this; } ~math_vector() { using namespace std; std::cout << this << ": ~math_vector()" << endl; } void swap(math_vector& mv) { using namespace std; for (std::size_t i = 0; i<N; ++i) swap(v_[i], mv[i]); } auto operator [](std::size_t index) const -> value_type const& { return v_[index]; } auto operator [](std::size_t index) -> value_type& { return v_[index]; } math_vector& operator +=(math_vector const& b) { for (std::size_t i = 0; i<N; ++i) v_[i] += b[i]; return *this; } #ifndef DONT_USE_EXPR_TEMPL template <typename LE, typename Op, typename RE> math_vector(math_vector_expr<LE,Op,RE>&& mve) { for (std::size_t i = 0; i < N; ++i) v_[i] = mve[i]; std::cout << this << ": math_vector(expr: " << &mve << ")" << std::endl; } template <typename RightExpr> math_vector& operator =(RightExpr&& re) { for (std::size_t i = 0; i<N; ++i) v_[i] = re[i]; return *this; } template <typename RightExpr> math_vector& operator +=(RightExpr&& re) { for (std::size_t i = 0; i<N; ++i) v_[i] += re[i]; return *this; } template <typename RightExpr> auto operator +(RightExpr&& re) const -> math_vector_expr< math_vector const&, plus_op<value_type>, decltype(std::forward<RightExpr>(re)) > { return math_vector_expr< math_vector const&, plus_op<value_type>, decltype(std::forward<RightExpr>(re)) >( *this, std::forward<RightExpr>(re) ) ; } #endif // #ifndef DONT_USE_EXPR_TEMPL private: impl_type v_; }; //=========================================================================== template <std::size_t N> inline void swap(math_vector<N>& a, math_vector<N>& b) { a.swap(b); } //=========================================================================== #ifdef DONT_USE_EXPR_TEMPL template <std::size_t N> inline math_vector<N> operator +( math_vector<N> const& a, math_vector<N> const& b ) { math_vector<N> retval(a); retval += b; return retval; } template <std::size_t N> inline math_vector<N> operator +( math_vector<N>&& a, math_vector<N> const& b ) { a += b; return std::move(a); } template <std::size_t N> inline math_vector<N> operator +( math_vector<N> const& a, math_vector<N>&& b ) { b += a; return std::move(b); } template <std::size_t N> inline math_vector<N> operator +( math_vector<N>&& a, math_vector<N>&& b ) { a += std::move(b); return std::move(a); } #endif // #ifdef DONT_USE_EXPR_TEMPL //=========================================================================== template <std::size_t N> std::ostream& operator <<(std::ostream& os, math_vector<N> const& mv) { os << ''(''; for (std::size_t i = 0; i < N; ++i) os << mv[i] << ((i+1 != N) ? '','' : '')''); return os; } //=========================================================================== int main() { using namespace std; try { { cout << "CASE 1:/n"; math_vector<3> a{1.0, 1.1, 1.2}; math_vector<3> b{2.0, 2.1, 2.2}; math_vector<3> c{3.0, 3.1, 3.2}; math_vector<3> d{4.0, 4.1, 4.2}; math_vector<3> result = a + b + c + d; cout << ''['' << &result << "]: " << result << "/n"; } cout << endl; { cout << "CASE 2:/n"; math_vector<3> result = math_vector<3>{1.0, 1.1, 1.2} + math_vector<3>{2.0, 2.1, 2.2} + math_vector<3>{3.0, 3.1, 3.2} + math_vector<3>{4.0, 4.1, 4.2} ; cout << ''['' << &result << "]: " << result << "/n"; } } catch (...) { return 1; } } //===========================================================================