如何自我记录模板库class调用的回调函数?

How to self-document a callback function that is called by template library class?

我有一个函数 User::func()(回调),模板 class (Library<T>) 会调用它。

在开发的第一次迭代中,每个人都知道 func() 仅用于该单一目的。
几个月后,大多数成员忘记了 func() 的用途。
经过一些重大重构后,func() 有时会被某些编码人员删除。

起初,我根本不认为这是个问题。
但是,在我多次遇到这种模式之后,我觉得我需要一些对策。

问题

如何优雅地记录下来? (可爱&&简洁&&无额外CPU成本)

例子

这是一个简化的代码:-
(现实世界的问题是分散在 10 多个库文件和 20 多个用户文件和 40 多个函数周围。)

Library.h

template<class T> class Library{
    public: T* node=nullptr;
    public: void utility(){
        node->func();  //#1
    }
};

User.h

class User{
    public: void func(){/** some code*/} //#1
    //... a lot of other functions  ...
    // some of them are also callback of other libraries
};

main.cpp

int main(){
    Library<User> li; .... ;  li.utility();
}

我糟糕的解决方案

1。评论/文档

作为第一个解决方法,我倾向于添加这样的评论:-

class User{ 
    /** This function is for "Library" callback */
    public: void func(){/** some code*/}
};

但它变脏的速度非常快 - 我必须将它添加到每个 class 中的每个 "func"。

2。重命名 "func()"

在实际情况下,我倾向于像这样为函数名添加前缀:-

class User{ 
    public: void LIBRARY_func(){/** some code*/}
};

很明显,但是函数名现在很长。
(特别是当 Library-class 有更长的 class 名称时)

3。虚拟 class 与 "func()=0"

我正在考虑创建一个抽象 class 作为回调的接口。

class LibraryCallback{ 
    public: virtual void func()=0;
};
class User : public LibraryCallback{ 
    public: virtual void func(){/** some code*/}
};

给人的感觉是func()是为了一些非常外在的东西:)
但是,我必须牺牲虚拟呼叫成本 (v-table).
在性能关键的情况下,我买不起。

4。静态函数

(Daniel Jour 的想法在评论中,谢谢!)

差不多 1 ​​个月后,我是这样使用的:-

Library.h

template<class T> class Library{
    public: T* node=nullptr;
    public: void utility(){
        T::func(node);  //#1
    }
};

User.h

class User{
    public: static void func(Callback*){/** some code*/} 
};

main.cpp

int main(){
    Library<User> li;
}

它可能更干净,但仍然缺少自文档。

摘要class是最好的强制功能不被删除的方法。所以我建议用纯虚函数实现基class,这样派生就必须定义函数。 或者第二种解决方案是使用函数指针,这样可以通过避免 V-table 创建和调用的额外开销来节省性能。

func 不是 User 的特征。这是 User-Library<T> 耦合的一个特征。

如果它在 Library<T> 使用之外没有明确的语义,则将它放在 User 中是一个坏主意。如果它确实有明确的语义,它应该说明它的作用,删除它应该是一个明显的坏主意。

将它放在 Library<T> 中是行不通的,因为它的行为是 Library<T>T 的函数。

答案是两个地方都不放。

template<class T> struct tag_t{ using type=T; constexpr tag_t(){} };
template<class T> constexpr tag_t<T> tag{};

现在 Library.h:

struct ForLibrary;
template<class T> class Library{
  public: T* node=nullptr;
  public: void utility(){
    func( tag<ForLibrary>, node ); // #1
  }
};

User.h中:

struct ForLibrary;
class User{ 
/** This function is for "Library" callback */
public:
  friend void func( tag_t<ForLibrary>, User* self ) {
    // code
  }
};

或者只是将其放入与 User 相同的名称空间中 ForLibrary:

friend func( tag_t<ForLibrary>, User* self );

在删除 func 之前,您将找到 ForLibrary

它不再是 User 的 "public interface" 的一部分,所以不要弄乱它。它可以是朋友(帮助者),也可以是 UserLibrary.

相同命名空间中的自由函数

您可以在需要 Library<User> 的地方实现它,而不是在 User.hLibrary.h 中实现它,特别是如果它只使用 User 的 public 接口.

这里使用的技术是 "tag dispatching"、"argument dependent lookup"、"friend functions" 并且更喜欢自由函数而不是方法。

从用户端来说,我会用crtp创建一个回调接口,强制用户使用它。例如:

template <typename T>
struct ICallbacks
{
    void foo() 
    {
        static_cast<T*>(this)->foo();
    }
};

