Подключение трех разных объектов

Я задал аналогичный вопрос пару часов назад о соединении двух элементов вектора. Теперь я хотел бы сделать мой вопрос более общим. Предположим, что мы имеем два объекта типа double, а именно double d1, d2. Мы хотим, чтобы третий объект (double d3) получил значение d1+d2, так что, если мы изменим d1 или d2, тогда d3 автоматически получит новое значение d1+d2. Как мы можем это сделать в С++?

Вот что я имею в виду:

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

Спасибо.

Ответы

Ответ 1

Вы можете создать обертку в виде лямбда:

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

l(); // 0
d1 = 40;
d2 = 2;
l(); // 42

Если вы хотите, чтобы все переменные имели один и тот же тип, вы можете добавить оболочку стирания типа как 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

Ответ 2

Ваша проблема - классическая мотивация привязки параметров.

#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 и справочные обертки могут помочь в достижении функциональности, которой вы пользуетесь.

Ответ 3

Напишите оболочку, в которой будут храниться указатели на double (как рекомендовано в исходном вопросе). Помните, что это не сработает, если double выйдет за пределы области видимости, но counter не будет. Кроме того, вы можете перегрузить преобразование в оператор T, чтобы избавиться от функции 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
}

Ответ 4

Во время компиляции Нет, в лучшем случае, у вас появятся некоторые неприятные шаблоны и макро-хаки, которые по-прежнему будут сильно ограничены. Если ваша мысль находится во время компиляции, не читайте остальную часть ответа.

От уровня пользователя (во время выполнения)? Да, ты можешь. Хотя логика довольно проста, вы просто должны найти способ прагматически создавать и поддерживать инварианты дерева выражений. Для этого требуется немного работы.

Итак, Давайте, займемся логикой... Для простоты определим некоторые основные термины здесь, чтобы соответствовать нашим намерениям

  • An operator - это функция, которая требует не более 2 operands, так что при вызове она выдает результат, который является другим operand
  • An operand - объект, представляющий интерес, в вашем случае, цифры, точнее double. Он может быть создан вами или expression
  • An expression - это объект, который занимает не более 2 operands и operator, и создает результирующий operand, вызывая функцию operator.

Я сделал несколько рисунков, чтобы проиллюстрировать это....

Дерево выражений

Как вы можете видеть, стрелки показывают направление знания.

  • An operand знает все выражения, в которых он участвует.
  • An expression знает созданный operands.

Итак, дадим им некоторые тождества...

Дерево выражений

Скажем, вы создали Operand 1, Operand 2, Operand 4. И вы начали строить это дерево выражений в следующем порядке:

  • Вы создали связь (expression) между Operand 1 и Operand 2, которая представлена ​​Expression1.

  • Expression1 использует operator, который был сконструирован для получения его результата, Operand 3

  • Вы объединили полученный Operand 3 с созданным Operand 4 в новое выражение Expression2, чтобы создать другой результат, Operand 5


Теперь давайте посмотрим, что произойдет, когда мы решим изменить Operand 1.

Влияние изменения одного операнда

Как вы можете видеть, модифицированный операнд будет рекурсивно проходить и обновлять все подвыражения, результат которых зависит от него (напрямую или через прокси).


Теперь, когда у нас есть эта очень простая идея, как мы это сделаем. Существует несколько способов его реализации, более общий и гибкий, тем меньше он будет, по-видимому, (с точки зрения памяти и скорости).

Я сделал простую реализацию ниже (очевидно, далеко не что-то Оптимальное).

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

Выход:

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

Смотрите Live on Coliru

Для простых типов integral мое использование shared_ptr было чрезмерным, я мог бы сделать это с помощью обычных указателей. Но эта реализация имеет тенденцию обобщать на тип typename T.

Другие вещи, о которых нужно подумать...

  • Варианты API
  • Использование памяти
  • Предотвращение обнаружения цикла (бесконечно рекурсивные обновления)
  • ... и т.д.

Комментарии, критика и предложения приветствуются.: -)

Ответ 5

Нет никакого способа сделать это, не изменяя тип d1 и d2 во что-то наблюдаемое. Вы можете сделать что-то вроде этого.

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

Live on Coliru