ventajas tipos programación programacion perezosa funcional evaluacion desventajas datos con c++ c++11 templates

tipos - Cómo combinar la evaluación perezosa con auto en C++



programación funcional (4)

Creo que su problema básico es que la evaluación perezosa no se combina bien con el cambio de estado. Veo dos posibles rutas para salir de esto:

  1. Haz tus matrices inmutables. Si "modifica" una matriz, en realidad crea una copia con el cambio incorporado, el original permanece intacto. Esto funciona bien semánticamente (cualquier cálculo matemático funciona exactamente como espera que lo haga), sin embargo, puede incurrir en una sobrecarga de tiempo de ejecución intolerable si está configurando el valor de sus matrices por valor.

    Esto permite que su implementación de matrix_add se reemplace silenciosamente con un objeto de matrix cuando se evalúa, asegurando que cada evaluación se realice solo una vez.

  2. Haz tus funciones explícitas. No cree objetos matrix_add que actúen como si fueran matrices, sino que cree objetos matrix_function que operen en algunas matrices de entrada para obtener algún resultado. Esto le permite realizar explícitamente la evaluación donde lo considere oportuno y reutilizar las funciones que defina. Sin embargo, este enfoque conducirá a una gran cantidad de complejidad de código adicional.

No creo que sea una buena idea tratar de solucionar este problema mediante la introducción de puntos implícitos de evaluación forzada: perderá una gran parte de lo que puede lograrse con una evaluación perezosa. ¿Por qué molestarse en primer lugar? Sólo mis dos centavos.

Algunos antecedentes sobre lo que trato de hacer: estoy tratando de implementar una biblioteca haciendo mecánica cuántica. Como la mecánica cuántica es básicamente un álgebra lineal, estoy usando la biblioteca de álgebra lineal de armadillo que se encuentra debajo. Armadillo usa la evaluación perezosa para hacer algunos trucos inteligentes con matrices, lo que proporciona una buena abstracción de lo que realmente está sucediendo y parece estar cerca del código matlab.

Quiero hacer algo similar, pero también quiero poder usar el auto , lo que no es posible con armadillo (o eigen).

He estado mirando un poco a mi alrededor, y esta respuesta contiene lo que creo que es la forma típica de implementar esto: https://stackoverflow.com/a/414260/6306265

El problema con este enfoque es que cuando escribes

auto C = A+B;

obtienes una C que es una matrix_add , no una matrix . Incluso si matrix_add comporta de manera similar a la matrix , el hecho de que matrix_add contenga referencias a A y B hace que sea difícil de llevar. P.ej

auto A = matrix(2,2,{0,1,0,1}); auto B = matrix(2,2,{1,0,1,0}); auto C = A+B; C.printmatrix(); // 1,1 ; 1,1

pero

auto A = matrix(2,2,{0,1,0,1}); auto B = matrix(2,2,{1,0,1,0}); auto C = A+B; A(0,0) = 1; C.printmatrix(); // 2,1 ; 1,1

Lo cual es contraintuitivo. Como lo que quiero lograr es un comportamiento matemáticamente intuitivo, eso es un problema.

Peor aún es cuando lo hago.

auto sumMatrices(const matrix& A, const matrix& B) { return A+B; }

que devuelve un matrix_add con referencias a la memoria local.

Realmente me gustaría poder tener un comportamiento agradable y sobrecargado, pero también poder usar el auto . Mi idea fue hacer una envoltura que pueda contener una referencia o una instancia:

template<class T> class maybe_reference { public: maybe_reference(const T& t): ptr_(std::make_unique<T>(t)), t_(*ptr_) {} maybe_reference(std::reference_wrapper<const T> t): t_(t.get()) {} const T& get(){return t_;} private: unique_ptr<T> ptr_; const T& t_; }

Puede que no se implemente exactamente de esta manera, pero la idea general es tener dos constructores que puedan distinguirse claramente para garantizar que get() devuelva el objeto al que se hace referencia o el que está en el unique_ptr .

matrix_add modificada:

class matrix_add { public: friend matrix_add operator+(const matrix& A, const matrix& B); matrix_add(matrix_add&& other): A_(other.A_.get()), B_(other.B_.get()){} private: matrix_add(const matrix& A, const matrix& B): A_(std::ref(A)), B_(std::ref(B)){} maybe_reference<matrix> A_; maybe_reference<matrix> B_; };

