Comments 39
play.google.com/store/apps/details?id=com.keuwl.functiongenerator&hl=en
Кроме того, на смартфоне невозможно генерировать PWM сигналы с частотой в мегагерцы.
А ещё в смартфоне на выходе стоит разделительный конденсатор и сигнал там колеблется относительно земли. В моей же «поделке» сигнал колеблется относительно половины напряжения питания и его смело можно подавать на на входы микросхем которые не толерантный к напряжениям вне диапазона 0-3.3 вольта.
Есть ещё вариант с callback классом имеющим pure virtual функцию, но там свои нюансы. Например, невозможность использовать несколько функций обратного вызова в одном классе.
class ICallback
{
public:
virtual void Callback() = 0;
};
class Application : public ICallback
{
public:
virtual void Callback();
};
void Application::Callback()
{
printf("Class Application Callback()\n");
}
class Invoker
{
ICallback *_callback;
public:
void SetCallback(ICallback *callback)
{
_callback = callback;
}
void invoke()
{
if (_callback != nullptr)
{
_callback->Callback();
}
}
};
int main(int s, char** o)
{
Application app = Application();
Invoker inv = Invoker();
inv.SetCallback(&app);
inv.invoke();
return 0;
}
А как вы возьмёте указатель на не статическую функцию? В C++ вы не можете этого сделать.
Строго говоря, можно. Другой вопрос, что указатель на функцию-член без указателя на объект, для которого нужно её вызвать, большого смысла не имеет.
А что бы работать с остальными им нужна ссылка или указатель на объект (а вот о такой возможности не говорят, да и в книжках прямого указания на это я не встречал).
С одной стороны, да, явно про это не пишут. Но на мой взгляд ничего необычного и секретного, как Вы это пытаетесь преподнести, нет. Строго говоря, нестатические функции-члены осуществляют доступ к нестатическим переменным- и функциям-членам объекта класса не при помощи какой-то магии, а посредством указателя this, указание которого можно опустить при доступе. Статические функции-члены класса отличаются исключительно тем, что в них данный указатель не передаётся. Мне кажется, не трудно сопоставить факты, чтобы понять, что статической функции нужна ссылка (в общем смысле) на объект, к нестатическим членам которого необходимо произвести доступ.
Я извиняюсь за такой уровень моего занудства, просто я искренне не понимаю, почему Вы преподносите обыденную вещь как магию. Я не помню, когда решение подобной задачи вызывало у меня сложности, при том, что опыта у меня не так уж и много.
Строго говоря, можно. Другой вопрос, что указатель на функцию-член без указателя на объект, для которого нужно её вызвать, большого смысла не имеет.Как? Можно код для вывода на экран адреса функции-члена класса?
Простейший код:
printf("Ponter to SetCallback(): 0x%08X", &SetCallback);
не компилируется: Error C2276 '&': illegal operation on bound member function expressionМне кажется, не трудно сопоставить факты, чтобы понять, что статической функции нужна ссылка (в общем смысле) на объект, к нестатическим членам которого необходимо произвести доступ.Это вам кажется теперь, когда эти факты за вас сопоставлены, сделан вывод, причем все это не противоречит вашим собственным знаниям и опыту. Есть такая штука как инерция мышления — если в университете говорили нельзя, значит нельзя и даже не пытаемся это сделать.
Изначально была идея взять указатель на функцию-член класса, преобразовать её к функции вида void (*callback)(void* ptr, void* param_ptr, uint32_t param); и вызывать передавая в качестве первого указателя указатель на объект. Это не сработало, потому что указатель на функцию-член класса, даже если и можно взять, то нельзя преобразовать ни к чему другому. И вот только после этого до меня дошло попробовать со статической функцией.
В C++ есть указатели на не статические функции классов/структур: https://ideone.com/XiIW9b
Но они не эквивалентны простому адресу начала метода (в примере это видно хотя бы по sizeof). Например так: https://itanium-cxx-abi.github.io/cxx-abi/abi.html#member-pointers
Изначально была идея взять указатель на функцию-член класса, преобразовать её к функции вида void (callback)(void ptr, void* param_ptr, uint32_t param); и вызывать передавая в качестве первого указателя указатель на объект.
У вызовов методов может быть отдельное соглашение о передаче параметров — https://en.wikipedia.org/wiki/X86_calling_conventions#thiscall
Как? Можно код для вывода на экран адреса функции-члена класса?
Во-первых, я привёл ссылку на авторитетный источник — FAQ на сайте isocpp.org, где показано, как взять адрес функции-члена.
Во-вторых:
std::printf ("Ponter to SetCallback(): 0x%p", &UiButton::SetCallback);
Чтобы не возникло вопросов: да, я проверил такой синтаксис. Единственное что, проверял на своём коде, т.к. скачивать и собирать что-то под STM нет никакого желания. Если функция-член SetCallback принадлежит другому классу — используйте исправьте приведённый выше код.
Это вам кажется теперь, когда эти факты за вас сопоставлены, сделан вывод, причем все это не противоречит вашим собственным знаниям и опыту.
Кто эти факты за меня сопоставлял? Кто делал какие-то выводы? Или Вы считаете, что я увидел использование указателя на объект класса в статической функции-члене того же класса в Вашем коде впервые, и решил, что это очевидно? А ничего, что я использовал точно такой же подход в своём коде задолго до того, как Вы решили написать данную статью?
Есть такая штука как инерция мышления — если в университете говорили нельзя, значит нельзя и даже не пытаемся это сделать.
Я не понимаю, почему Вы так фиксируетесь на том том, что говорили в университете, причём строго на некорректной формулировке. У меня своя голова на плечах есть, чтобы пробовать что-то за пределами того, что давали на парах в университете. Я уж не буду говорить, что на пары по C++ я не ходил, т.к. ничего нового для меня там не рассказывали.
Во-вторых:Убедили. Похоже можно взять указатель на функцию член и положить её в переменный аргумент-лист(кладется она туда как указатель на функцию член), а потом вынуть как что угодно. Но вот сделать вот так:
Чтобы не возникло вопросов: да, я проверил такой синтаксис.std::printf ("Ponter to SetCallback(): 0x%p", &UiButton::SetCallback);
std::printf ("Ponter to SetCallback(): 0x%p", (void*)&UiButton::SetCallback);
не получится. Да и не нужно — все решается теми методами, которые тут уже обсудили.С помощью std::bind. 2018 год на дворе, как никак.
В приведённом коде функции Application::Callback я вижу сразу несколько проблем:
- Использование void* для передачи указателей на объекты. Помимо нарушения идеи безопасности на уровне типов, это также усложняет понимание сигнатуры функции. EDIT: Видимо, это вынужденная мера, т.к. UiButton хочет такую сигнатуру функции обратного вызова. Моего бессмысленного недовольства это не уменьшает, но позволяет понять, почему так.
- Отсутствие проверки указателей на nullptr. Если данные проверки были опущены для простоты понимания в рамках статьи — стоит указать на это в тексте статьи, чтобы не возникало непонимания. Если же проверок нет, то их разумно добавить, как минимум в виде assert'ов.
- Отсутствие проверки типов аргументов. Раз уж передаются произвольные указатели, то необходимо проверять, что это указатели на объекты правильных типов. Простейшим, хотя и не самым эффективным, является использование dunamic_cast(). Более эффективных методов много, требуют они написания различного объёма вспомогательного кода. Но лучшим решением является, разумеется, использование правильных типов параметров функции.
Еще с университетской скамьи я помню постулат: «Статические функции не имеют доступа к не статическим членам класса». Так вот это не соответствует действительности. Поскольку статическая функция является членом класса, то она имеет доступ ко всем членам класса, если имеет ссылку/указатель на этот класс.
Как бы Вам сказать… Это не совсем корректно сформулированный постулат. Идея, заложенная в постулате, заключается в том, что нельзя в статических функциях-членах класса использовать нестатические переменные-члены и функции-члены без объекта данного класса. Поэтому, Вы уж извините, никакой магии Вы не используете.
Проблемы с callback реализуемыми в с++:
1) с++ в callback должен сам ловить все свои Exceptions, иначе вызывающий С код может работать предельно некорректно — например оставлять занятые мутексы, не освобожденные ресурсы и пр.
2) в общем случае с++ должен сам убедиться, что в указателе именно экземпляр нужного класса. dynamic_cast для этого годится плохо, т.к. dynamic_cast лишь предоставляет «навигацию» по дереву наследования полиморфных классов, а тут на входе вовсе не класс, а указатель на нечто (void*) с которым dynamic_cast работать не должен (допускаю, что возможно какой-то компилятор по ошибке позволяет, но стандарт чётко это запрещает).
С другой стороны, с++ часть может «полагаться» на правильную работу С части и ожидать в ответе именно тот указатель, который был ей передан ранее. Тогда нужно гарантировать что lifetime объекта будет достаточен, до всех вызовов callback с этим классом.
Есть еще вариант — передать указатель на память, в котором разместить признак (просто ID объекта) и указатель на объект. Эта память может быть частью самого объкта, и в деструкторе, если вдруг объект досрочно уничтожается, можно этот ID менять на другой — легче ловить dangling pointers. Правда это вовсе не гарантирует что следующий размещенный объект не будет создан в том же месте кучи.
Если реализуется взаимодействие между С и C++, то такие callbacks с void* opaque pointer являются наистандартнейшим решением.
С этим я полностью согласен. В рамках же рассматриваемого в статье кода взаимодействия между C и C++, насколько я могу судить, не наблюдается, т.к. Application::Callback используется в связке с классом UiButton, т.е. это не C. Насколько я могу судить, весь код написан на C++, и в моём представлении в этом случае лучше использовать механизмы C++.
в общем случае с++ должен сам убедиться, что в указателе именно экземпляр нужного класса. dynamic_cast для этого годится плохо, т.к. dynamic_cast лишь предоставляет «навигацию» по дереву наследования полиморфных классов, а тут на входе вовсе не класс, а указатель на нечто (void*) с которым dynamic_cast работать не должен (допускаю, что возможно какой-то компилятор по ошибке позволяет, но стандарт чётко это запрещает).
Признаю, был неправ. Для проверки типа объекта, размещённого по адресу, хранимому в void*, использовать dynamic_cast нельзя. Произвёл небольшие опыты, которые показали, что dynamic_cast либо не будет работать правильно, либо приложение аварийно завершит работу. С dynamic_cast всё ещё можно что-то сделать, но это потребует передачи указателя на некоторый базовый тип, что, мягко говоря, является сомнительной идеей в общем случае, особенно при взаимодействии между C и C++.
Относительно же времени жизни и т.п.: ох, как же у меня всегда болит голова, когда я вижу передаваемые куда-либо указатели на объекты. Я знаю, что эти объекты будут жить, сколько надо, но как-то неспокойно мне на фоне страшных рассказов про несоответствие времени жизни и времени использования объекта.
В рамках же рассматриваемого в статье кода взаимодействия между C и C++, насколько я могу судить, не наблюдается, т.к. Application::Callback используется в связке с классом UiButton
Да, я видел это, но не смотрел что лежит под UiButton. Если UiButton лишь wrapper вокруг С библиотеки, то могу допустить что интерфейс callback напрямую вытащен наружу для упрощения.
С другой стороны, вы правы — сам UiButton уже С++ и мог бы взять на себя функции взаимодействия с С++ приложением более подобающим для С++ способом. Для этого нужны или шаблоны или полиморфные классы. Т.е. библиотека должна знать API потребителя (приложения) — т.е. вызывать его методы — или совсем не зная его классов, работая через шаблоны, или должна знать лишь базовые классы, которые в ней же в библиотеке реалиованы и которые потребитель отнаследует и реализует соответственно.
Я знаю, что эти объекты будут жить, сколько надо, но как-то неспокойно мне на фоне страшных рассказов
Всякое бывает. Если время жизни не получается легко контролировать, то используется shared_ptr. Решение не идеальное, конечно, но в ряде случаев ничего другого не остается.
2) Замечание верное, в данном случае — это просто упрощение и допущение: тут всего 4 кнопки, создаются они вначале и им тут же всем задаются callback функции с передачей указателя this, т.е. nullptr в данном случае там в теории не должен появится никогда.
3) "Правильных" — это каких? Из тех что я знаю — использование callback класса с виртуальной функцией. Пример я приводил выше. Минус — в классе Application будет всего одна callback функция для всех объектов UiButton. Впрочем, она и сейчас одна, так что, возможно, переделаю именно на такой способ.
А еще мне не очень нравится в таком способе, что если понадобится еще какой-нибудь объект(UiScroll например), то уже нужно будет множественное наследование в классе Application. Ну и дополнительно еще где-то таблицы виртуальных функций будут место занимать. Зато снимутся все вопросы с типами и безопасностью.
Идея, заложенная в постулате, заключается в том, что нельзя в статических функциях-членах класса использовать нестатические переменные-члены и функции-члены без объекта данного класса.Идея понятна, но то, как она преподнесена создало, как минимум у меня, инерцию мышления на преодоление которой я потратил пару вечеров.
Поэтому, Вы уж извините, никакой магии Вы не используете.Может поэтому слово "магия" — было взято в кавычки? Все действительно происходит в рамках стандарта.
1) А как передать указатели на разные объекты в UiButton для последующей передачи в callback функцию? Я знаю только один такой способ — привести к единому типу.
Как вариант, использовать объект специального класса для вызова функции обратного вызова:
class ICallback {
public:
// Можно использовать NVI при желании/по необходимости.
void operator()(// Параметры функции) = 0;
};
class ApplicationCallbackWrapper : public ICallbackWrapper {
public:
ApplicationCallbackWrapper (Application* i_application)
: application_ {i_application} {
/*
При необходимости - проверка указателя на nullptr. Если считается,
что вызов может быть NOOP, то можно разрешить использование нулевого
указателя.
*/
}
void operator()(// Параметры функции) override {
if (!application_) {
/*
Можно добавить обработку по умолчанию. Например, вывод
предупреждения в режиме отладки.
*/
return;
}
// Необходимая обработка аргументов функции.
/*
Будем считать, что существует одна нестатическая функция-член,
которую необходимо вызывать в функции обратного вызова. В случае,
если требуется вызывать различные функции, можно написать несколько
подобных классов-обёрток - по одному на каждую функцию. Если выбор
функции должен осуществляться на основе аргументов функции -
необходимо добавить соответствующую обработку и
*/
application_->Callback(// Аргументы для вызова основной функции);
}
private:
/*
Можно использовать std::weak_ptr, что позволит узнавать об удалении
объекта. Использование данного подхода требует, чтобы время жизни
объекта (*application_) контроллировалось std::shared_ptr, что
ограничивает возможность создания объектов ApplicationCallbackWrapper
из функций-членов класса Application. Данное ограничение можно обойти
через использование интрузивного подсчёта ссылок.
*/
Application* application_;
};
/*
При желании можно использовать владеющий (std::unique_ptr,
std::shared_ptr если требуется использовать один объект обратного вызова
из нескольких объектов) либо не владеющий (std::weak_ptr) указатель.
В примене простой указатель для описания общей идеи. Если объект UiButton
владеет функтором обратного вызова, и используется обычный указатель, то
следует удалить объект при задании нового функтора обратного вызова и в
деструкторе.
*/
UiButton::SetCallback (ICallback* i_callback) {
if (!i_callback) {
// Необходимая обработка.
}
// callback_ - переменая-член класса UiButton для хранения функтора обратного вызова
callback_ = i_callback;
}
UiButton::SomeFunction (...) {
// ...
if (callback_) {
(*callback_)(// Аргументы);
}
// ...
}
int main (...) {
// Используются простый указатели для краткости
Application* application = new Application{};
UiButton* button = new UiButton{};
button->SetCallback (new ApplicationCallbackWrapper{application});
}
Альтернативно, можно использовать std::function:
class ApplicationCallbackWrapper {
/*
Такой же класс, как выше, но без наследования.
*/
};
class UiButton {
using Callback = std::function <void()>;
// ...
}
UiButton::SetCallback (UiButton::Callback i_callback) {
if (!i_callback) {
// Необходимая обработка.
}
// callback_ - переменая-член класса UiButton для хранения функтора обратного вызова
callback_ = i_callback;
}
UiButton::SomeFunction (...) {
// ...
if (callback_) {
callback (// Аргументы);
}
// ...
}
int main (...) {
// Используются простый указатели для краткости
Application* application = new Application{};
UiButton* button = new UiButton{};
button->SetCallback (ApplicationCallbackWrapper{application});
/*
Если можжно гарантировать, что время жизни application больше времени
жизни button, то можно использовать лямбда-функцию.
*/
button->SetCallback ([application](// Параметры функции){
// Использование application.
});
// Так тоже можно
button->SetCallback (SomeFreeFunction);
}
Может лучше на DDS?
На той же AD9850
Старенький "ламповый" осциллограф! Когда-то у меня подобный Phillips был...
settling time (...) составляет 3 ms
Микросекунд наверное, всё-таки.
И что это за странные всплески у вас на всех осциллограммах?
Я что-то не понял, зачем какие-то заморочки со статическими методами, какими-то кастами и прочим или java-style передача интерфейсов. Чем этот метод плох? Также можно и темплейт использовать. Пруф, можете проверить, что работает
#include <iostream>
using namespace std;
class BaseFoo
{
public:
virtual void bar()=0;
};
class Foo1: public BaseFoo
{
public:
virtual void bar() override
{
cout << "Foo1::bar()\n";
}
};
class Foo2: public BaseFoo
{
public:
virtual void bar() override
{
cout << "Foo2::bar()\n";
}
};
void func(BaseFoo* ptr, void (BaseFoo::*func)())
{
(ptr->*func)();
}
int main()
{
Foo1 foo1;
Foo2 foo2;
func(&foo1, &BaseFoo::bar);
func(&foo2, &BaseFoo::bar);
return 0;
}
Выше был пример, когда делали интерфейс нужного коллбека, передавали и вызывали. Как зачем? Это, собственно, та функция-член другого класса, которую мы хотим вызвать. Внутри метода func
мы можем даже не знать, что это за метод. Мы можем вместо BaseFoo*
использовать шаблон и даже не знать, что за класс. Такой подход применяется, к примеру, в std::thread. Я не стал писать пример с шаблоном, чтобы показать, что с полиморфизмом тоже работает.
#include <iostream>
using namespace std;
class Foo
{
public:
void bar()
{
cout << "Foo::bar()\n";
}
};
template<class T>
void func(T* ptr, void (T::*func)())
{
(ptr->*func)();
}
int main()
{
Foo foo;
func(&foo, &Foo::bar);
return 0;
}
Ну первый пример, может быть, и не очень наглядный. Но если шаблон использовать, мы передать указатель на фунцию-член, в то время как принимающая функция ничего не знает о нашем классе/базовом классе.
Также у нас может быть несколько функций-членов с подходящей сигнатурой, мы можем передать любую из них. Например, у нас есть класс Window
, который содержит два объекта Button
. Мы хотим, чтобы нажатие на них обрабатывались независимыми обработчиками.
class MyWindow: Window, OnClickListener
{
public:
MyWindow()
{
btn1.setOnClickListener(this);
btn2.setOnClickListener(this);
}
// Implement IClickListener (View - какой-нибудь базовый класс Button)
virtual onClick(View& sender) override
{
// Кто вызвал метод? Надо проверить sender
}
}
В Android+Java часто применяют анонимные классы.
btn1.setOnClickListener(new OnClickListener(){
@Override
void onClick(View v) {doFoo();}
});
btn2.setOnClickListener(new OnClickListener(){
@Override
void onClick(View v) {doBar();}
});
С описываемым мною подходом можно поступить так:
class MyWindow: Window
{
public:
MyWindow()
{
btn1.setOnClickListener(this, &MyWindow::onBtn1Click);
btn2.setOnClickListener(this, &MyWindow::onBtn2Click);
}
onBtn1Click(View& sender) {doFoo(); }
onBtn2Click(View& sender) {doBar(); }
}
Ну первый пример, может быть, и не очень наглядный. Но если шаблон использовать, мы передать указатель на фунцию-член, в то время как принимающая функция ничего не знает о нашем классе/базовом классе.Как это "не знает"? Очень даже знает! Потому что шаблонный класс есть только в исходниках, в скомпилированном коде будет много почти одинаковых классов с разными типами.
Да, можно в классе UiButton объявить указатель на callback шаблонным типом, и функция SetCallback() может принимать шаблонный тип. Но тогда для каждого типа callback'ов будет создаваться отдельный класс UiButton со свои кодом. Если callback один(как в этой статье) — проблем нет, будет все равно один экземпляр UiButton. А вот если программа посложнее, с множеством классов использующих UiButton, то объем исполняемого кода в бинарнике начнет расти очень даже значительно.
Вы посмотрели пример с кнопками? Он и без шаблонов делается, там может быть аргументом базовый класс окна или еще чего-нибудь. Но можно разные коллбеки в одном классе использовать, в отличие от способа, когда мы у переданного указаетля/ссылки на интерфейс вызываем определенный метод коллбека.
P.S. Собственно, я совершенно не претендую на то, что мой вариант самый лучший и/или единственно правильный. Просто это тот метод, который первый приходит мне в голову при упоминании ссылки на функцию-член.
Помимо уже заданных вопросов у меня есть ещё один: при чём тут Java?
Также можно и темплейт использовать.
Для чего Вы предлагаете использовать шаблоны?
Помимо уже заданных вопросов у меня есть ещё один: при чём тут Java?
Ну, я часто встречал подход, когда класс реализует некий интерфейс слушателя события, в котором требуемый метод обратного вызова, и передают его куда надо. В Android такое постоянно.
Для чего Вы предлагаете использовать шаблоны?
Чтобы можно было передать указатель на функцию-член произвольного класса. Например, так реализовано в std::thread
: https://stackoverflow.com/a/10673671
Ну, я часто встречал подход, когда класс реализует некий интерфейс слушателя события, в котором требуемый метод обратного вызова, и передают его куда надо. В Android такое постоянно.
Паттерн проектирования наблюдатель (если я конечно не ошибаюсь в данном случае) является универсальным и его применимость не зависит от языка программирования.
Распространённость варианта с интерфейсами в Java можно объяснить тем, что свободных функций нет (насколько я знаю), а сам язык позиционирует себя как объектно-ориентированный, что приводит к созданию классов на каждый чих.
В C++ единой универсальной парадигмы нет, да и язык несколько старше, с определённым багажом, в результате чего варианты с интерфейсами типа IObservable и IObserver (которые, разумеется, не интерфейса, а абстрактные либо чисто абстрактные классы) существуют на ровне с сигналами-слотами, функциями обратного вызова, событиями и другими вариациями на тему реализации того же самого паттерна.
Если я заврался на почве собственного невежества — покорнейше прошу поправить меня.
Чтобы можно было передать указатель на функцию-член произвольного класса. Например, так реализовано в std::thread: stackoverflow.com/a/10673671
Могу я попросить Вас привести короткий пример кода, а то никак не могу сообразить, где и как надо использовать шаблоны в данном случае. Самым гибким, на мой взгляд, является вариант с использованием std::function, который я описал выше.
Распространённость варианта с интерфейсами в Java можно объяснить тем, что свободных функций нет (насколько я знаю), а сам язык позиционирует себя как объектно-ориентированный, что приводит к созданию классов на каждый чих.
Именно поэтому я и назвал Java-style. Я не могу утверждать, что именно в Java это наиболее распространено, исключительно по скромному опыту Android разработки. В C# тоже нет свободных функций, но там есть делегаты (что-то вроде типизированного указателя на функцию с ссылки на объект) и события (фактически observer на уровня языка). А в Java нет и идут On***Listener
на каждый чих.
Могу я попросить Вас привести короткий пример кода, а то никак не могу сообразить, где и как надо использовать шаблоны в данном случае.
Как-то так. Здесь DataLoader
ничего не знает о классах, которые его используют, и чьи коллбеки он вызывает.
#include <iostream>
using namespace std;
struct SomeData{};
template<class T>
class DataLoader
{
public:
DataLoader(T* ptr, void (T::*callback)(SomeData))
: _callback(callback), _ptr(ptr)
{}
void beginLoading()
{
//...
cout << "DataLoader::beginLoading()\n";
SomeData some_data;
(_ptr->*_callback)(some_data);
}
private:
T* _ptr;
void (T::*_callback)(SomeData);
};
class Foo
{
public:
void foo()
{
cout << "Foo::foo()\n";
// do something
DataLoader<Foo> loader(this, &Foo::callback);
loader.beginLoading();
}
void callback(SomeData data)
{
cout << "Foo::callback()\n";
}
};
class Bar
{
public:
void bar()
{
cout << "Bar::bar()\n";
// do something
DataLoader<Bar> loader(this, &Bar::data_loaded);
loader.beginLoading();
}
void data_loaded(SomeData data)
{
cout << "Bar::data_loaded()\n";
}
};
int main()
{
Foo foo;
Bar bar;
foo.foo();
bar.bar();
return 0;
}
Самым гибким, на мой взгляд, является вариант с использованием std::function, который я описал выше.
Соглашусь. Забыл о нем. А у std::function
есть оверхед? Я их только с лямбдами использовал, а лямбды — объект создается.
По коду: про такой вариант не подумал, потому что Вами предложено обобщённое решение проблемы, а я пытался придумать, как применить шаблоны в рамках представленного в статье кода без его сильного изменения. Единственным недостатком Вашего варианта является, собственно, статический полиморфизм и сопутствующая невозможность использования прямолинейного использования динамического полиморфизма для объектов типа DataLoader. Решить это можно через стирание типов, но это приведёт к тому же, что происходит в std::function или boost::type_erasure, только самодельному.
Кстати, можно решить всю задачу целиком с использованием boost::type_erasure. Тогда уже можно передавать более сложные обработчики обратного вызова (если можно это так назвать), со сложным интерфейсом и т.п., без использования механизмов наследования напрямую. Наследование при этом магическим образом перенесётся в код, сгенерированный boost::type_erasure.
DevBoy: делаем генератор сигналов