【问题标题】:How to ensure that every method of a class calls some other method first?如何确保一个类的每个方法都先调用其他方法?
【发布时间】:2017-06-20 13:41:37
【问题描述】:

我有:

class Foo {
   public:
      void log() { }

      void a() {
         log();
      }

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

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

这甚至可能吗?我无法想象如何用宏来做到这一点,所以不知道从哪里开始......到目前为止我想到的唯一方法是添加一个“预构建步骤”,以便在编译之前扫描文件并编辑源代码,但这似乎不是很聪明....

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

编辑:我更喜欢使用可以跨平台工作的方法,并且只使用 stl.

【问题讨论】:

  • 可以用宏来完成,但这并不是我推荐的任何东西(因此不会显示)。 IMO 最好的方法是明确说明,然后首先调用该函数。这将使未来的读者(包括您)更容易理解正在发生的事情。
  • google C++ 方面编程。没用过,不推荐,只值得一读
  • 添加显式调用比突然想知道为什么在代码似乎没有实际调用log 时调用它更容易。隐藏这些细节会让你的代码很难在几年后维护,即使是你自己回来。
  • 您可以使用一个参数创建函数logAndCallFunc() - 指向要在log() 之后调用的函数的指针。
  • 在这种情况下,我认为你应该read about the XY problem

标签: c++ c++11


【解决方案1】:

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

See it live on Coliru

【讨论】:

  • @Quentin:这假设log() 是一个空函数,这对我来说似乎不切实际。我希望a() 记录与b() 不同的内容。
  • @VittorioRomeo 确实如此 - 取决于 OP 的实际需要,但问题 is 是这样指定的 :) / 题外话:我喜欢你的黑魔法自动线程 ECS 框架,以及你给出的介绍:D
  • @xDaizu 啊啊啊,JS和C++的祖宗之战,每个人都觉得对方的语法很可怕:)
  • @Quentin haha​​ha...它实际上来自我学习 JS 之前的 waaaay。这不是语法,我喜欢语法,it's the low level operations and obscure overriding。有些晚上我仍然尖叫着醒来,当我梦见我在大学的第一年及其指针、函数指针、模板、运算符重载、具有 12 个 2 字母参数的函数(由老师提供)、编译错误、堆栈溢出,内存违规和......对不起,我要癫痫发作:)
  • @MatthieuM。提防墨菲,而不是马基雅维利。
【解决方案2】:

这是包装器问题的最小(但非常通用)解决方案:

#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 类(使用-&gt; 访问您原来的类的成员函数),并且每次调用都会执行前缀和后缀代码。

live

感谢this paper,我在那里找到了解决方案。


附带说明:如果必须包装的class 没有virtual 函数,则可以将Wrapper::p 成员变量声明为不是指针,而是作为一个普通对象 em>,然后对 Wrapper箭头运算符 的语义稍作修改;结果是您将不再有动态内存分配的开销。

【讨论】:

  • @John 好吧,我觉得他们俩都不错。我认为昆汀给出了更具体(但远)的答案;我用更一般的方式解决了这个问题,但结果更长
  • 来自 Stroustrup 的论文:“我简要地采用了该想法的变体,用于 C++ 的直接祖先 C 与类。在那里,可以定义一个函数,该函数将在每个成员函数的每次调用之前被隐式调用(除了构造函数)和另一个将在每个成员函数(析构函数除外)每次返回之前被隐式调用的函数。提供这种前缀/后缀语义的函数被称为call()return()。[...]这个提议死了——经过一些实验性使用——因为处理参数和返回类型的复杂性,而且因为它是侵入性的”
  • @John 好吧,我的不是更好......它只是以 deeper 方式处理问题......我的意思是:现在你有执行一些前缀的问题代码到您的成员函数;明天,您可能会遇到执行下标代码的问题。假设我有点向前看;)
  • @John 你可以有一个单独的w-&gt;foo(); 调用logBefore(),然后是foo(),然后是logAfter(),我的解决方案不支持后者。请注意,由于它依赖于临时对象的生命周期,因此声明 bar(w-&gt;foo()); 将调用 logBefore()foo()bar() 然后 logAfter()
  • @John 示例名称 :) -- logBefore()logAfter() 是您在 PrefixSuffix 的构造函数和析构函数中放入的内容,将在 Foo 的每个函数之前和之后调用. bar 是一个任意函数,可以直观地预期它会在 logAfter 之后执行——使用临时函数会导致它的执行。
【解决方案3】:

你可以做一个包装器,比如

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

然后用-&gt;代替.

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

foo->a();
foo->b();

【讨论】:

    【解决方案4】:

    使用 lambda 表达式higher-order 函数来避免重复并尽量减少忘记调用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 的调用一样。


    您可以使用宏 (sigh) 来避免一些样板:

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

    【讨论】:

    • 但是你仍然必须在每个函数中写入“Log_and_do....”,这与首先调用 log() 一样多.....挑战在于将函数调用插入到每个函数中,而不必在每个函数的开头实际输入“log()”...
    • @John:不幸的是,没有任何好的方法可以将代码“注入”到现有函数中。宏可以提供帮助,更新我的答案...
    • 但这与在每个方法的开头手动键入 log() 一样,对吧?所以我们并没有避免任何样板......挑战是如何“注入”代码(如你所说),使用宏或其他一些技术,这样我们就不必“记住”添加 log() 调用到每个方法,或者让一个局外人“记住”用指向我们真正想要调用的函数的指针来调用其他函数......
    • @John:关键是没有办法“注入”代码,甚至没有使用宏。您要么在方法定义期间需要一些样板文件,要么使用外部call(...) 函数。
    【解决方案5】:

    我同意您原始帖子的 cmets 上写的内容,但如果您确实需要这样做并且您不喜欢使用 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);
    }
    

    【讨论】:

    • 这样的问题是,外部调用 foo 的公共方法,必须知道调用“call”,而不是直接调用“a”或“b”......
    • 如果您知道如何使用 C 宏执行此操作,我想知道 - 我不确定如何使用宏执行此操作...
    • @John:将 call() 设为公开,a(), b() 设为私有。这样,外人只知道要调用的一个函数,即call()
    • 您的语法有点错误 -- a 应该代替 val,而 MFP 调用应该类似于 (this-&gt;*a)();
    • @sameerkn 如果外人不知道 a() 和 b(),那么他们将如何将 call() 指针传递给他们......
    【解决方案6】:

    是否可以避免样板文件?

    没有。

    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 中的逻辑难以实现,这通常足以确保维护者遵循该模式。

    【讨论】:

      猜你喜欢
      • 2012-05-01
      • 1970-01-01
      • 1970-01-01
      • 2017-02-27
      • 1970-01-01
      • 2013-12-26
      • 2011-09-03
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多