Подтвердить что ты не робот

Как самостоятельно документировать функцию обратного вызова, вызываемую классом библиотеки шаблонов?

У меня есть функция User::func() (callback), которая будет вызвана классом шаблона (Library<T>).

На первой итерации развития все знают, что func() служит только для этой единственной цели.
Несколько месяцев спустя большинство членов забывают, что такое func().
После некоторого тяжелого рефакторинга, func() иногда удаляется некоторыми кодировщиками.

Сначала я не думал, что это проблема вообще.
Однако после того, как я несколько раз столкнулся с этим шаблоном, мне кажется, что мне нужна контрмера.

Вопрос

Как оформлять его элегантно? (cute && сжатие && no дополнительные затраты ЦП)

Пример

Вот упрощенный код: -
 (Реальная проблема заключается в рассеянии около 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. Комментарий /doc

В качестве первого обходного пути я хотел бы добавить комментарий следующим образом: -

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

Но он становится грязным довольно быстро - я должен добавить его к каждому "func" в каждом классе.

2. Переименуйте "func()"

В реальном случае я предпочитаю префикс имени функции следующим образом: -

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

Это очень заметно, но имя функции теперь очень велико.
(особенно когда Library -класс имеет более длинное имя класса)

3. Виртуальный класс с "func() = 0"

Я рассматриваю возможность создания абстрактного класса в качестве интерфейса для обратного вызова.

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

Он обеспечивает ощущение, что func() для что-то-довольно-внешнего. :)
Тем не менее, я должен пожертвовать стоимостью виртуального вызова (v-table).
В критичных по производительности случаях я не могу себе этого позволить.

4. Статическая функция

(идея от Дэниела Жора в комментарии, спасибо!)

Почти через месяц, вот как я использую: -

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

Это, вероятно, более чистый, но все еще не хватает самодокумента.

4b9b3361

Ответ 1

func не является признаком User. Это особенность связи User - Library<T>.

Размещение в User, если у него нет четкой семантики вне Library<T>, использование - плохая идея. Если у него есть четкая семантика, он должен сказать, что он делает, и удаление его должно быть явно плохой идеей.

Размещение в Library<T> не может работать, потому что его поведение является функцией T в Library<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
  }
};

in 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, поэтому он не загромождает его. Это либо друг (помощник), либо свободная функция в том же пространстве имен либо User, либо Library.

Вы можете реализовать его там, где вам нужен Library<User> вместо User.h или Library.h, особенно если он использует только общедоступные интерфейсы User.

Используемые здесь методы - "диспетчеризация тегов", "зависимый от аргументов поиск", "функции друзей" и предпочтение бесплатных функций над методами.

Ответ 2

С пользовательской стороны я бы использовал crtp, чтобы создать интерфейс обратного вызова и заставить пользователей использовать его. Например:

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

Пользователи должны наследовать от этого интерфейса и реализовать foo() callback

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

Ответ 3

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 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 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.

Я смог компилировать, создавать и запускать это без ошибок в VS2017 CE на Windows 7 - 64-битной Intel Core 2 Quad Extreme.

Любые мысли?

Я бы рекомендовал назвать класс-оболочку соответствующим образом, а затем для каждой конкретной функции обратного вызова, которая имеет уникальное назначение, соответственно, в классе оболочки.



Edit

После игры с этой "магией шаблонов" хорошо нет такой вещи... Я прокомментировал шаблон функции в классе Wrapper и обнаружил, что он не нужен. Затем я прокомментировал class Wrapper*, который является списком аргументов для Callback() в User. Это дало мне ошибку компилятора, в которой указано, что User::Callback() не принимает аргументы 0. Поэтому я оглянулся на Wrapper, так как User наследует от него. Ну, в этот момент Wrapper - пустой шаблон шаблона.

Это заставило меня взглянуть на Library. Библиотека имеет указатель на User как открытый член и функцию utility(), которая вызывает метод User's static Callback. Именно здесь вызывающий метод принимает указатель на объект 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* означает или находится в списке аргументов для метода Callback в классе User.

Я попытаюсь объяснить это так ясно, как только могу, но сначала класс-оболочка - это просто пустая оболочка шаблона, которая User будет наследовать, и она ничего не делает, а действует как базовый класс, и теперь она выглядит следующим образом:

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

Когда мы смотрим на класс User:

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

Мы видим, что User является неклассическим классом, который наследуется от класса шаблона, но использует его как аргумент шаблона. Он содержит публичный статический метод и этот метод не возвращает ничего, но он принимает один параметр; это также очевидно в классе Library, который имеет свой шаблонный параметр как класс User. Когда метод Library's utility() вызывает метод User's Callback(), параметр, ожидаемый Библиотекой, является указателем на объект User. Поэтому, когда мы возвращаемся в класс User вместо того, чтобы объявлять его как указатель User* непосредственно в его объявлении, я использую пустой шаблон класса, на который он наследует. Однако если вы попытаетесь сделать это:

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

