Wie kann sichergestellt werden, dass jede Methode einer Klasse zuerst eine andere Methode aufruft?

74

Ich habe :

class Foo {
   public:
      void log() { }

      void a() {
         log();
      }

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

Gibt es eine Möglichkeit, wie ich jede Methode Fooaufrufen log()kann, ohne dass ich log () explizit als erste Zeile jeder Funktion eingeben muss? Ich möchte dies tun, damit ich jeder Funktion Verhalten hinzufügen kann, ohne jede Funktion durchlaufen und sicherstellen zu müssen, dass der Aufruf erfolgt, und damit beim Hinzufügen neuer Funktionen der Code automatisch hinzugefügt wird ...

Ist das überhaupt möglich? Ich kann mir nicht vorstellen, wie man das mit Makros macht, also nicht sicher, wo ich anfangen soll ... Der einzige Weg, an den ich bisher gedacht habe, ist das Hinzufügen eines "Pre-Build-Schritts", so dass ich vor dem Kompilieren die Datei scanne und bearbeiten Sie den Quellcode, aber das scheint nicht sehr intelligent ...

EDIT: Nur zur Klarstellung - ich möchte nicht, dass sich log () offensichtlich selbst aufruft. Es muss nicht Teil der Klasse sein.

EDIT: Ich würde es vorziehen, Methoden zu verwenden, die plattformübergreifend funktionieren, und nur die stl zu verwenden.

Rahul Iyer
quelle
1
Es könnte mit Makros gemacht werden, aber es ist nicht wirklich etwas, was ich empfehle (und daher nicht zeigen wird). Der beste Weg, IMO, besteht darin, explizit darüber zu sprechen und die Funktion als Erstes aufzurufen. Dies erleichtert zukünftigen Lesern (einschließlich Ihnen) das Verständnis der Vorgänge.
Einige Programmierer Typ
2
Google C ++ Aspektprogrammierung. Ich habe nicht verwendet, dies ist keine Empfehlung, nur Punkt lesenswert
Jacek Cz
1
Das Hinzufügen des expliziten Aufrufs ist einfacher, als sich plötzlich zu fragen, warum logaufgerufen wird, wenn der Code ihn nicht tatsächlich aufzurufen scheint. Das Ausblenden solcher Details macht es sehr schwierig, Ihren Code ein paar Jahre später beizubehalten, selbst wenn Sie selbst darauf zurückkommen.
Einige Programmierer Typ
6
Sie können eine Funktion logAndCallFunc()mit einem Parameter erstellen - dem Zeiger auf die Funktion, nach der Sie aufrufen möchten log().
Yuriy Ivaskevych
3
In diesem Fall sollten Sie über das XY-Problem lesen .
Einige Programmierer Typ

Antworten:

112

Dank der ungewöhnlichen Eigenschaften von operator ->können wir Code vor jedem Mitgliederzugriff auf Kosten einer leicht verbogenen Syntax einfügen :

// 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; }
};

Die Verwendung sieht wie folgt aus:

LoggingFoo f;
f->a();

Sehen Sie es live auf Coliru

QUentin
quelle
7
@ Quentin: Dies macht die Annahme, dass log()es sich um eine Nullfunktion handelt, was mir unrealistisch erscheint. Ich würde erwarten a(), etwas anderes als zu protokollieren b().
Vittorio Romeo
4
@VittorioRomeo tut es - hängt vom tatsächlichen Bedarf des OP ab, aber die Frage wird so spezifiziert :) / Off-Topic: Ich mochte Ihr schwarz-magisches Autothreading-ECS-Framework und die Präsentation, die Sie davon gegeben haben: D
Quentin
6
@xDaizu aaah, der Ahnen-Clan kämpft zwischen JS und C ++, wobei jeder die Syntax des anderen fürchterlich findet :)
Quentin
11
@ Quentin hahaha ... es kommt tatsächlich von waaaay zurück, bevor ich JS gelernt habe. Es ist nicht die Syntax, ich mag die Syntax, es sind die Operationen auf niedriger Ebene und das obskure Überschreiben . Einige Nächte wache ich immer noch schreiend auf, wenn ich von meinem ersten Studienjahr und seinen Zeigern, Zeigern auf Funktionen, Vorlagen, Überladungen von Operatoren, Funktionen mit 12 2-Buchstaben-Parametern (vom Lehrer so bereitgestellt), Kompilierungsfehlern und Stapeln träume Überläufe, Gedächtnisverletzungen und ... entschuldigen Sie einen Moment, ich werde einen Anfall bekommen :)
xDaizu
9
@MatthieuM. Wache gegen Murphy, nicht gegen Machiavelli.
Quentin
37

Dies ist eine minimale (aber ziemlich allgemeine) Lösung für das Wrapper-Problem :

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

