Как я могу сделать переменную всегда равной результату некоторых вычислений?
В математике, если z = x+y/2
, то z
всегда будет меняться всякий раз, когда мы заменяем значения x
и y
. Можем ли мы сделать это в программировании без необходимости конкретного обновления z
всякий раз, когда мы меняем значения x
и y
?
Я имею в виду что-то подобное не будет работать, верно?
int x;
int y;
int z{x + y};
cin >> x;
cin >> y;
cout << z;
Если вы не уверены, зачем мне это нужно, я хочу, чтобы переменная показывалась в реальном времени, и она обновлялась автоматически при изменении rhs-переменной.
Например, когда убиваете крипа и получаете золото, чистая стоимость (наличные + стоимость собственных предметов) меняется. Или измеритель скорости автомобиля, который меняется в зависимости от скорости или скорости движения.
Ответы
Ответ 1
Изменить: Хотя я полностью ответил на вопрос в том виде, в котором он был задан, пожалуйста, взгляните и на ответ Артелия. В нем рассматриваются некоторые вопросы, на которые мой ответ не распространяется (инкапсуляция, избежание избыточности, риски висячих ссылок). Возможная оптимизация, если расчет дорог, показан в ответе Джонатана Ми.
Вы имеете в виду что-то вроде этого:
class Z
{
int& x;
int& y;
public:
Z(int& x, int& y) : x(x), y(y) { }
operator int() { return x + y; }
};
Класс задерживает вычисление результата, пока не будет приведен к типу int. Поскольку оператор приведения не является явным, Z
может использоваться всякий раз, когда требуется int. Поскольку существует перегрузка operator<<
для int, вы можете использовать его, например, непосредственно с std::cout
:
int x, y;
Z z(x, y);
std::cin >> x >> y;
if(std::cin) // otherwise, IO error! (e. g. bad user input)
std::cout << z << std::endl;
Имейте в виду, что вызов функции (неявный оператор приведения) по-прежнему существует, даже если он не отображается. И на самом деле оператор выполняет некоторые истинные вычисления (вместо того, чтобы просто получить доступ к внутреннему члену), поэтому сомнительно, что хорошая идея скрыть вызов функции...
Ответ 2
Вы можете приблизиться к этому с помощью лямбды в C++. Как правило, когда вы устанавливаете переменную, как
int x;
int y;
int z{x + y};
z
будет только результатом x + y
в это время. Вы должны будете сделать z = x + y;
каждый раз, когда вы меняете x
или y
чтобы обновлять его.
Однако если вы используете лямбду, вы можете сделать так, чтобы он захватывал, на какие объекты он должен ссылаться, и какие вычисления следует выполнить, и затем каждый раз, когда вы получаете доступ к лямбде, он будет давать вам результат в тот момент времени. Это выглядит как
int x;
int y;
auto z = [&](){ return x + y; };
cin >> x;
cin >> y;
cout << z();
и теперь z()
будет иметь правильное значение вместо неинициализированного мусора, который имел оригинальный код.
Если вычисления очень дороги, вы можете даже добавить кеширование в лямбду, чтобы убедиться, что вычисления не выполняются, когда вам это не нужно. Это будет выглядеть
auto z = [&](){ static auto cache_x = x;
static auto cache_y = y;
static auto cache_result = x + y;
if (x != cache_x || y != cache_y)
{
cache_x = x;
cache_y = y;
cache_result = x + y;
}
return cache_result;
};
Ответ 3
Самое близкое, что вы можете получить, это создать функтор:
#include <iostream>
int main() {
int x;
int y;
auto z = [&x, &y] { return x + y; }; // a lambda capturing x and y
while(true) {
std::cin >> x;
std::cin >> y;
std::cout << z() << "\n";
}
}
Ответ 4
Есть два главных метода:
-
Отсроченный расчет - вместо того, чтобы z
была простой переменной, сделайте ее функцией, которая вычисляет значение по требованию (примеры см. В других ответах). Это может быть прозрачно для исходного кода, если z
- некоторый прокси-объект с неявным преобразованием в требуемый тип (как в ответе Аконкагуа).
-
Явное уведомление об изменениях. Это требует, чтобы x
и y
были наблюдаемыми типами; когда любой из них изменяет значение, z
обновляет себя (и уведомляет своих наблюдателей, если это применимо).
Первая версия обычно предпочтительнее, но вторая может быть более подходящей, если вам нужно, чтобы z
был наблюдаемым типом.
Ответ 5
Это звучит как проблема XY (каламбур).
Судя по всему, вы на самом деле не пишете код в соответствии с хорошими объектно-ориентированными практиками. Я бы посоветовал вам не использовать "хитрости", предложенные другими людьми, а научиться лучше использовать структуру ОО.
Прежде чем я углублюсь в это, обратите внимание, что присвоение отличается от отношения равенства. =
В C++ является присваиванием, которое не совпадает с =
в математике. Существует несколько (но не так много) языков программирования, которые поддерживают отношения равенства, но C++ не является одним из них. Дело в том, что добавление поддержки отношений равенства создает кучу новых проблем, поэтому это не так просто, как "почему этого еще нет в C++".
В любом случае, в этом случае вам, вероятно, следует инкапсулировать связанные переменные в классе. Затем вы можете использовать методы для получения "актуальной" информации. Например:
class Player {
std::vector<int> inventory;
int cash;
public:
int inventory_total();
int net_worth();
}
//adds up total value of inventory
int Player::inventory_total() {
int total = 0;
for(std::vector<int>::iterator it = inventory.begin(); it != inventory.end(); ++it) {
total += *it;
}
return total;
}
//calculates net worth
int Player::net_worth() {
//we are using inventory_total() as if it were a variable that automatically
//holds the sum of the inventory values
return inventory_total() + cash;
}
...
//we are using net_worth() as if it were a variable that automatically
//holds the sum of the cash and total holdings
std::cout << player1.net_worth();
Я допускаю, что добавить это поведение в класс немного сложнее, чем сказать z = x + y
, но на самом деле это всего лишь несколько дополнительных строк кода.
Это было бы очень раздражающим и подверженным ошибкам, если бы вы забыли вызвать функцию где-нибудь.
В этом случае у объекта нет переменной-члена net_worth
, поэтому вы не можете случайно использовать ее вместо вызова функции.
Ответ 6
- Вы создаете функцию для этого.
- Вы вызываете функцию с соответствующими аргументами, когда вам нужно значение.
int z(int x, int y)
{
return (x + y);
}
int x;
int y;
// This does ot work
// int z{x + y};
cin >> x;
cin >> y;
cout << z(x, y);
Ответ 7
Вы можете определить следующую лямбду z
которая всегда возвращает текущее значение x+y
потому что x
и y
захвачены ссылкой:
DEMO
int main()
{
int x;
int y;
const auto z = [&x, &y](){ return x+y; };
std::cin >> x; // 1
std::cin >> y; // 2
std::cout << z() << std::endl; // 3
std::cin >> x; // 3
std::cin >> y; // 4
std::cout << z() << std::endl; // 7
}
Ответ 8
Таким образом, большая проблема, которую я вижу с предоставленными лямбда-решениями, состоит в том, что z
вычисляется каждый раз, когда он проверяется, даже если x
и y
не изменились. Чтобы обойти это, вам действительно нужно связать эти переменные. Я бы предложил сделать это через class
:
class foo {
int x;
int y;
int z;
void calculate() { z = (x + y) / 2; }
friend istream& operator >>(istream& lhs, foo& rhs);
public:
void set_x(const int param) {
x = param;
calculate();
}
int get_x() const { return x; }
void set_y(const int param) {
y = param;
calculate();
}
int get_y() const { return y; }
int get_z() const { return z; }
};
istream& operator >>(istream& lhs, foo& rhs) {
lhs >> rhs.x >> rhs.y;
rhs.calculate();
return lhs;
}
Это будет пересчитывать z
каждый раз, когда x
или y
установлены. Это хорошее решение, если вы часто z
к z
, а x
и y
задаются нечасто. Если x
и y
устанавливаются часто или calculate
стоит дорого, вы можете подумать:
class foo {
int x;
int y;
int z;
bool dirty;
void calculate() { z = (x + y) / 2; }
friend istream& operator >>(istream& lhs, foo& rhs);
public:
void set_x(const int param) {
x = param;
dirty = true;
}
int get_x() const { return x; }
void set_y(const int param) {
y = param;
dirty = true;
}
int get_y() const { return y; }
int get_z() const {
if(dirty) {
calculate();
}
return z;
}
};
istream& operator >>(istream& lhs, foo& rhs) {
lhs >> rhs.x >> rhs.y;
rhs.dirty = true;
return lhs;
}
Обратите внимание, что я включил оператор извлечения, поэтому любой выбранный вами код может превратиться во что-то простое:
foo xyz;
cin >> xyz;
cout << xyz.get_z();
Ответ 9
Вы можете получить то, что просите, используя макросы:
{
int x, y;
#define z (x + y)
/* use x, y, z */
#undef z
}
#undef
для небольшого здравомыслия. Для большего здравомыслия, не используйте макросы вообще, иди с одним из других ответов, и разберись с лишним многословием.
Хотя во многих случаях класс с пользовательским operator int
будет работать... хм.
Ответ 10
То, что вы описываете, является поздним связыванием, которое скомпилированный язык, такой как C++, может делать только с трудом. В интерпретируемом языке все, что вам нужно, - это возможность установить z в неоцененное выражение и задержать привязку значения z до тех пор, пока не понадобятся вычисления, как правило, сигнализируемые вызовом функции, которая вызывает оценку, такую как eval в Lisp. На языке правил моей Экспертной системы у меня есть не только eval, но и noeval, который защищает свои аргументы от одного уровня оценки. Это обеспечивает детальный контроль над связыванием, причем некоторые подвыражения оцениваются (связываются), а другие - нет, если это необходимо. Это не применимо к вашему сценарию, но устанавливает сцену с точки зрения языкового ландшафта.
Ответ 11
Используйте функцию. Всякий раз, когда вы захотите изменить x или y, вызовите функцию, передавая их новые значения, и z будет соответственно обновлено.
void func(int x, int y)
{
z = // something involving x or y
}
Ответ 12
Вы можете написать класс, который инкапсулирует его состояние, чтобы обновлять его либо при мутации, либо возвращать правильный результат по запросу:
#include <iostream>
template<typename T, typename U, typename V>
class DynamicCalc
{
public:
DynamicCalc(const T& func, const U& memberOne, const V& memberTwo) :
_func(func)
, _memberOne(memberOne)
, _memberTwo(memberTwo)
{
}
void SetMemberOne(const U& memberOne) { _memberOne = memberOne; }
void SetMemberTwo(const U& memberTwo) { _memberTwo = memberTwo; }
auto Retrieve() { return _func(_memberOne, _memberTwo); }
U GetMemberOne() { return _memberOne; }
V GetMemberTwo() { return _memberTwo; }
private:
T _func;
U _memberOne;
V _memberTwo;
};
int main() {
auto func = [](int x, int y) {
return x + y;
};
DynamicCalc<decltype(func), int, int> c(func, 3, 5);
c.SetMemberOne(5);
std::cout << c.Retrieve();
}
По правде говоря, если вы счастливы, что вычисление произойдет при повторном запросе значения, тогда методы получения/установки не нужны.
Ответ 13
Хорошо, позвольте мне наконец написать правильный и единственно верный ответ на поставленный вопрос:
Ты не можешь
Вы не можете написать z = x + y
а затем заставить весь код, используя z
магически перезапускаться всякий раз, когда x
или y
изменяется.
Так что можно сделать?
Как упомянуто в других ответах, есть несколько шаблонов, чтобы выразить, что вы хотите, чтобы изменения x и y вызывали некоторые обновления, но в любом случае вам нужно, чтобы эти обновления происходили более или менее явно.
В зависимости от варианта использования вы можете:
-
Имейте значение, все равно пересчитанное в любом случае, это имеет значение. Например, если вы пишете игру и перерисовываете экран каждый кадр, то, вероятно, достаточно просто убедиться, что вы случайно не удерживаете значение z между кадрами. Знайте, когда ваша ценность может измениться, а когда нет. Используете ли вы функцию, лямбду, метод класса или просто повторяете выражение - это в основном эстетическое решение. Если возможно, это лучший подход, потому что он полностью прозрачен.
Например, в гоночной игре вы, скорее всего, обновите текущую скорость в начале расчета нового тика, а затем будете использовать обновленное значение при вычислении движения вашего автомобиля, при перерисовке индикатора скорости, при создании размытия движения и т.д., Вам не нужно никакого волшебства и даже не функции, вы можете просто использовать переменную, потому что вы знаете, что ваша скорость не изменится в течение одного кадра.
-
Назовите обновление явно. Используйте его, например, когда у вас есть один виджет, который нужно обновить. Недостатком является то, что вы должны помнить, чтобы вызвать обновление, что несколько хрупко, но с другой стороны - это очень просто. Середина - интегрировать вызов обновления с сеттером, что делает его некачественной реализацией Observer.
-
Используйте шаблон Observer (см. Также сигналы и слоты, это один из способов реализации Observer). Используйте его, например, когда у вас есть много виджетов для обновления, или вы создаете их динамически. Старайтесь не использовать его, когда один из вышеперечисленных работает, они намного проще.
-
Используйте выделенную библиотеку реактивного программирования. Поскольку такие вещи существуют, я чувствую себя обязанным упомянуть об этом. Однако я, честно говоря, не вижу ни одного приложения, где бы я его использовал. В основном это кажется сложным способом отстрелить ноги. Неявные обновления будут иметь неприятные последствия, и вам придется все переписать. Только не в C++. Что важно: хотя этот подход наиболее близок к "магическому обновлению всего", он будет накладывать ограничения на то, как вы пишете свой код, и в итоге вы получите одно из вышеперечисленных решений, только более сложное.