[Development] RFC: RAII for property changes
Marc Mutz
marc.mutz at kdab.com
Wed Apr 15 21:33:12 CEST 2015
On Wednesday 15 April 2015 16:58:24 André Somers wrote:
> Basically, if you have this code (or something like it) somewhere, you
> already have a problem:
>
> void MyClass::setFooAndBar(QString foo, int bar)
> {
> if (m_foo != foo) {
> m_foo = foo;
> emit fooChanged(foo); //this is where things may start to go wrong
> already
> }
>
> if (m_bar != bar) {
> m_bar = bar;
> emit barChanged(bar);
> }
> }
1. This function has more than one responsibility. Functions with more than
one responsibility are hard to get right, to maintain, and in general
hard to make exception-safe.
2. This function doesn't have all-or-nothing / transactional semantics.
That's basically both different way of saying that functions should be
structured such that they:
1st perform anything that can fail _without_ modifying the state of the
program
2nd commit the new state with never-fail operations
3rd clean up (this includes notification)
> void MyClass::setFooAndBar(QString foo, int bar)
> {
>
> bool fooHasChanged(false);
> bool barHasChanged(false);
>
> if (m_foo != foo) {
>
> m_foo = foo;
> fooHasChanged = true;
>
> }
>
> if (m_bar != bar) {
>
> m_bar = bar;
> barHasChanged = true;
>
> }
>
> if (fooHasChanged) emit fooHasChanged(foo);
> if (barHasChanged) emit barHasChanged(bar); // [...]
> }
This code still doesn't meet that goal. It first modifies m_foo. If the
assignment to m_bar throws, then m_foo has been changed, but m_bar hasn't.
void MyClass::setFooAndBar(QString foo, int bar)
{
const bool fooHasChanged = m_foo != foo;
const bool barHasChanged = m_bar != bar;
m_foo.swap(foo);
m_bar = bar; // int can't throw
if (fooHasChanged) emit fooHasChanged(m_foo);
if (barHasChanged) emit barHasChanged(m_bar);
}
Untested code:
#define EMIT_AT_SCOPE_EXIT_WHEN_CHANGED(value, signal) \
struct Guard##value {
decltype(value) oldValue;
decltype(value) &ref;
~Guard##value() { if (ref != oldValue) emit signal(ref); }
} guard##value = { value, value };
For C++98, the type would have to be passed instead of using decltype.
It's still less efficient than it could be, due to oldValue (imagine it being
a std::vector), but that can be fixed at the cost of more arguments. Maybe a
lot of this stuff can also be templatised, to effectively get something like:
const GuardBase &valueGuard = guard(value, &signal);
But if the call to signal in this local scope is still indirect, as it is with
most function pointers, this is still a no-no, imo.
Thanks,
Marc
--
Marc Mutz <marc.mutz at kdab.com> | Senior Software Engineer
KDAB (Deutschland) GmbH & Co.KG, a KDAB Group Company
Tel: +49-30-521325470
KDAB - The Qt Experts
More information about the Development
mailing list