Як забезпечити, щоб кожен метод класу спочатку викликав якийсь інший метод?


74

Я маю :

class Foo {
   public:
      void log() { }

      void a() {
         log();
      }

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

Чи є спосіб, яким я можу користуватися для кожного методу Foo, дзвінкаlog() , але без необхідності явного введення log () як першого рядка кожної функції? Я хочу зробити це, щоб я міг додати поведінку кожній функції, не переглядаючи кожну функцію та переконуючись, що дзвінок зроблений, а також, щоб при додаванні нових функцій код автоматично додавався ...

Чи можливо це взагалі? Я не уявляю, як це зробити за допомогою макросів, тому не знаю, з чого почати ... Єдиний спосіб, про який я до цього думав, - це додати "крок попередньої збірки", щоб перед компіляцією відсканувати файл і редагувати вихідний код, але це здається не дуже розумним ....

EDIT: Просто для уточнення - я не хочу, щоб log () називав себе очевидно. Не обов’язково бути частиною класу.

EDIT: Я б віддав перевагу використанню методів, які працювали б на різних платформах, і використанню лише stl.


1
Це можна зробити за допомогою макросів, але насправді це не те, що я рекомендую (і тому не відображатиметься). Найкращий спосіб IMO - це чітко про це сказати і просто викликати функцію першим ділом. Це полегшить майбутнім читачам (включаючи вас) зрозуміти, що відбувається.
Якийсь програміст, чувак,

2
програмування аспектів google C ++ - -. Я не використовував, це не рекомендація, єдиний пункт, який варто прочитати
Jacek Cz

1
Додавання явного виклику буде простішим, ніж раптом задатися питанням, чому logвикликається, коли код, здається, насправді його не викликає. Приховуючи такі деталі, ваш код буде дуже важко підтримувати пару років, навіть якщо до нього повертаєтесь самі.
Якийсь програміст, чувак,

6
Ви можете створити функцію logAndCallFunc()з одним параметром - покажчиком на функцію, яку потрібно викликати після log().
Юрій Іваскевич

3
У такому випадку, я думаю, вам слід прочитати про проблему XY .
Якийсь програміст, чувак,

Відповіді:


112

Завдяки незвичайним властивостям 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();

Перегляньте це у прямому ефірі на Coliru


7
@Quentin: це робить припущення, що log()є нульовою функцією, що мені здається нереальним. Я сподівався б a()записати щось інше, ніж b().
Вітторіо Ромео,

4
@VittorioRomeo це робить - залежить від фактичної потреби ФПА, але питання буде вказано як то :) / Не по темі: Я любив свою чорну магію autothreading рамки ECS і презентацію , яку ви дали його: D
Квентін

6
@xDaizu aaah, війни родових кланів між JS та C ++, кожен з яких виявляє жахливий синтаксис іншого
Квентін,

11
@Quentin ха-ха-ха ... це насправді походить від waaaay ще до того, як я дізнався JS. Це не синтаксис, мені подобається синтаксис, це низькорівневі операції та неясне перевизначення . Я все ще прокидаюся з криками кілька ночей, коли мрію про свій перший курс в університеті та його покажчики, вказівники на функції, шаблони, перевантаження оператора, функції з 12 двобуквеними параметрами (такі як викладач), помилки компіляції, стеки переповнення, порушення пам’яті та ... вибачте, мить, у мене буде судом :)
xDaizu

9
@MatthieuM. охороняти Мерфі, а не Макіавеллі.
Квентін

37

Це мінімальне (але досить загальне) рішення проблеми обгортки :

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

Перегляньте це в прямому ефірі .

Подяки цій роботі , де я знайшов рішення.


В якості побічного примітка: якщо classщо повинен бути обгорнутий не має virtualфункції, можна оголосити Wrapper::pзмінну - член не як покажчик, а як простий об'єкт , а потім злому трохи на семантику Wrapper«s стрілка оператора ; результатом є те, що у вас більше не буде накладних витрат на динамічне виділення пам'яті.


Це теж здається гарною відповіддю. Я не знаю достатньо про C ++, щоб зрозуміти, чи ваша відповідь чи відповідь Квентіна є кращою ... :)
Рахул Айєр

