Search code examples
c++templatescallbackreadabilitymaintainability

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


I have a function User::func()(callback) that would be called by a template class (Library<T>).

In the first iteration of development, everyone know that func() serves only for that single purpose.
A few months later, most members forget what func() is for.
After some heavy refactoring, the func() is sometimes deleted by some coders.

At first, I didn't think this is a problem at all.
However, after I re-encountered this pattern several times, I think I need some counter-measure.

Question

How to document it elegantly? (cute && concise && no additional CPU cost)

Example

Here is a simplified code:-
(The real world problem is scattering around 10+ library-files & 20+ user files & 40+ functions.)

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

My poor solutions

1. Comment / doc

As the first workaround, I tend to add a comment like this:-

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

But it gets dirty pretty fast - I have to add it to every "func" in every class.

2. Rename the "func()"

In real case, I tend to prefix function name like this:-

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

It is very noticeable, but the function name is now very longer.
(especially when Library-class has longer class name)

3. Virtual class with "func()=0"

I am considering to create an abstract class as interface for the callback.

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

It provides feeling that func() is for something-quite-external. :)
However, I have to sacrifice virtual-calling cost (v-table).
In performance-critical cases, I can't afford it.

4. Static function

(idea from Daniel Jour in comment, thank!)

Almost 1 month later, here is how I use :-

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

It is probably cleaner, but still lack self-document.


Solution

  • func is not a feature of User. It is a feature of the User-Library<T> coupling.

    Placing it in User if it doesn't have clear semantics outside of Library<T> use is a bad idea. If it does have clear semantics, it should say what it does, and deleting it should be an obviously bad idea.

    Placing it in Library<T> cannot work, because its behavior is a function of the T in Library<T>.

    The answer is to place it in neither spot.

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

    Now in Library.h:

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

    in User.h:

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

    or just put this into the same namespace as User, or the same namespace as ForLibrary:

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

    Before deleting func, you'll track down ForLibrary.

    It is no longer part of the "public interface" of User, so doesn't clutter it up. It is either a friend (a helper), or a free function in the same namespace of either User or Library.

    You can implement it where you need a Library<User> instead of in User.h or Library.h, especially if it just uses public interfaces of User.

    The techniques used here are "tag dispatching", "argument dependent lookup", "friend functions" and preferring free functions over methods.