Ich habe :
class Foo {
public:
void log() { }
void a() {
log();
}
void b() {
log();
}
};
Gibt es eine Möglichkeit, wie ich jede Methode Foo
aufrufen 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.
log
aufgerufen 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.logAndCallFunc()
mit einem Parameter erstellen - dem Zeiger auf die Funktion, nach der Sie aufrufen möchtenlog()
.Antworten:
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:
Sehen Sie es live auf Coliru
quelle
log()
es sich um eine Nullfunktion handelt, was mir unrealistisch erscheint. Ich würde erwartena()
, etwas anderes als zu protokollierenb()
.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
PrefixSuffix
Klasse mit dem Präfixcode im Konstruktor und dem Suffixcode im Destruktor ist der richtige Weg. Anschließend können Sie dieWrapper
Klasse 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 ,
class
die nicht über gewickelt werden muss ,virtual
Funktionen, könnte man das erklärenWrapper::p
variable Element nicht als Zeiger, sondern als ebenes Objekt , dann ein wenig Hacking auf dem semantischen vonWrapper
‚s Pfeil Operator ; Das Ergebnis ist, dass Sie nicht mehr den Overhead der dynamischen Speicherzuweisung haben würden.quelle
call()
undreturn()
. [...] Dieser Vorschlag starb - nach einiger experimenteller Verwendung - aufgrund der Komplexität der Behandlung von Argumenten und Rückgabetypen und weil es aufdringlich war "w->foo();
Anruf habenlogBefore()
, dannfoo()
,logAfter()
nacheinander, letzteres, was meine Lösung nicht tut. Sie weisen den Nachteil , dass, da es auf einem temporarie Lebenszeit angewiesen ist , die Aussagebar(w->foo());
nennenlogBefore()
,foo()
,bar()
dannlogAfter()
.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();
quelle
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_do
alle Funktionsaufrufe ändern können, anstatt sie zu ändern,log
wenn Sie das Protokollierungsverhalten ändern möchten. Sie können auch eine beliebige Anzahl zusätzlicher Argumente an übergebenlog
. Schließlich sollte es vom Compiler optimiert werden - es verhält sich so, als hätten Sielog
in 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"); };
quelle
call(...)
Funktion.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); }
quelle
call()
öffentlich unda(), b()
privat machen. Auf diese Weise kennen Außenstehende nur eine Funktion, die sie aufrufen können, nämlichcall()
.a
sollte anstelle von seinval
, und der MFP-Aufruf sollte so aussehen(this->*a)();
.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.
Das Erzwingen der Delegierung über einen Proxy ist ... ärgerlich. Insbesondere sind die Funktionen nicht möglich sein
public
oder werdenprotected
, 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 verfolgen
return
.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:
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::Impl
erschwert die Implementierung von LogikMyClass
, was im Allgemeinen ausreicht, um sicherzustellen, dass die Betreuer dem Muster folgen.quelle