c++ c++11 c++14 shared-ptr

c++ - Conectando tres objetos diferentes



c++11 c++14 (5)

Hace un par de horas hice una pregunta similar sobre la conexión de dos elementos de un vector. Ahora, me gustaría hacer mi pregunta más general. Supongamos que tenemos dos objetos del tipo double: double d1, d2 . Queremos que el tercer objeto ( double d3 ) obtenga el valor de d1+d2 , de modo que si cambiamos d1 o d2 , d3 obtendrá automáticamente el nuevo valor de d1+d2 . ¿Cómo podemos hacerlo en C ++?

Esto es lo que quiero decir:

int main(){ double d1,d2,d3; d1=4; d2=7; //some operations to make d3=d1+d2 std::cout<<d3<<endl;// I want it to print 11 d2=-4; std::cout<<d3<<endl;//Now without any further operations between these line, it should print 0 return 0; }

Gracias.


En el momento de la compilación, No, en el mejor de los casos, terminarás con una plantilla desagradable y hacks de Macro que aún estarán muy limitados. Si su pensamiento está en tiempo de compilación, no lea el resto de la respuesta

¿Desde un nivel de usuario (en tiempo de ejecución)? Sí tu puedes. Aunque, la lógica es bastante simple, solo tienes que encontrar una manera pragmática de crear y mantener los invariantes de un árbol de expresiones. Requiere un poco de trabajo para actualizarlo.

Entonces, abordemos la lógica ... Para simplificar, definamos algunos términos básicos aquí para que se ajusten a nuestra intención.

  • Un operator es una función que requiere como máximo 2 operands modo que cuando se le llama, produce un resultado que es otro operand
  • Un operand es un objeto de interés, en su caso, los números, más precisamente el double . Puede ser producido por usted o por una expression
  • Una expression es un objeto que toma como máximo 2 operands y un operator , y produce un operand resultante llamando a la función de operator .

Hice algunos dibujos para ilustrar esto ...

Como puedes ver, las flechas muestran la dirección del conocimiento.

  • Un Operand conoce todas las expresiones en las que participa.
  • Una Expression conoce los Operands que produjo.

Por lo tanto, vamos a darles algunas identidades ...

Digamos que usted creó el Operand 1 , el Operand 2 , el Operand 4 . Y comenzaste a construir este árbol de expresiones en este orden:

  1. Creó una relación (una Expression ) entre el Operand 1 y el Operand 2 que está representado por Expression1 .

  2. Expression1 usa el Operator que fue construido para producir su resultado, Operand 3

  3. Combinó el Operand 3 resultante con su Operand 4 creado en una nueva expresión Expression2 para producir otro resultado, Operand 5

Ahora, veamos qué sucede cuando decidimos modificar el Operand 1 .

Como puede ver, el operando modificado recursivamente revisará y actualizará todas las subexpresiones cuyo resultado depende de ello (ya sea directamente o por proxy).

Ahora que tenemos una idea muy simple, ¿cómo lo hacemos? Hay varias formas de implementarlo, cuanto más genérico y flexible sea, menos rendimiento tendrá (en términos de memoria y velocidad)

Hice una implementación simple a continuación (Obviamente, lejos de ser algo óptimo).

template<typename T> class Operand; template<typename T> class Expression { std::shared_ptr<Operand<T>> m_operand1; std::shared_ptr<Operand<T>> m_operand2; std::shared_ptr<Operand<T>> m_result; T (*m_operator)(const T&, const T&); friend class Operand<T>; public: Expression( T(*operator_func)(const T&, const T&), std::shared_ptr<Operand<T>> operand_1, std::shared_ptr<Operand<T>> operand_2) : m_operand1(operand_1), m_operand2(operand_2), m_result(std::make_shared<Operand<T>>(T{})), m_operator(operator_func) { } void update(){ m_result->value() = m_operator(m_operand1->value(), m_operand2->value()); m_result->update(); } std::shared_ptr<Operand<T>>& result() { return m_result; } }; template<typename T> class Operand { T val; std::vector<std::shared_ptr<Expression<T>>> expressions; friend class Expression<T>; public: Operand(T value) : val(value) {} T& value() { return val; } void update(){ for(auto& x : expressions) x->update(); } static std::shared_ptr<Operand<T>> make(const T& t){ return std::make_shared<Operand<T>>(t); } static std::shared_ptr<Operand<T>> relate( T(*operator_func)(const T&, const T&), std::shared_ptr<Operand<T>> operand_1, std::shared_ptr<Operand<T>> operand_2 ){ auto e = std::make_shared<Expression<T>>(operator_func, operand_1, operand_2); operand_1->expressions.push_back( e ); operand_2->expressions.push_back( e ); e->update(); return e->result(); } }; //template<typename T> //double add(const double& lhs, const double& rhs){ return lhs + rhs; } template<typename T> T add(const T& lhs, const T& rhs){ return lhs + rhs; } template<typename T> T mul(const T& lhs, const T& rhs){ return lhs * rhs; } int main() { using DOperand = Operand<double>; auto d1 = DOperand::make(54.64); auto d2 = DOperand::make(55.36); auto d3 = DOperand::relate(add<double>, d1, d2); auto d4 = DOperand::relate(mul<double>, d3, d2); //---------------PRINT------------------------// std::cout << "d1 = " << d1->value() << "/nd2 = " << d2->value() << "/nd3 = d1 + d2 = " << d3->value() << "/nd4 = d3 * d2 = " << d4->value() << std::endl; //---------------UPDATE ONE VARIABLE------------------------// std::cout << "/n/n====================/n" << std::endl; std::cout << "changed d1 from " << d1->value() << " to "; d1->value() = -863.2436356; d1->update(); std::cout << d1->value() << "/n/n=======================/n/n"; //---------------PRINT------------------------// std::cout << "d1 = " << d1->value() << "/nd2 = " << d2->value() << "/nd3 = d1 + d2 = " << d3->value() << "/nd4 = d3 * d2 = " << d4->value() << std::endl; // ******************************************* std::cout << "/n/n/n/n/nSizeof(Operand<int>) = " << sizeof(Operand<int>) << "/nSizeof(Expression<int>) = " << sizeof(Expression<int>) << std::endl; }

La salida es:

d1 = 54.64 d2 = 55.36 d3 = d1 + d2 = 110 d4 = d3 * d2 = 6089.6 ==================== changed d1 from 54.64 to -863.244 ======================= d1 = -863.244 d2 = 55.36 d3 = d1 + d2 = -807.884 d4 = d3 * d2 = -44724.4

Véalo en vivo en Coliru

Para los tipos integral simples, mi uso de shared_ptr fue una exageración, en realidad podría hacer esto con los punteros normales. Pero esta implementación tiende a generalizarse en el tipo de typename T de typename T

Otras cosas en que pensar ...

  • Elecciones API
  • Uso de memoria
  • Evitar la detección de ciclos (actualizaciones infinitamente recursivas)
  • ... etc

Comentarios, críticas y sugerencias son bienvenidas. :-)


