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


Answers

Это минимальное (но довольно общее) решение проблемы обертки :

#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 (используя Wrapper -> для доступа к функциям-членам вашего исходного класса), а для каждого вызова будет выполняться код префикса и суффикса.

Смотрите это вживую .

Кредиты на эту статью , где я нашел решение.

В качестве примечания: если class который должен быть обернут, не имеет virtual функций, можно объявить переменную Wrapper::p member не как указатель, а как простой объект , а затем немного взломать семантику Wrapper оператор стрелки ; результат состоит в том, что у вас не будет больше затрат на распределение динамической памяти.

Question

У меня есть :

class Foo {
   public:
      void log() { }

      void a() {
         log();
      }

      void b() {
         log();
      }
};

Есть ли способ, которым я могу иметь каждый метод Foo , call log() , но без необходимости явного ввода log () в качестве первой строки каждой функции? Я хочу сделать это, чтобы я мог добавлять поведение к каждой функции без необходимости проходить через каждую функцию и убедиться, что вызов сделан, а также так, что, когда я добавляю новые функции, код автоматически добавляется ...

Возможно ли это? Я не могу представить, как это сделать с помощью макросов, поэтому не уверен, с чего начать ... Единственный способ, о котором я думал до сих пор, - добавить «шаг предварительной сборки», чтобы перед компиляцией я просматривал файл и редактировать исходный код, но это не кажется очень умным ....

EDIT: просто для пояснения - я не хочу, чтобы log () явно называл себя. Он не должен быть частью класса.

EDIT: Я бы предпочел использовать методы, которые будут работать на кросс-платформе и использовать только stl.




Используйте лямбда-выражение и функцию более высокого порядка, чтобы избежать повторения и свести к минимуму вероятность забыть 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");
};



Можно ли избежать шаблона?

Нет .

C ++ имеет очень ограниченные возможности генерации кода, автоматически вводящий код, не является частью их.

Отказ от ответственности: следующее заключается в глубоком погружении в прокси, с призывом не допустить, чтобы пользователь получал свои грязные лапы над функциями, которые они не должны вызывать, не обходя прокси.

Можно ли забыть перевести функцию pre- / post-function сложнее?

Принуждение делегирования через прокси-сервер ... раздражает. В частности, функции не могут быть public или protected , поскольку в противном случае вызывающий может получить свои грязные руки, и вы можете объявить неустойку.

Таким образом, одним из потенциальных решений является объявление всех функций private и предоставление прокси-серверов, обеспечивающих ведение журнала. Сказанное это, чтобы сделать этот масштаб по нескольким классам, ужасно котел-плита, хотя это одноразовая стоимость:

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();
    }
};

Он добавляет по одному байту за «метод» (потому что C ++ странно подобен этому) и требует некоторых довольно связанных определений:

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