Как обеспечить, чтобы каждый метод класса сначала вызывал какой-то другой метод?

У меня есть:

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, что обычно достаточно, чтобы гарантировать, что сопровождающие следуют шаблону.