Escriba una envoltura, que almacenará los punteros a double s (como se recomienda en su pregunta original). Tenga en cuenta que esto no funcionará si el double s saldrá del alcance pero el counter no. Además, puede sobrecargar la conversión al operador T para deshacerse de la función total() .

template<typename T> class counter { public: void regist(T& d) { refs.push_back(&d); } T total() { T total{}; for (auto d : refs) total += *d; return total; } private: std::vector<T*> refs; }; int main(int argc, char* argv[]) { double d1 = 1.6; double d2 = 7.2; double d3 = 1e-4; counter<double> cnt; cnt.regist(d1); cnt.regist(d2); cnt.regist(d3); std::cout << cnt.total() << std::endl; // 8.8001 d2 = -7.1; std::cout << cnt.total() << std::endl; // -5.4999 }


No hay manera de hacer esto sin cambiar el tipo de d1 y d2 en algo observable. Puedes hacer algo como esto.

#include <iostream> #include <boost/signals2.hpp> class ObservableDouble { public: boost::signals2::signal<void()> sigChanged; void operator=( double val ) { d = val; sigChanged(); } operator double() const { return d; } private: double d; }; int main() { ObservableDouble d1; ObservableDouble d2; double d3; auto add = [&](){ d3 = d1 + d2; }; d1.sigChanged.connect( add ); d2.sigChanged.connect( add ); d1 = 4.0; d2 = 7.0; std::cout << d3 << std::endl; // prints 11 d2 = -4.0; std::cout << d3 << std::endl; // prints 0 }

Vivir en coliru


Puedes crear una envoltura, como un lambda:

double d1 = 0, d2 = 0; auto l = [&](){ return d1 + d2; }; l(); // 0 d1 = 40; d2 = 2; l(); // 42

Si desea que todas las variables tengan el mismo tipo, puede agregar un contenedor de borrado de tipo como std::function :

std::function<double()> f1 = [] { return 0; }; std::function<double()> f2 = [] { return 0; }; std::function<double()> sum = [&] { return f1() + f2(); }; std::cout << sum() << std::endl; // 0 f1 = [] { return 40; }; f2 = [] { return 2; }; std::cout << sum() << std::endl; // 42


Su problema es la motivación clásica para la vinculación de parámetros.

#include <iostream> #include <functional> //generic add template <typename T> void Add(T x, T y, T & z){ z = x + y; } int main(){ //z will change automatically in function call double z = 0; //bind z as the result using namespace std::placeholders; auto add = std::bind(Add<double>, _1, _2, std::ref(z)); //z is implicity passed and changed add(6,4); //outputs 10 std::cout << z << ''/n''; }

bind envoltorios de bind y referencia pueden ayudar a lograr la funcionalidad que está buscando.