1
@ Джон Ну, вони обидва мені добре виглядають. Гадаю, Квентін дав більш конкретну (але набагато коротшу ) відповідь; mine розглядає проблему більш загальним чином , проте, тим часом, довше .
Паоло М

1
Із статті Страуструпа: "Я коротко прийняв варіант цієї ідеї для прямого предка C ++ з класами. Там можна було б визначити функцію, яка б неявно викликалася перед кожним викликом кожної функції-члена (крім конструктора), а іншу, яка була б неявно викликається перед кожним поверненням від кожної функції-члена (крім деструктора). Функції, що забезпечують цю семантику префікса / суфікса, були викликані call()і return(). [...] Ця пропозиція загинула - після деякого експериментального використання - через складність обробки аргументів і типів повернення і тому, що це було настирливим "
Паоло М

4
@John Ну, моє, це не краще ... Це просто вирішує проблему глибше ... Я маю на увазі: тепер у вас проблема виконання деякого коду префікса для ваших функцій-членів; завтра у вас може виникнути проблема з виконанням коду субфікса. Скажімо, я дивлюсь трохи вперед;)
Паоло М

2
@John Ви можете зробити один w->foo();дзвінок logBefore(), потім foo(), потім logAfter()послідовно, останній з яких моє рішення не робить. Зверніть увагу на застереження , що, оскільки він спирається на цілого життя temporarie, в заяві bar(w->foo());буде дзвонити logBefore(), foo(), bar() то logAfter() .
Квентін

17

Ви можете зробити обгортку, щось на зразок

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

9

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

3
Але тоді вам все одно потрібно написати "Log_and_do ...." для кожної функції, що є стільки ж роботою, скільки спочатку викликом log () ... Завдання полягає в тому, щоб вставити виклик функції у кожну функцію без маючи фактично набирати "log ()" на початку кожної функції ...
Рахул Айєр

@John: на жаль, немає жодного хорошого способу "вливання" коду в існуючі функції. Макрос міг би допомогти, оновивши мою відповідь ...
Вітторіо Ромео

2
Але це працює так само, як ручне введення log () на початку кожного методу, чи не так? Отже, ми не уникаємо будь-якого типового шаблону ... проблема полягає в тому, як «вводити» код (як ви висловлюєтесь), використовуючи макроси або будь-який інший прийом, щоб уникнути необхідності «пам’ятати», щоб додати виклик log () до кожного методу, або попросіть сторонніх "запам'ятати" викликати якусь іншу функцію за допомогою вказівника на функцію, яку ми дійсно хочемо викликати ...
Рахул Айєр,

@John: справа в тому, що не існує способу "вливання" коду, навіть використання макросів. Вам або знадобиться якийсь шаблон під час визначення методу, або скористайтеся call(...)функцією сторонніх .
Вітторіо Ромео,

9

Я погоджуюся з тим, що написано в коментарях до ваших вихідних публікацій, але якщо вам дійсно потрібно це зробити, і вам не подобається використовувати макрос С, ви можете додати метод для виклику своїх методів.

Ось повний приклад використання 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);
}

1
Проблема в цьому полягає в тому, що сторонні, які називають загальнодоступні методи foo, повинні знати, щоб зателефонувати "call", а не "a" або "b" безпосередньо ...
Рахул Айєр,

Якщо ви знаєте, як це зробити з макрокомандою C, я хотів би знати - я не впевнений, як я це зробив би з макросом ...
Rahul Iyer

@ John: зробити call()публічним та a(), b()приватним. Таким чином сторонні люди знають лише одну функцію для виклику, тобто call().
sameerkn

Ваш синтаксис трохи порушений - a повинен бути замість val, а дзвінок МФУ повинен виглядати так (this->*a)();.
Квентін

1
@sameerkn, якщо сторонні люди не знають про a () та b (), то як вони передаватимуть на них покажчик call () ....
Рахул Айєр

5

Чи можна уникнути шаблону?

Ні.

C ++ має дуже обмежені можливості генерації коду, автоматичне введення коду не є частиною їх.


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

Чи можна ускладнити забуття про дзвінок до / після функції?

Примусове делегування через проксі-сервер ... дратує. Зокрема, функції не можуть бути 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();
    }
};

Він додає по одному байту на "метод" (оскільки 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, що, як правило, достатньо для того, щоб супровідники слідували зразку.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.