Question Details

No question body available.

Tags

c++ class-design invariants

Answers (3)

Accepted Answer Available
Accepted Answer
August 28, 2025 Score: 1 Rep: 220,789 Quality: High Completeness: 60%

Ideally, a third party library which provides a method FooDestroy(...) provides also a method to check whether the object was already destroyed, like IsFooDestroyed(...). That will allow to keep the class invariant mptr!=nullptr. Any method which is incompatible with a "destroyed" mptr needs to look like this :

void Increment() {
    assert(mptr);
    if(IsFooDestroyed(mptr)) {
       throw std::runtimeerror{ "Foo object already destroyed" };
       // or - do nothing - just return, whatever fits best here
    }

++(*m
ptr); }

(you may want to make this thread-safe, but I leave this out for the sake of simplicity, I guess you get the idea.)

Note in your FooHandle example, the wrapper never takes the ownership of the 3rd party pointer, it does not construct the Foo pointer, nor does it delete it. Hence, there could be two or more FooHandle objects sharing the same Foo pointer. When one calls FooDestroy, the others would not get informed about it - just setting mptr to nullptr would only inform "ones own" object about the FooDestroy call, but not the others.

In short, as long as your wrapper class does not create the Foo object to which m
ptr points, it it is a bad idea to mix up the call to "FooDestroy" with setting mptr to nullptr - better maintain the status independently and outside of any FooHandle object. The Destroymethod should look like this:

void Destroy() {
    assert(mptr);
    if (IsFooDestroyed(mptr)) {
        return;
    }

if (FooDestroy(m
ptr) != 0) { throw std::runtime_error{ "Failed to destroy the Foo object" }; } // no nullptr assignment here! }
August 29, 2025 Score: 2 Rep: 79,094 Quality: Medium Completeness: 30%

If you want to write reliable C++ code, then there is one rule that you must always obey: a destructor must not throw or allow an exception to pass out of it. The reason is that there cannot be multiple exceptions trying to unwind the stack at the same time. If that happens, the application will be terminated.

The implication of this rule is that if you have to perform a potentially fallible operation during the cleanup in your destructor, you are more limited to how you can respond to failures in that operation.

Instead of reporting the failure to the calling code and letting them deal with it, you will have to deal with it yourself. If the failure is especially serious, this can mean terminating the application, but it is more likely that the appropriate reaction is to log the failure for future (offline) analysis and continue as-if nothing happened.


If you decide to modify your resource-handling class to expose a cleanup operation to the user, then you have expanded the valid states of the class with a "cleaned-up" state and you must update all existing methods to properly deal with that new state (even if that is testing for the state and throwing an exception that the method cannot be used in that state).

August 29, 2025 Score: 0 Rep: 4,567 Quality: Low Completeness: 60%

Yes, assertions can not be used to track a state exposed to clients. Replace all assertions that verify user-modifiable state with exceptions. Do not expose the state otherwise, prefer idempotent disposal.

As @GregBurghardt correctly noticed, construction and destruction should be done in the same context, make sure not to pass around a pointer to a third-party resource without associated disposer.

class FooHandle {
private:
    Foo m_ptr;

public: FooHandle(Arguments arguments) : m_ptr{ ptr } { m_ptr = FooCreate(arguments); // ensure that resource creation and disposal share component, do not split them assert(m_ptr); // Replace with an exception, if user can provide invalid Arguments and break FooCreate }

void Increment() { EnsureOpened(); ++(
mptr); }

operator Foo() const { EnsureOpened(); return *m
ptr; }

void Destroy() { if (mptr && FooDestroy(mptr) != 0) { // Do not require clients to track our internal state, make the API idempotent instead throw std::runtimeerror{ "Failed to destroy the Foo object" }; } mptr = nullptr; }

~FooHandle() { try { Destroy(); } catch(const std::runtimeerror & e ) { // report failure } } private: void EnsureOpened() { if (!mptr) { throw std::logic_error("The resource is destroyed"); } } };