用户应继承此接口并实现foo()回调

struct User : public ICallbacks<User>
{
    void foo() {std::cout << "User call back" << std::endl;}
};

它的好处是,如果 Library 使用 ICallback 接口并且 User 忘记实现 foo(),您将得到一个很好的编译器错误消息。

注意没有虚函数,所以这里没有性能损失。

在库方面,我只会通过其接口调用那些回调(在本例中为 ICallback)。按照 OP 使用指针,我会做这样的事情:

template <typename T>
struct Library
{
    ICallbacks<T> *node = 0;

    void utility()
    {
       assert(node != nullptr);
       node->foo();
    }
};

请注意,事情会以这种方式自动记录下来。很明显,你正在使用回调接口,node 是具有这些功能的对象。

下面是一个完整的工作示例:

#include <iostream>
#include <cassert>

template <typename T>
struct ICallbacks
{
    void foo() 
   {
       static_cast<T*>(this)->foo();
   }
};

struct User : public ICallbacks<User>
{
     void foo() {std::cout << "User call back" << std::endl;}
};

template <typename T>
struct Library
{
    ICallbacks<T> *node = 0;

    void utility()
    {
        assert(node != nullptr);
        node->foo();
    }
};

int main()
{
    User user;

    Library<User> l;
    l.node = &user;
    l.utility();
}

Test.h

#ifndef TEST_H
#define TEST_H

// User Class Prototype Declarations
class User;

// Templated Wrapper Class To Contain Callback Functions
// User Will Inherit From This Using Their Own Class As This
// Class's Template Parameter
template <class T>
class Wrapper {
public:
    // Function Template For Callback Methods. 
    template<class U>
    auto Callback(...) {};
};

// Templated Library Class Defaulted To User With The Utility Function
// That Provides The Invoking Of The Call Back Method
template<class T = User>
class Library {
public:
    T* node = nullptr;
    void utility() {
        T::Callback(node);
    }
};

// User Class Inherited From Wrapper Class Using Itself As Wrapper's Template Parameter.
// Call Back Method In User Is A Static Method And Takes A class Wrapper* Declaration As 
// Its Parameter
class User : public Wrapper<User> {
public:
    static void Callback( class Wrapper* ) { std::cout << "Callback was called.\n";  }
};

#endif // TEST_H

main.cpp

#include "Test.h"

int main() {

    Library<User> l;
    l.utility();

    return 0;
}

输出

Callback was called.

我能够在 Windows 7 - 64 位 Intel Core 2 Quad Extreme 上的 VS2017 CE 中编译、构建和 运行 这没有错误。

Any Thoughts?

我建议适当地命名包装器 class,然后对于每个具有独特用途的特定回调函数,在包装器中相应地命名它们 class.



编辑

玩过这个“模板魔术”之后,就没有这样的东西了...... Wrapperclass中的函数模板我已经注释掉了,发现不需要了。然后我注释掉 class Wrapper*,它是 UserCallback() 的参数列表。这给了我一个编译器错误,指出 User::Callback() 不接受 0 参数。所以我回头看了看 Wrapper 因为 User 继承自它。那么此时 Wrapper 是一个空的 class 模板。

这让我看 Library。库有一个指向 User 作为 public 成员的指针和一个调用 User's static Callback 方法的 utility() 函数。正是在这里,调用方法将指向 User 对象的指针作为其参数。所以它引导我尝试这个:

class User; // Prototype
class A{}; // Empty Class

template<class T = User>
class Library {
public: 
    T* node = nullptr;
    void utility() {
        T::Callback(node);
    }
};

class User : public A {
public:
    static void Callback( A* ) { std::cout << "Callback was called.\n"; }
};

并且这可以作为简化版本正确编译和构建。然而;当我想到它时;模板版本更好,因为它是在编译时推导出来的,而不是 运行 时间。因此,当我们回到使用模板时,javaLover 问我 class Wrapper* 是什么意思,或者在 User class.[=52= 中的 Callback 方法的参数列表中]

我会尽可能清楚地解释这一点,但首先包装器 Class 只是一个空模板 shell,User 将继承它,它除了作为基础 class 现在看起来像这样:

template<class T>
class Wrapper {   // Could Be Changed To A More Suitable Name Such As Shell or BaseShell
};

当我们查看 User class:

