У меня есть функция 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();
}
В качестве первого обходного пути я, как правило, добавляю такой комментарий:
class User{
/** This function is for "Library" callback */
public: void func(){/** some code*/}
};
Но он быстро испачкается — я должен добавить его к каждому «функционалу» в каждом классе.
В реальном случае я склоняюсь к префиксу имени функции так:
class User{
public: void LIBRARY_func(){/** some code*/}
};
Это очень заметно, но имя функции теперь очень длинное.
(особенно когда Library
-класс имеет более длинное имя класса)
Я рассматриваю возможность создания абстрактного класса в качестве интерфейса для обратного вызова.
class LibraryCallback{
public: virtual void func()=0;
};
class User : public LibraryCallback{
public: virtual void func(){/** some code*/}
};
Это дает ощущение, что func()
для что-то-очень-внешнее. :)
Однако я должен пожертвовать стоимостью виртуальных звонков (v-таблица).
В критических случаях я не могу себе этого позволить.
(идея от Даниэля Жура в комментарии, спасибо!)
Почти 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;
}
Это, вероятно, чище, но все еще не хватает самодокументирования.
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
}
};
в 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
,
Используемые здесь методы: «диспетчеризация тегов», «поиск, зависящий от аргумента», «функции друга» и предпочтение свободных функций методам.
Со стороны пользователя, я бы использовал 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.
Я смог скомпилировать, собрать и запустить это без ошибок в 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
Ключевое слово, так как это шаблон класса. Следовательно, волшебство шаблона … ну, здесь нет волшебства … только внутренняя компиляция и оптимизация.
Хотя я знаю, что не отвечаю на ваш конкретный вопрос (как задокументировать функцию, которую нельзя удалить), я бы решил вашу проблему (сохранение, казалось бы, неиспользуемой функции обратного вызова в базе кода) путем создания экземпляра Library<User>
и зовет utility()
функция в модульном тесте (или, может быть, его лучше назвать API-тестом …). Это решение, вероятно, будет также соответствовать вашему реальному примеру, если вам не нужно проверять каждую возможную комбинацию классов библиотеки и функций обратного вызова.
Если вам повезло работать в организации, где требуется успешное модульное тестирование и проверка кода, прежде чем изменения будут внесены в базу кода, это потребует изменения в модульных тестах, прежде чем кто-либо сможет удалить User::func()
функция и такое изменение, вероятно, привлечет внимание рецензента.
С другой стороны, вы знаете свою среду, а я нет, и я знаю, что это решение не подходит для всех ситуаций.
Вот решение с использованием класса черт:
// 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
классы).Это сильно напоминает старое добро Политический дизайн, кроме как в вашем случае вы не наследуете Library
класс из User
учебный класс.
Хорошие имена — лучшие друзья любого API. Объедините это и известный шаблон проектирования на основе политик (известный очень важен, потому что имена классов со словом Policy
в нем сразу же прозвучит звонок во многих читателях кода) и, я полагаю, вы получите хорошо самодокументированный код.
Наследование не приведет к снижению производительности, но даст возможность Callback
как защищенный метод, это даст некоторый намек на то, что он должен наследоваться и использоваться где-то.
Иметь четко выделяющиеся и последовательные названия среди нескольких User
подобные классы (например, SomePolicyOfSomething
в порядке вышеупомянутой разработки на основе политик), а также Library
(например SomePolicy
или я бы назвал это TSomePolicy
).
имеющий using
декларация Callback
в Library
Класс может дать гораздо более четкие и ранние ошибки (например, из IDE или современного Clang, синтаксические анализаторы визуальной студии для IDE).
Другой спорный вариант может быть static_assert
если у вас есть C ++> = 11. Но в этом случае он должен быть использован в каждом User
класс ((.
Не прямой ответ на ваш вопрос о том, как это документировать, а то, что нужно учесть:
Если ваш шаблон библиотеки требует реализации someFunction()
для каждого класса, который будет использоваться в нем, я бы рекомендовал добавить его в качестве аргумента шаблона.
#include <functional>
template<class Type, std::function<void(Type*)> callback>
class Library {
// Some Stuff...
Type* node = nullptr;
public:
void utility() {
callback(this->node);
}
};
Может сделать это еще более явным, чтобы другие разработчики знали, что это необходимо.
абстрактный класс — лучший способ заставить функцию не удаляться. Поэтому я рекомендую реализовать базовый класс с чисто виртуальной функцией, чтобы производный должен определять функцию.
ИЛИ Второе решение будет иметь указатели на функции, чтобы производительность была сохранена, избегая дополнительных затрат на создание и вызов V-таблицы.