He matrix_add todas las partes que hacen que matrix_add comporte como una matrix . La idea es hacer que el objeto se refiera a los objetos externos A&B siempre que se haya construido con A + B, pero cuando se construya con movimiento, tendrá copias.

Mi pregunta es básicamente: ¿esto funciona?

He estado pensando que el constructor de movimientos puede ser desviado en algunos o en todos los casos, lo que podría ser devastador.

Además, ¿hay una alternativa para lograr lo mismo? He estado buscando, pero parece que para el álgebra lineal al menos es perezoso o automático.

EDITAR : Gracias a que me recuerdan el término "plantillas de expresión", mi búsqueda en Google fue mucho más fructífera. Encontré este reddit-post: https://www.reddit.com/r/cpp/comments/4puabu/news_about_operator_auto/
y los documentos a los que se hace referencia, que permiten la especificación de "modelos" para auto. Esa sería la característica que realmente haría que todo este trabajo funcione.


Podría escribir una función de plantilla que, de forma predeterminada, sea un NOP, y luego sobrecargar según sea necesario.

#include <utility> #include <type_traits> struct matrix {}; struct matrix_add { matrix operator()() const; }; matrix_add operator + (matrix const& a, matrix const& b); template<class T> decltype(auto) evaluate(T&& val) { return std::forward<T>(val); } matrix evaluate(matrix_add const& lazy) { return lazy(); } matrix evaluate(matrix_add & lazy) { return lazy(); } matrix evaluate(matrix_add && lazy) { return lazy(); } int main() { auto a = matrix(); auto b = matrix(); auto c = evaluate(a + b); auto d = evaluate(1 + 2); static_assert(std::is_same<decltype(c), matrix>::value, ""); static_assert(std::is_same<decltype(d), int>::value, ""); }


con la deducción de argumentos de la plantilla de clase c ++ 17, puede escribir

struct matrix_expr_foo {}; struct matrix_expr_bar {}; template< typename L, typename R > struct matrix_add { // ... }; matrix_add<matrix_expr_foo,matrix_expr_bar> operator + (matrix_expr_foo const& a, matrix_expr_bar const& b); template< typename T > struct expr { expr( T const& expr ){ // evaluate expr ( to be stored in an appropriate member ) } // ... }; int main() { auto a = matrix_expr_foo(); auto b = matrix_expr_bar(); expr c = a + b; /* different naming ? auto_ c = a + b; ... */ }

donde expr está destinado a actuar como un auto para las plantillas de expresión ...


eager_eval un nuevo operador: eager_eval , así:

namespace lazy { template<class T> void eager_eval(T const volatile&)=delete; template<class T> struct expression { template<class D, std::enable_if_t<std::is_base_of<expression, std::decay_t<D>>{}, int> =0 > friend T eager_eval( D&& d ) { return std::forward<D>(d); } }; }

Cuando quiera que algo sea evaluable de una manera ansiosa, defina eager_eval en su espacio de nombres, o derive de lazy::lazy_expression<target_type> .

Así que modificamos su matrix_add para que (A) se derive de ella con el tipo de producción perezosa que desea, y (B) tiene una matrix operador:

struct matrix_add: lazy::expression<matrix> { matrix_add(matrix const& a, matrix const& b) : a(a), b(b) { } operator matrix() && { // rvalue ref qualified as it should be. matrix result; // Do the addition. return result; } private: matrix const& a, b; };

Y ahora, cualquiera puede hacer:

auto e = eager_eval( a+b );

y ADL encuentra el tipo correcto para evaluar ansiosamente la expresión perezosa.

ejemplo vivo .

De manera opcional, podría implementar un eager_eval predeterminado que devuelva su argumento:

template<class T, class...Ts> T eager_eval(T&& t, Ts&&...) { return std::forward<T>(t); }

entonces

using lazy::eager_eval; auto x = eager_eval( 1+2 );

le permite ser agnóstico con respecto al tipo que pasa a eager_eval ; si es un tipo que es consciente de ser perezoso a través de una sobrecarga eager_eval , convierte, y si no, no convierte.

El paquete en lazy::eager_eval anterior es para garantizar que tenga la prioridad más baja como una sobrecarga.