class User : public Wrapper<User> {
public:
    static void Callback( class Wrapper* ) { // print statement }
};

我们看到 User 是一个非模板 class,它继承自模板 class 但将其自身用作模板的参数。它包含一个 public 静态方法 这个方法没有 return 任何东西,但它确实需要一个参数;这在 Library class 中也很明显,它的模板参数是 User class。当 Library's utility() 方法调用 User's Callback() 方法时,库期望的参数是指向 User 对象的指针。因此,当我们回到 User class 而不是直接在其声明中将其声明为 User* 指针时,我使用的是它继承自的空 class 模板。但是,如果您尝试这样做:

class User : public Wrapper<User> {
public:
    static void Callback( Wrapper* ) { // print statement }
};

您应该收到一条消息,指出 Wrapper* 缺少它的参数列表。我们可以在这里做 Wrapper<User>* 但这是多余的,因为我们已经看到 User 是从 Wrapper 继承的。所以我们可以通过在 Wrapper* 前面加上 class 关键字来解决这个问题并使其更清晰,因为它是一个 class 模板。因此,模板魔法...好吧,这里没有魔法...只有编译器内在和优化。

虽然我知道我不会回答您的具体问题(如何记录不可删除的函数),但我会通过实例化来解决您的问题(在代码库中保留看似未使用的回调函数) Library<User> 并在单元测试中调用 utility() 函数(或者它应该被称为 API 测试...)。只要您不必检查库 类 和回调函数的每个可能组合,此解决方案也可能会扩展到您的真实示例。

如果您有幸在一个组织中工作,在该组织中,在更改进入代码库之前需要成功的单元测试和代码审查,这将需要更改单元测试,然后任何人都可以删除 User::func()功能,这样的更改可能会引起审阅者的注意。

话又说回来,你知道你的环境而我不知道,而且我知道这个解决方案并不适合所有情况。

不是对您关于如何记录它的问题的直接回答,而是需要考虑的事项:

如果您的库模板需要为其中使用的每个 class 实现 someFunction(),我建议将其添加为模板参数。

#include <functional>
template<class Type, std::function<void(Type*)> callback>
class Library {
    // Some Stuff...
    Type* node = nullptr;
public:
    void utility() {
         callback(this->node);
    }
};

可能会使其更加明确,以便其他开发人员知道它是必需的。

这是一个使用 Traits class:

的解决方案
// Library.h:
template<class T> struct LibraryTraits; // must be implemented for every User-class

template<class T> class Library {
public:
    T* node=nullptr;
    void utility() {
        LibraryTraits<T>::func(node);
    }
};

// User.h:
class User { };

// must only be implemented if User is to be used by Library (and can be implemented somewhere else)
template<> struct LibraryTraits<User> {
    static void func(User* node) { std::cout << "LibraryTraits<User>::func(" << node << ")\n"; }
};

// main.cpp:

int main() {
    Library<User> li; li.utility();
}

优势:

  • 从命名中可以明显看出,LibraryTraits<User> 仅在通过 Library 连接 User 时需要(并且可以删除,一旦 LibraryUser 被删除。
  • LibraryTraits 可以独立于 LibraryUser

缺点:

  • 无法轻松访问 User 的私人成员(让 LibraryTraits 成为 User 的朋友会消除独立性)。
  • 如果不同的Library需要相同的func class需要实现多个Trait classes(可以默认解决从其他 Trait classes).
  • 继承的实现

这让我想起了一个古老的好东西 Policy-Based Design,除了在你的情况下你没有从 User class 继承 Library class。 好名字是任何 API 最好的朋友。结合这个和众所周知的基于策略设计的模式(知名度非常重要,因为 class 名称中带有单词 Policy 会立即在代码的许多读者中敲响警钟)而且,我假设,你会得到一个很好的自我记录代码。

  • 继承不会给您带来任何性能开销,但会让您能够将 Callback 作为受保护的方法,这会给出一些提示,表明它应该是继承并在某处使用。

  • 在多个 User-like classes 中具有明显突出和一致的命名(例如 SomePolicyOfSomething 以上述基于策略的设计方式) ,以及 Library 的模板参数(例如 SomePolicy,或者我称之为 TSomePolicy)。

  • usingLibrary class 中声明 Callback 可能会给出更清晰和更早的错误(例如来自 IDE,或 IDE).

  • 的现代 clang、visial studio 语法分析器

如果您的 C++>=11,另一个有争议的选项可能是 static_assert。但在这种情况下,它必须用于每个 User-like class ((.

如果 User 中需要 func() 并不明显,那么我认为您违反了 single responsibility principle. Instead create an adapter class 其中 [=12] =] 作为会员。

class UserCallback {
  public:
    void func();

  private:
    User m_user;
}

这样 UserCallback 的存在证明 func() 是一个外部回调,并将 Library 对回调的需求与 [=12] 的实际职责分开=].