Is it possible to avoid the boilerplate?
No.
C++ has very limited code generation abilities, injecting code automatically is not part of them.
Disclaimer: the following is a deep-dive in proxying, with the call of preventing the user from getting their grubby paws on the functions they should not call without bypassing the proxy.
Is it possible to make forgetting to call pre-/post-function harder?
Enforcing delegation through a proxy is... annoying. Specifically, the functions cannot possibly be public
or protected
, as otherwise the caller can get its grubby hands on them and you may declare forfeit.
One potential solution is thus to declare all functions private, and provide proxies that enforce the logging. Abstracted this, to make this scale across multiple classes, is horrendously boiler-platey, though it is a one-time cost:
template <typename O, typename R, typename... Args>
class Applier {
public:
using Method = R (O::*)(Args...);
constexpr explicit Applier(Method m): mMethod(m) {}
R operator()(O& o, Args... args) const {
o.pre_call();
R result = (o.*mMethod)(std::forward<Args>(args)...);
o.post_call();
return result;
}
private:
Method mMethod;
};
template <typename O, typename... Args>
class Applier<O, void, Args...> {
public:
using Method = void (O::*)(Args...);
constexpr explicit Applier(Method m): mMethod(m) {}
void operator()(O& o, Args... args) const {
o.pre_call();
(o.*mMethod)(std::forward<Args>(args)...);
o.post_call();
}
private:
Method mMethod;
};
template <typename O, typename R, typename... Args>
class ConstApplier {
public:
using Method = R (O::*)(Args...) const;
constexpr explicit ConstApplier(Method m): mMethod(m) {}
R operator()(O const& o, Args... args) const {
o.pre_call();
R result = (o.*mMethod)(std::forward<Args>(args)...);
o.post_call();
return result;
}
private:
Method mMethod;
};
template <typename O, typename... Args>
class ConstApplier<O, void, Args...> {
public:
using Method = void (O::*)(Args...) const;
constexpr explicit ConstApplier(Method m): mMethod(m) {}
void operator()(O const& o, Args... args) const {
o.pre_call();
(o.*mMethod)(std::forward<Args>(args)...);
o.post_call();
}
private:
Method mMethod;
};
Note: I am not looking forward to adding support for volatile
, but nobody uses it, right?
Once this first hurdle passed, you can use:
class MyClass {
public:
static const Applier<MyClass, void> a;
static const ConstApplier<MyClass, int, int> b;
void pre_call() const {
std::cout << "before\n";
}
void post_call() const {
std::cout << "after\n";
}
private:
void a_impl() {
std::cout << "a_impl\n";
}
int b_impl(int x) const {
return mMember * x;
}
int mMember = 42;
};
const Applier<MyClass, void> MyClass::a{&MyClass::a_impl};
const ConstApplier<MyClass, int, int> MyClass::b{&MyClass::b_impl};
It's quite the boilerplate, but at least the pattern is clear, and any violation will stick out like a sore thumb. It's also easier to apply post-functions this way, rather than tracking each and every return
.
The syntax to call is not exactly that great either:
MyClass c;
MyClass::a(c);
std::cout << MyClass::b(c, 2) << "\n";
It should be possible to do better...
Note that ideally you would want to:
- use a data-member
- whose type encode the offset to the class (safely)
- whose type encode the method to call
A half-way there solution is (half-way because unsafe...):
template <typename O, size_t N, typename M, M Method>
class Applier;
template <typename O, size_t N, typename R, typename... Args, R (O::*Method)(Args...)>
class Applier<O, N, R (O::*)(Args...), Method> {
public:
R operator()(Args... args) {
O& o = *reinterpret_cast<O*>(reinterpret_cast<char*>(this) - N);
o.pre_call();
R result = (o.*Method)(std::forward<Args>(args)...);
o.post_call();
return result;
}
};
template <typename O, size_t N, typename... Args, void (O::*Method)(Args...)>
class Applier<O, N, void (O::*)(Args...), Method> {
public:
void operator()(Args... args) {
O& o = *reinterpret_cast<O*>(reinterpret_cast<char*>(this) - N);
o.pre_call();
(o.*Method)(std::forward<Args>(args)...);
o.post_call();
}
};
template <typename O, size_t N, typename R, typename... Args, R (O::*Method)(Args...) const>
class Applier<O, N, R (O::*)(Args...) const, Method> {
public:
R operator()(Args... args) const {
O const& o = *reinterpret_cast<O const*>(reinterpret_cast<char const*>(this) - N);
o.pre_call();
R result = (o.*Method)(std::forward<Args>(args)...);
o.post_call();
return result;
}
};
template <typename O, size_t N, typename... Args, void (O::*Method)(Args...) const>
class Applier<O, N, void (O::*)(Args...) const, Method> {
public:
void operator()(Args... args) const {
O const& o = *reinterpret_cast<O const*>(reinterpret_cast<char const*>(this) - N);
o.pre_call();
(o.*Method)(std::forward<Args>(args)...);
o.post_call();
}
};
It adds one byte per "method" (because C++ is weird like this), and requires some fairly involved definitions:
class MyClassImpl {
friend class MyClass;
public:
void pre_call() const {
std::cout << "before\n";
}
void post_call() const {
std::cout << "after\n";
}
private:
void a_impl() {
std::cout << "a_impl\n";
}
int b_impl(int x) const {
return mMember * x;
}
int mMember = 42;
};
class MyClass: MyClassImpl {
public:
Applier<MyClassImpl, sizeof(MyClassImpl), void (MyClassImpl::*)(), &MyClassImpl::a_impl> a;
Applier<MyClassImpl, sizeof(MyClassImpl) + sizeof(a), int (MyClassImpl::*)(int) const, &MyClassImpl::b_impl> b;
};
But at least usage is "natural":
int main() {
MyClass c;
c.a();
std::cout << c.b(2) << "\n";
return 0;
}
Personally, to enforce this I would simply use:
class MyClass {
public:
void a() { log(); mImpl.a(); }
int b(int i) const { log(); return mImpl.b(i); }
private:
struct Impl {
public:
void a_impl() {
std::cout << "a_impl\n";
}
int b_impl(int x) const {
return mMember * x;
}
private:
int mMember = 42;
} mImpl;
};
Not exactly extraordinary, but simply isolating the state in MyClass::Impl
makes difficult to implement logic in MyClass
, which is generally sufficient to ensure that maintainers follow the pattern.