Das Definieren einer PrefixSuffixKlasse mit dem Präfixcode im Konstruktor und dem Suffixcode im Destruktor ist der richtige Weg. Anschließend können Sie die WrapperKlasse verwenden (mit der ->, um auf die Mitgliedsfunktionen Ihrer ursprünglichen Klasse zuzugreifen). Bei jedem Aufruf werden Präfix- und Suffixcode ausgeführt.

Sehen Sie es live .

Dank an dieses Papier , in dem ich die Lösung gefunden habe.


Als Randbemerkung: Wenn die , classdie nicht über gewickelt werden muss , virtualFunktionen, könnte man das erklären Wrapper::pvariable Element nicht als Zeiger, sondern als ebenes Objekt , dann ein wenig Hacking auf dem semantischen von Wrapper‚s Pfeil Operator ; Das Ergebnis ist, dass Sie nicht mehr den Overhead der dynamischen Speicherzuweisung haben würden.

Paolo M.
quelle
Dies scheint auch eine gute Antwort zu sein. Ich weiß nicht genug über C ++, um herauszufinden, ob Ihre oder Quentins Antwort besser ist ... :)
Rahul Iyer
1
@ John Nun, beide sehen für mich gut aus. Ich denke, Quentin gab eine spezifischere (aber weitaus kürzere ) Antwort. meins spricht das Problem allgemeiner an, was jedoch länger dauert.
Paolo M
1
Aus Stroustrups Artikel: "Ich habe kurz eine Variante dieser Idee für C ++ 's direkten Vorfahren C mit Klassen übernommen. Dort könnte man eine Funktion definieren, die implizit vor jedem Aufruf jeder Mitgliedsfunktion (außer dem Konstruktor) aufgerufen wird, und eine andere implizit vor jeder Rückgabe von jeder Mitgliedsfunktion (mit Ausnahme des Destruktors) aufgerufen. Die Funktionen, die diese Präfix- / Suffix-Semantik bereitstellen, wurden aufgerufen call()und return(). [...] Dieser Vorschlag starb - nach einiger experimenteller Verwendung - aufgrund der Komplexität der Behandlung von Argumenten und Rückgabetypen und weil es aufdringlich war "
Paolo M
4
@ John Nun, meins, es ist nicht besser ... Es behandelt das Problem nur tiefer ... Ich meine: Jetzt haben Sie das Problem, Präfixcode für Ihre Mitgliedsfunktionen auszuführen. Morgen haben Sie möglicherweise das Problem, Subfix-Code auszuführen. Nehmen wir an, ich freue mich ein bisschen nach vorne;)
Paolo M
2
@ John Du kannst einen einzelnen w->foo();Anruf haben logBefore(), dann foo(), logAfter()nacheinander, letzteres, was meine Lösung nicht tut. Sie weisen den Nachteil , dass, da es auf einem temporarie Lebenszeit angewiesen ist , die Aussage bar(w->foo());nennen logBefore(), foo(), bar() dann logAfter() .
Quentin
17

Sie können einen Wrapper machen, so etwas wie

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

Und dann ->anstelle von .:

LogFoo foo{/* args...*/};

foo->a();
foo->b();
Jarod42
quelle
9

Verwenden Sie einen Lambda-Ausdruck und eine Funktion höherer Ordnung , um Wiederholungen zu vermeiden und die Wahrscheinlichkeit zu minimieren, dass Sie vergessen, Folgendes aufzurufen 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");
    }
};

Der Vorteil dieses Ansatzes besteht darin, dass Sie log_and_doalle Funktionsaufrufe ändern können, anstatt sie zu ändern, logwenn Sie das Protokollierungsverhalten ändern möchten. Sie können auch eine beliebige Anzahl zusätzlicher Argumente an übergeben log. Schließlich sollte es vom Compiler optimiert werden - es verhält sich so, als hätten Sie login jeder Methode manuell einen Aufruf an geschrieben .


Sie können ein Makro (Seufzen) verwenden , um ein Boilerplate zu vermeiden:

#define LOG_METHOD(...) \
    __VA_ARGS__ \
    { \
        log_and_do([&]

#define LOG_METHOD_END(...) \
        , __VA_ARGS__); \
    }

Verwendung:

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");
};
Vittorio Romeo
quelle
3
Aber dann müssen Sie immer noch "Log_and_do ...." in jede Funktion schreiben, was genauso viel Arbeit ist wie das Aufrufen von log () an erster Stelle ..... Die Herausforderung besteht darin, den Funktionsaufruf in jede Funktion ohne einzufügen zu Beginn jeder Funktion "log ()"
Rahul Iyer
@ John: Leider gibt es keine gute Möglichkeit, Code in vorhandene Funktionen zu "injizieren". Ein Makro könnte helfen, meine Antwort zu aktualisieren ...
Vittorio Romeo
2
Aber das funktioniert genauso wie die manuelle Eingabe von log () zu Beginn jeder Methode, oder? Wir vermeiden also keine Boilerplate ... Die Herausforderung besteht darin, Code (wie Sie sagen) mithilfe von Makros oder einer anderen Technik zu "injizieren", damit wir uns nicht "merken" müssen, um den log () -Aufruf hinzuzufügen zu jeder Methode, oder lassen Sie einen Außenstehenden "daran denken", eine andere Funktion mit einem Zeiger auf die Funktion aufzurufen, die wir wirklich aufrufen möchten ...
Rahul Iyer
@ John: Der Punkt ist, dass es keine Möglichkeit gibt, Code zu "injizieren", auch nicht mit Makros. Entweder benötigen Sie während der Methodendefinition ein Boilerplate oder Sie verwenden eine Outsider- call(...)Funktion.
Vittorio Romeo
9

