Как самостоятельно документировать функцию обратного вызова, вызываемую классом библиотеки шаблонов?
У меня есть функция 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;
}
Это, вероятно, более чистый, но все еще не хватает самодокумента.
Ответы
Ответ 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
.