Вы должны получить сообщение о том, что Wrapper* отсутствует список аргументов. Мы могли бы просто сделать Wrapper<User>* здесь, но это избыточно, так как мы уже видим, что пользователь наследует от Wrapper, который берет себя. Поэтому мы можем исправить это и сделать его более чистым, просто префикс Wrapper* с ключевым словом class, так как это шаблон класса. Следовательно, магия шаблона... ну нет здесь волшебства... просто встроенный компилятор и оптимизация.

Ответ 4

В то время как я знаю, что я не отвечаю на ваш конкретный вопрос (как документировать не подлежащую удалению функцию), я бы решил вашу проблему (сохраняя, казалось бы, неиспользуемую функцию обратного вызова в базе кода) путем создания экземпляра Library<User> и вызов функции utility() в unit test (или, возможно, ее скорее следует назвать тестом API...). Это решение, вероятно, будет масштабироваться и для вашего реального примера, если вам не нужно проверять каждую возможную комбинацию классов библиотек и функций обратного вызова.

Если вам посчастливилось работать в организации, где требуются успешные модульные тесты и проверка кода до того, как изменения войдут в базу кода, это потребует изменения в модульных тестах, прежде чем кто-либо сможет удалить функцию User::func() и такую изменение, вероятно, привлечет внимание рецензента.

Затем вы знаете свою среду, а я нет, и я знаю, что это решение не подходит для всех ситуаций.

Ответ 5

Вот решение, использующее класс Traits:

// 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> требуется только для взаимодействия User с помощью Library (и его можно удалить, как только удаляются теги Library или User.
  • LibraryTraits может быть специализированным независимо от Library и User

Недостатки:

  • Легкий доступ к частным членам User (создание LibraryTraits друга User приведет к удалению независимости).
  • Если для разных классов Library требуется один и тот же func, необходимо реализовать несколько классов Trait (их можно решить по умолчанию, наследующим от других классов Trait).

Ответ 6

Это сильно напоминает старый добрый Policy-Based Design, за исключением вашего случая, когда вы не наследуете класс Library из User класс. Хорошие имена - лучшие друзья любого API. Объедините это и хорошо известное средство разработки на основе политик (хорошо известно, что очень важно, потому что имена классов со словом Policy в нем немедленно вызовут звонок во многих читателях кода), и, я полагаю, вы получить хорошо самодокументирующий код.

  • Наследование не даст вам каких-либо служебных накладных расходов, но даст вам возможность использовать Callback как защищенный метод, который даст некоторый намек на то, что он должен быть унаследован и использоваться где-то.

  • Иметь четкое и последовательное присвоение имен нескольким User -подобным классам (например, SomePolicyOfSomething в соответствии с вышеупомянутым политическим дизайном), а также аргументы шаблона для Library (например, SomePolicy, или я бы назвал его TSomePolicy).

  • Объявление using Callback в классе Library может дать гораздо более ясные и более ранние ошибки (например, из IDE или современных clang, синтаксических синтаксических анализаторов синтаксиса для IDE).

Другим аргументированным вариантом может быть static_assert, если у вас есть С++ >= 11. Но в этом случае он должен использоваться в каждом классе User -like ((.

Ответ 7

Не прямой ответ на ваш вопрос о том, как документировать его, но что-то, что нужно учитывать:

Если для шаблона библиотеки требуется реализация someFunction() для каждого класса, который будет использоваться в нем, я бы рекомендовал добавить его как аргумент шаблона.

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

Возможно, он станет еще более явным, чтобы другие разработчики знали, что это необходимо.

Ответ 8

абстрактный класс - лучший способ обеспечить выполнение функции, которую нельзя удалить. Поэтому я рекомендую реализовать базовый класс с чистой виртуальной функцией, так что производный должен определить функцию. ИЛИ вторым решением было бы иметь указатели на функции, чтобы производительность была сохранена, избегая дополнительных накладных расходов на создание и вызов V-таблицы.

Ответ 9

Если не очевидно, что func() требуется в User, я бы сказал, что вы нарушаете принцип принцип единой ответственности. Вместо этого создайте adapter класс, в котором User в качестве члена.

class UserCallback {
  public:
    void func();

  private:
    User m_user;
}

Таким образом, существование UserCallback документов, что func() является внешним обратным вызовом, и отделяет Library от обратного вызова от фактических обязанностей User.