Ich stimme zu, was in den Kommentaren Ihrer ursprünglichen Beiträge steht, aber wenn Sie dies wirklich tun müssen und kein C-Makro verwenden möchten, können Sie eine Methode zum Aufrufen Ihrer Methoden hinzufügen.

Hier ist ein vollständiges Beispiel mit C ++ 2011, um korrekt variierende Funktionsparameter zu verarbeiten. Getestet mit GCC und 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);
}
gabry
quelle
1
Das Problem dabei ist, dass Außenstehende, die öffentliche Methoden von foo aufrufen, wissen müssen, dass sie "call" anstelle von "a" oder "b" direkt aufrufen müssen ...
Rahul Iyer
Wenn Sie wissen, wie man das mit einem C-Makro macht, würde ich gerne wissen - ich bin nicht sicher, wie ich das mit einem Makro machen würde ...
Rahul Iyer
@ John: call()öffentlich und a(), b()privat machen. Auf diese Weise kennen Außenstehende nur eine Funktion, die sie aufrufen können, nämlich call().
Sameerkn
Ihre Syntax ist etwas kaputt - asollte anstelle von sein val, und der MFP-Aufruf sollte so aussehen (this->*a)();.
Quentin
1
@sameerkn Wenn Außenstehende nicht über a () und b () wissen, wie werden sie dann call () einen Zeiger auf sie weitergeben ....
Rahul Iyer
5

Ist es möglich, die Kesselplatte zu vermeiden?

Nein.

C ++ verfügt nur über sehr eingeschränkte Möglichkeiten zur Codegenerierung. Das automatische Einfügen von Code gehört nicht dazu.


Haftungsausschluss: Das Folgende ist ein tiefer Einblick in das Proxying mit dem Aufruf, zu verhindern, dass der Benutzer seine schmutzigen Pfoten auf die Funktionen bekommt, die er nicht aufrufen sollte, ohne den Proxy zu umgehen.

Ist es möglich, das Vergessen, Pre- / Post-Funktion aufzurufen, schwieriger zu machen?

Das Erzwingen der Delegierung über einen Proxy ist ... ärgerlich. Insbesondere sind die Funktionen nicht möglich sein publicoder werden protected, da sonst der Anrufer seine schmutzigen Hände auf sie bekommen können , und Sie können verwirkt erklären.

Eine mögliche Lösung besteht daher darin, alle Funktionen als privat zu deklarieren und Proxys bereitzustellen, die die Protokollierung erzwingen. Abstrahiert, um diese Skala über mehrere Klassen hinweg zu erstellen, ist schrecklich kesselartig, obwohl es sich um einmalige Kosten handelt:

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

Hinweis: Ich freue mich nicht darauf, Unterstützung für hinzuzufügen volatile, aber niemand nutzt sie, oder?

Sobald diese erste Hürde überwunden ist, können Sie Folgendes verwenden:

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

Es ist ziemlich das Kesselschild, aber zumindest ist das Muster klar, und jede Verletzung wird wie ein schmerzender Daumen herausragen. Es ist auch einfacher, Post-Funktionen auf diese Weise anzuwenden, als jede einzelne zu verfolgenreturn .

Die aufzurufende Syntax ist auch nicht gerade so gut:

MyClass c;
MyClass::a(c);
std::cout << MyClass::b(c, 2) << "\n";

Es sollte möglich sein, es besser zu machen ...


Beachten Sie, dass Sie im Idealfall:

  • Verwenden Sie ein Datenelement
  • deren Typ den Offset zur Klasse codiert (sicher)
  • deren Typ die aufzurufende Methode codiert

Eine Lösung auf halbem Weg ist (auf halbem Weg, weil unsicher ...):

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

Es fügt ein Byte pro "Methode" hinzu (weil C ++ so seltsam ist) und erfordert einige ziemlich komplizierte Definitionen:

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

Aber zumindest ist die Verwendung "natürlich":

int main() {
    MyClass c;
    c.a();
    std::cout << c.b(2) << "\n";
    return 0;
}

Um dies persönlich durchzusetzen, würde ich einfach Folgendes verwenden:

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

Nicht gerade außergewöhnlich, aber das einfache Isolieren des Zustands in MyClass::Implerschwert die Implementierung von Logik MyClass, was im Allgemeinen ausreicht, um sicherzustellen, dass die Betreuer dem Muster folgen.

Matthieu M.
quelle