Как обеспечить, чтобы каждый метод класса сначала вызывал какой-то другой метод?
У меня есть:
class Foo {
public:
void log() { }
void a() {
log();
}
void b() {
log();
}
};
Есть ли способ, которым я могу иметь каждый метод Foo
, вызывать log()
, но без необходимости явного ввода log() в качестве первой строки каждой функции?
Я хочу сделать это, чтобы я мог добавлять поведение к каждой функции без необходимости проходить через каждую функцию и убедиться, что вызов сделан, а также так, что когда я добавляю новые функции, код автоматически добавляется...
Возможно ли это? Я не могу представить, как это сделать с помощью макросов, поэтому не уверен, с чего начать... Единственный способ, о котором я думал до сих пор, - добавить "шаг предварительной сборки", чтобы перед компиляцией я просматривал файл и редактировать исходный код, но это не кажется очень умным....
EDIT: просто для пояснения - я не хочу, чтобы log() явно вызывал себя. Он не должен быть частью класса.
EDIT: Я бы предпочел использовать методы, которые будут работать на кросс-платформе, и используя только stl.
Ответы
Ответ 1
Благодаря необычным свойствам operator ->
мы можем вводить код перед любым доступом члена за счет слегка изогнутого синтаксиса:
// Nothing special in Foo
struct Foo {
void a() { }
void b() { }
void c() { }
};
struct LoggingFoo : private Foo {
void log() const { }
// Here comes the trick
Foo const *operator -> () const { log(); return this; }
Foo *operator -> () { log(); return this; }
};
Использование выглядит следующим образом:
LoggingFoo f;
f->a();
Смотрите на живую в Колиру
Ответ 2
Это минимальное (но довольно общее) решение проблемы обертки:
#include <iostream>
#include <memory>
template<typename T, typename C>
class CallProxy {
T* p;
C c{};
public:
CallProxy(T* p) : p{p} {}
T* operator->() { return p; }
};
template<typename T, typename C>
class Wrapper {
std::unique_ptr<T> p;
public:
template<typename... Args>
Wrapper(Args&&... args) : p{std::make_unique<T>(std::forward<Args>(args)...)} {}
CallProxy<T, C> operator->() { return CallProxy<T, C>{p.get()}; }
};
struct PrefixSuffix {
PrefixSuffix() { std::cout << "prefix\n"; }
~PrefixSuffix() { std::cout << "suffix\n"; }
};
struct MyClass {
void foo() { std::cout << "foo\n"; }
};
int main()
{
Wrapper<MyClass, PrefixSuffix> w;
w->foo();
}
Определение класса PrefixSuffix
с кодом префикса внутри его конструктора и кодом суффикса внутри деструктора - путь. Затем вы можете использовать класс Wrapper
(используя ->
для доступа к функциям-членам вашего исходного класса), и для каждого вызова будут выполняться код префикса и суффикса.
Смотрите live.
Кредиты на в этой статье, где я нашел решение.
В качестве примечания: если class
, которое должно быть завернуто, не имеет функций virtual
, можно было бы объявить переменную-член Wrapper::p
не как указатель, а как простой объект, а затем взломать бит на семантике оператора стрелок Wrapper
; в результате у вас не будет больше затрат на динамическое выделение памяти.
Ответ 3
Вы можете сделать обертку, что-то вроде
class Foo {
public:
void a() { /*...*/ }
void b() { /*...*/ }
};
class LogFoo
{
public:
template <typename ... Ts>
LogFoo(Ts&&... args) : foo(std::forward<Ts>(args)...) {}
const Foo* operator ->() const { log(); return &foo;}
Foo* operator ->() { log(); return &foo;}
private:
void log() const {/*...*/}
private:
Foo foo;
};
И затем используйте ->
вместо .
:
LogFoo foo{/* args...*/};
foo->a();
foo->b();
Ответ 4
Используйте лямбда-выражение и функцию более высокого порядка, чтобы избежать повторения и свести к минимуму вероятность забыть позвонить log
:
class Foo
{
private:
void log(const std::string&)
{
}
template <typename TF, typename... TArgs>
void log_and_do(TF&& f, TArgs&&... xs)
{
log(std::forward<TArgs>(xs)...);
std::forward<TF>(f)();
}
public:
void a()
{
log_and_do([this]
{
// `a` implementation...
}, "Foo::a");
}
void b()
{
log_and_do([this]
{
// `b` implementation...
}, "Foo::b");
}
};
Преимущество этого подхода заключается в том, что вы можете изменить log_and_do
вместо изменения каждой функции, вызывающей log
, если вы решите изменить поведение ведения журнала. Вы также можете передать любое количество дополнительных аргументов в log
. Наконец, он должен быть оптимизирован компилятором - он будет вести себя так, как если бы вы написали вызов log
вручную в каждом методе.
Вы можете использовать макрос (вздох), чтобы избежать некоторого шаблона:
#define LOG_METHOD(...) \
__VA_ARGS__ \
{ \
log_and_do([&]
#define LOG_METHOD_END(...) \
, __VA_ARGS__); \
}
Использование:
class Foo
{
private:
void log(const std::string&)
{
}
template <typename TF, typename... TArgs>
void log_and_do(TF&& f, TArgs&&... xs)
{
log(std::forward<TArgs>(xs)...);
std::forward<TF>(f)();
}
public:
LOG_METHOD(void a())
{
// `a` implementation...
}
LOG_METHOD_END("Foo::a");
LOG_METHOD(void b())
{
// `b` implementation...
}
LOG_METHOD_END("Foo::b");
};
Ответ 5
Я согласен с тем, что написано в комментариях ваших оригинальных сообщений, но если вам действительно нужно это сделать, и вам не нравится использовать макрос C, вы можете добавить метод для вызова своих методов.
Вот полный пример использования С++ 2011 для корректного изменения параметров функции. Протестировано с помощью GCC и clang
#include <iostream>
class Foo
{
void log() {}
public:
template <typename R, typename... TArgs>
R call(R (Foo::*f)(TArgs...), const TArgs... args) {
this->log();
return (this->*f)(args...);
}
void a() { std::cerr << "A!\n"; }
void b(int i) { std::cerr << "B:" << i << "\n"; }
int c(const char *c, int i ) { std::cerr << "C:" << c << '/' << i << "\n"; return 0; }
};
int main() {
Foo c;
c.call(&Foo::a);
c.call(&Foo::b, 1);
return c.call(&Foo::c, "Hello", 2);
}
Ответ 6
Можно ли избежать шаблона?
Нет.
С++ имеет очень ограниченные возможности генерации кода, автоматически вводящий код, не является частью их.
Отказ от ответственности: следующее заключается в глубоком погружении в прокси-сервер, с призывом не допустить, чтобы пользователь получал свои грязные лапы над функциями, которые они не должны вызывать без обхода прокси.
Можно ли забыть перевести функцию pre-/post-function сложнее?
Выполнение делегирования через прокси-сервер... раздражает. В частности, функции не могут быть public
или protected
, так как иначе вызывающий может получить свои грязные руки, и вы можете объявить неустойку.
Одно потенциальное решение состоит в том, чтобы объявить все функции частными и предоставить прокси-серверы, обеспечивающие ведение журнала. Сказанное это, чтобы сделать этот масштаб по нескольким классам, ужасно котельная, но это одноразовая стоимость:
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;
};
Примечание. Я не ожидаю добавления поддержки для volatile
, но никто не использует его, правильно?
Как только это первое препятствие прошло, вы можете использовать:
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};
Это довольно шаблонный, но по крайней мере шаблон ясен, и любое нарушение будет торчать как больной палец. Также проще применять пост-функции таким образом, а не отслеживать каждый return
.
Синтаксис для вызова также не так хорош:
MyClass c;
MyClass::a(c);
std::cout << MyClass::b(c, 2) << "\n";
Это должно быть возможно сделать лучше...
Обратите внимание, что в идеале вы хотели бы:
- использовать элемент данных
- тип которого кодирует смещение класса (безопасно)
- тип которого кодирует метод для вызова
На полпути есть решение (на полпути, потому что небезопасно...):
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();
}
};
Он добавляет по одному байту за "метод" (потому что С++ странно подобен этому) и требует некоторых довольно связанных определений:
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;
};
Но по крайней мере использование является "естественным":
int main() {
MyClass c;
c.a();
std::cout << c.b(2) << "\n";
return 0;
}
Лично для обеспечения этого я просто использовал бы:
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;
};
Не совсем экстраординарно, но просто изолировать состояние в MyClass::Impl
затрудняет реализацию логики в MyClass
, что обычно достаточно, чтобы гарантировать, что сопровождающие следуют шаблону.