Skip to content

Conversation

@francisduffy
Copy link
Contributor

I noticed an issue with deferred updates in that elements of the deferredObservers_ set can become null during the ObservableSettings::enableUpdates() call. The problem is that at the start of that method here

updatesDeferred_ = false;
updatesDeferred_ is set to false. Then, there is the loop over deferredObservers_ calling deferredObserver->update() on each. It is during this loop, that the call to update() for a deferredObserver "earlier" in the deferredObservers_ set leads to the destruction of a deferredObserver "later" in the deferredObservers_ set. The destructor of the later deferredObserver is called and Observer::~Observer() and observable->unregisterObserver(this) are called
inline Observer::~Observer() {

The issue is that updatesDeferred_ is at this point false and hence the line here

ObservableSettings::instance().unregisterDeferredObserver(o);
is skipped and the deferredObserver that is destructed is left in the deferredObservers_ set.

I added a test in commit c2b5cc8 to demonstrate the issue. In commit ad90c6f, I added a fix. The fix is based on the proposal by @pcaspers here. Let me know what you think. I can make amendments if you think the fix can be improved.

The test gives a read access violation. The problem is that some
elements in deferredObservers_ can be destroyed while
ObservableSettings::enableUpdates() is running. Later in the call
deferredObserver->update() is called on the already destructed element
and it gives a read access violation.

The example here uses the zero coupon inflation swap helper to
demonstrate the issue. The zciis_ member is one of the deferred
observers. When ObservableSettings::enableUpdates() is running
ZeroCouponInflationSwapHelper::initializeDates() is called because the
helper is notified and updateDates_ is true. zciis_ is re-assigned and
the original pointee is destroyed. But the original pointee that has
been destroyed is still in the deferredObservers_ set and update() gets
called on it leading to the read access violation.
Add a bool runningDeferredUpdates_ that is set to true when
deferredObservers_ are being processed in
ObservableSettings::enableUpdates(). We keep a set
invalidDeferredObservers_ that holds elements of deferredObservers_ that
are destroyed while ObservableSettings::enableUpdates() is running. We
can then skip calling update on them in
ObservableSettings::enableUpdates().
@coveralls
Copy link

Coverage Status

coverage: 74.174% (+0.005%) from 74.169%
when pulling ad90c6f on francisduffy:deferred_observer_lifetime
into ecefd25 on lballabio:master.

francisduffy added a commit to francisduffy/QuantLib that referenced this pull request Dec 24, 2025
@francisduffy
Copy link
Contributor Author

I added a slightly alternative approach in the commit francisduffy@62d968a. It changes the container for deferredObservers_ from typedef std::set<Observer*> set_type; to typedef std::map<Observer*, bool> set_type; where the map value marks if the deferred Observer key is still valid. I guess there is the overhead of having an extra bool for every deferred observer but I think it may be a bit cleaner than having two sets.

@lballabio lballabio added this to the Release 1.41 milestone Dec 29, 2025
@lballabio lballabio merged commit 2bd9e3d into lballabio:master Dec 29, 2025
43 checks passed
@francisduffy francisduffy deleted the deferred_observer_lifetime branch December 30, 2025 18:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants