Reputation: 759
Consider the following code.
#include <cassert>
#include <stdexcept>
#include <vector>
struct Item {
Item() : m_enabled(false) {}
void enable()
{
m_enabled = true;
notify_observers(); // can throw
}
bool enabled() const {return m_enabled;}
private:
bool m_enabled;
};
struct ItemsContainer {
ItemsContainer() : m_enabled(false) {}
void enable()
{
bool error = false;
for(Item & item : m_items) {
try {
item.enable();
} catch(...) {
error = true;
}
}
m_enabled = true;
if(error) throw std::runtime_error("Failed to enable all items.");
}
bool enabled() const
{
for(Item const & item : m_items) assert(item.enabled() == m_enabled);
return m_enabled;
}
private:
std::vector<Item> m_items;
bool m_enabled;
};
In my situation, Item
is implemented as shown (I cannot change it) and I am trying to implement ItemsContainer
. I don't exactly know how to deal with exceptions.
In the proposed implementation, when enabling the container, we enable all the items even if notifying the observers of one of them throws, and in the end, we potentially throw an exception to notify the caller that there was a problem with (at least) one of the observers. However the state of the container is modified. Is it counter-intuitive? Should I try to give the strong exception guarantee to ItemsContainer::enable
by disabling the already enabled items in case of failure and keeping initial state unchanged?
In ItemsContainer::enabled
, are the assertions reasonable? Since I don't have the control on Item
and no exception guarantee is documented, it could occur that the instructions m_enabled = true;
and notify_observers();
are swapped. In this case, ItemsContainer::enable
could break the internal state of the container.
It is not the first time I face such a situation. Are there best practice rules?
NB: I reduced the code sample to the minimum, but in fact, Item::enable
is a setter: Item::set_enabled(bool)
and I want to implement the same thing for ItemsContainer
. This means that items can be disabled if enabling fails, but here again, without specification of the exception guarantee.
Upvotes: 2
Views: 168
Reputation: 72054
If the library you depend on does not give you behavior guarantees in exceptional cases, then you cannot give dependent behavior guarantees. If it gives you the basic guarantee, you can work with that to a point, but potentially at very high cost.
In your case, for example, you want to maintain the invariant that either all items are enable, or none are. So your options are
setEnabledWithTransaction
. Is Item
copyable? What does it mean for the observers if an item is copied, but the copy then discarded, or the old object modified?Upvotes: 2
Reputation: 3731
Well this seems more like an application choice than an API choice. Depending on your application you may want to do any of:
Item
s in their current stateItem
s to previous state (transaction type protection) on failure then throwIf you think that your users may want more than one of those then you could easily design it into the API:
bool setEnabled(bool enable, bool failFast = false)
{
bool error = false;
for(Item & item : m_items) {
try {
enable ? item.enable() : item.disable();
} catch(...) {
error = true;
if(failFast)
throw std::runtime_error("Failed to enable all items.");
}
}
m_enabled = true;
return error;
}
bool setEnabledWithTransaction(bool enable)
{
bool error = false;
vector<Item> clone(m_items);
for(Item & item : m_items) {
try {
enable ? item.enable() : item.disable();
} catch(...) {
// use the saved state (clone) to revert to pre-operation state.
}
}
m_enabled = true;
return error;
}
Note that the bool
return value indicated success. Alternatively you could return the number of successfully enabled items. This is what you should do if the chance of a failure is non-trivial. If the chance of a failure is very very small, or if a failure indicates something that is a system failure then you should throw.
Upvotes: 1