如何确保类的每个方法都首先调用其他方法?


74

我有 :

class Foo {
   public:
      void log() { }

      void a() {
         log();
      }

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

有没有一种方法可以让我使用Foo调用的每个方法log(),而无需显式键入log()作为每个函数的第一行?我想这样做,这样我就可以向每个函数添加行为而不必遍历每个函数并确保进行了调用,并且还可以在添加新函数时自动添加代码...

这有可能吗?我无法想象如何使用宏执行此操作,因此不确定从哪里开始...到目前为止,我唯一想到的方法是添加“预构建步骤”,以便在编译之前扫描文件并编辑源代码,但这似乎并不明智。

编辑:只是为了澄清-我不希望log()明显地调用自己。它不需要成为课程的一部分。

编辑:我更喜欢使用跨平台工作的方法,并且仅使用stl。


1
可以使用宏来完成的,但它不是真正的东西,我建议(并因此将不会显示)。IMO的最佳方法是明确说明它,然后首先调用该函数。这将使将来的读者(包括您)更容易了解正在发生的事情。
一些程序员伙计

2
Google C ++方面编程。我没有用过,这不是建议,只有一点值得一读
Jacek Cz

1
添加显式调用比突然想知道为什么log在代码似乎没有实际调用它时为什么调用它要容易。隐藏这样的细节将使您的代码很难维持几年,即使是回到您自己的代码。
一些程序员伙计

6
您可以logAndCallFunc()使用一个参数指针创建函数,该参数指针指向您要在其后调用的函数log()
Yuriy Ivaskevych

3
在那种情况下,我认为您应该阅读XY问题
一些程序员伙计

Answers:


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确实做到了–取决于OP的实际需求,但问题是这样指定的:) /主题外:我喜欢您的黑魔法自动线程ECS框架,以及您提供的演示文稿:D
Quentin

6
@xDaizu aaah,JS和C ++之间的宗族战争,每个人都发现对方的语法令人恐惧:)
Quentin

11
@Quentin哈哈哈...它实际上来自waaaay,在我学习JS之前。这不是语法,我喜欢语法,它是低级操作和模糊覆盖。我仍然梦dream以求的夜晚,当我梦想着大学的第一年,它的指针,指向函数的指针,模板,运算符重载,带有12个2字母参数的函数(由老师提供),编译错误,堆栈溢出,违反内存的行为……等等,请问一下我要癫痫发作:)
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箭头操作者; 结果是您将不再有动态内存分配的开销。


这似乎也是一个很好的答案。我对C ++的了解不足,无法确定您的答案还是Quentins的答案更好... :)
Rahul Iyer

1
@John好,他们俩对我都很好。我认为昆汀给出了一个更具体(但要短得多)的答案。我的问题以更一般的方式解决,但结果更长
Paolo M

1
在Stroustrup的论文中:“我为类的C ++直接祖先C简要地采用了该思想的一种变体。在那里,可以定义一个函数,该函数将在每个成员函数(构造函数除外)的每次调用之前隐式调用,而另一个可以在每个成员函数(析构函数除外)的每次返回之前都隐式调用。提供这种前缀/后缀语义的函数被称为call()return()。[...]由于一些复杂的参数和返回类型的处理,该建议在一些实验性使用后就死了。 ,并且因为它具有侵入性”
Paolo M

4
@John嗯,我的情况并没有更好……它只是以更深层次的方式解决了这个问题……我的意思是:现在您遇到了对成员函数执行一些前缀代码的问题。明天,您可能会遇到执行子代码的问题。假设我向前看;)
Paolo M

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

使用lambda表达式高阶函数来避免重复,并最大程度地减少忘记调用的机会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()” ...
Rahul Iyer

@John:不幸的是,没有任何将代码“注入”到现有函数中的好方法。宏可能会有所帮助,更新我的答案...
Vittorio Romeo

2
但这和在每个方法的开头手动键入log()一样吗?因此,我们不能避免任何样板...挑战在于如何使用宏或其他技术“注入”代码(如您所说),以便避免“记住”添加log()调用每种方法,或者让局外人“记住”调用其他函数,并带有指向我们真正要调用的函数的指针……
Rahul Iyer

@John:关键是无法“注入”代码,甚至不能使用宏。您可能需要在方法定义期间使用一些样板文件,或者使用局外人call(...)函数。
维托里奥·罗密欧

9

我同意原始帖子的评论内容,但是如果您确实需要执行此操作并且不喜欢使用C宏,则可以添加一个方法来调用您的方法。

这是一个使用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” ...
Rahul Iyer

如果您知道如何使用C宏来执行此操作,我想知道-我不确定如何使用宏来执行此操作...
Rahul Iyer

@约翰:call()公开和a(), b()私人。这样,局外人只知道要调用的一个功能,即call()
sameerkn

您的语法有点混乱-a应该代替它val,并且MFP调用应该看起来像(this->*a)();
昆汀,

1
@sameerkn,如果局外人不知道a()和b(),那么他们将如何传递call()指向它们的指针...
Rahul Iyer

5

有可能避免样板吗?

没有。

C ++的代码生成能力非常有限,自动注入代码不属于其中。


免责声明:以下内容是代理的深层探讨,它的作用是防止用户在不绕过代理的情况下无法使用自己不希望使用的功能,而使用肮脏的爪子。

是否可以使忘记打电话给功能更强大的前/后功能?

通过代理强制委派是令人讨厌的。具体来说,这些函数可能不能publicprotected,否则调用者可以将其肮脏的手拿到它们上,而您可能会宣布放弃。

因此,一种潜在的解决方案是将所有函数声明为私有,并提供强制执行日志记录的代理。抽象化这一点,以使其在多个类中达到这种规模,尽管花费了一次性的成本,却令人难以置信地难以理解:

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,这通常足以确保维护人员遵循该模式。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.