Alternatives to virtual functions, member function design, non-member functions, and exceptions in destructors.
Instead of making a function directly virtual, expose a public non-virtual wrapper that calls a private virtual implementation. This lets the base class control setup/teardown while still allowing derived classes to customize the core behavior:
class Enemy {
public:
virtual int HealthValue() const;
};
class Enemy {
public:
int HealthValue() const {
// setup...
int val = DoHealthValue();
// cleanup...
return val;
}
private:
virtual int DoHealthValue() const;
};
The public HealthValue() acts as a "wrapper" — it can enforce logging, locking, validation, or cleanup around whatever the derived class provides. Only DoHealthValue() can be changed by derived classes.
Instead of a virtual function, store a function pointer as a member. This allows different instances of the same type to use entirely different implementations — and the implementation can be swapped at runtime:
using HealthFunc = int(*)(const Enemy*);
class Enemy {
public:
int HealthValue() const {
return healthFunc(this);
}
private:
HealthFunc healthFunc;
};
std::function or a lambda, which are more flexible and can capture state.Member functions have access to all private data. A non-member non-friend function (just a regular free function) can only use the public interface. If functionality can be expressed through the public interface, moving it outside the class:
class Widget {
public:
void ClearScrollbar();
void ClearCookies();
void ClearHistory();
private:
void InternalData();
};
// Non-member non-friend — only uses public interface
void ClearBrowser(Widget& w) {
w.ClearScrollbar();
w.ClearCookies();
w.ClearHistory();
}
ClearBrowser adds no new access to Widget's private data — it just orchestrates public calls. This is strictly more encapsulated than making it a member function. You can still put this free function in a namespace alongside the class.When you want implicit conversions to apply to both operands of a binary operator, the operator must be a non-member. As a member function, only the right-hand side (the parameter) is eligible for conversion — the left-hand side (the caller) is not.
class Num {
public:
Num() = default;
Num(int x) {} // implicit conversion from int
// Member operator: left side must already be a Num
Num operator*(const Num& rhs) const;
};
Num n;
n * 4; // OK — 4 is converted to Num (it's the parameter)
4 * n; // ERROR — 4 is the caller, not the parameter, can't be converted
Moving the operator outside the class makes both sides parameters, so both are eligible for conversion:
// Non-member: both lhs and rhs are parameters — both can be converted
Num operator*(const Num& lhs, const Num& rhs) {
// ...
}
4 * n; // now OK — 4 is passed as lhs and converted to Num
Do not allow exceptions to leave destructors. If an exception is thrown in the middle of a destructor, the rest of the object may never be destroyed — leading to resource leaks and undefined behavior. This is especially dangerous if the destructor is being called during stack unwinding from another exception.
If a destructor calls code that might throw, you have three options:
| Option | What it does | When to use |
|---|---|---|
| Terminate the program | std::abort() if the operation fails | When continuing with a corrupted object is worse than crashing |
| Swallow the exception | try/catch inside the destructor, ignore the exception | When the failure is non-critical and you'd rather limp along |
| Expose a separate cleanup function (recommended) | Let the user call a Close() / Destroy() method manually, and only fall back to swallowing in the destructor | Most cases — gives the caller a chance to handle the error |
class Connection {
public:
void Close() {
// can throw — user handles it
}
~Connection() {
if (!closed) {
try {
Close();
} catch (...) {
Log("Close failed during destruction"); // swallow
}
}
}
};