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

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

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

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

Как оформить это элегантно? (милый && краткий && без дополнительных затрат процессора)

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

Но он быстро испачкается — я должен добавить его к каждому «функционалу» в каждом классе.

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-таблица).
В критических случаях я не могу себе этого позволить.

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

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

Почти 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;
}

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

19

Решение

func это не особенность User, Это особенность UserLibrary<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
}
};

в 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,

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

5

Другие решения

Со стороны пользователя, я бы использовал 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();
}
4

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.

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

Какие-нибудь мысли?

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



редактировать

После игры с этим «шаблоном магии» хорошо, что нет такой вещи …
Я закомментировал шаблон функции в 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 Ключевое слово, так как это шаблон класса. Следовательно, волшебство шаблона … ну, здесь нет волшебства … только внутренняя компиляция и оптимизация.

3

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

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

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

3

Вот решение с использованием класса черт:

// 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 удалил бы независимость).
  • Если то же самое func нужен для разных Library несколько классов Trait классы должны быть реализованы (могут быть решены по умолчанию реализации, наследуя от других Trait классы).
2

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

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

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

  • имеющий using декларация Callback в Library Класс может дать гораздо более четкие и ранние ошибки (например, из IDE или современного Clang, синтаксические анализаторы визуальной студии для IDE).

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

2

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

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

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

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

1

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

0
По вопросам рекламы [email protected]