В проекте для одного из клиентов уже используется C++20, так что есть возможность на практике прикоснуться к некоторым фичам двадцатого стандарта. Вроде ranges и concepts.
Добавил в проект кусок кода на обычных C++ные шаблонах. А потом подумал, что в паре мест там можно было бы попробовать задействовать концепты. Т.к. опыта с концептами не было, то попытался в очередной раз разобраться с этой темой и сделать что-то работающее.
Нужно сказать, что к теме концептов подступался уже несколько раз. В смысле читал разные статьи, рассказывающие о том, что такое концепты. Но, т.к. это все была только теория без практики, то "в одно ухо влетало, в другое вылетало". Тут же при попытке написать свой первый концепт пришлось все перечитывать заново 🙂 При этом оказалось, что моя задачка не похожая на те, что разбираются в обзорных статьях про концепты. Так что потребовалось не только перечитать, но еще и покурить бамбук.
В общем, смысл в том, что есть интерфейс диспетчера задач Scheduler и есть интерфейс отдельной задачи Task:
class Scheduler
{
public:
class Task
{
public:
enum class Status { Normal, Deleted };
virtual ~Task() = default;
virtual void run(Status) = 0;
};
using TaskUniquePtr = std::unique_ptr<Task>;
virtual void push(TaskUniquePtr task) = 0;
};
|
Существует несколько реализаций Scheduler-ов. В качестве же Task-ов планировалось использовать лямбда-функции, которые должны оборачиваться в реализацию интерфейса Scheduler::Task. Т.е. вместо того, чтобы делать как-то так:
class FirstTask final : public Scheduler::Task
{
... // Какие-то внутренности.
public:
... // Какой-то конструктор.
void run(Status status) { ... /* Какие-то действия */ }
};
...
scheduler.push(std::make_unique<FirstTask>(...));
|
предполагалось делать как-то так:
make_task(scheduler,
[...](Scheduler::Task::Status status) {... /* Какие-то действия */ });
|
И вспомогательный шаблон функции make_task должен был обернуть лямбду в некий класс-наследник Task. Что-то вроде:
template<typename H>
class LamdaAsTask final : public Scheduler::Task
{
H _handler;
public:
explicit LamdaAsTask(H handler) : _handler{std::move(handler)} {}
void run(Status condition) override
{
_handler(condition);
};
};
template<typename H>
void make_task(
Scheduler & scheduler,
H && handler)
{
scheduler.push(std::make_unique<LamdaAsTask<H>>(std::forward<H>(handler)));
}
|
Но ситуация была чуть сложнее. Нужно было использовать разные фабрики для Task. Т.е. в make_task передавались не только лямбда и ссылка на Scheduler, но еще и некая фабрика. Именно фабрика брала лямбду и возвращала указатель на новый Task. Т.е. шаблон make_task выглядел так:
template<typename Factory, typename H>
void make_task(
Scheduler & scheduler,
Factory && factory,
H && handler)
{
scheduler.push(factory(std::forward<H>(handler)));
}
|
И место для улучшения кода я видел в том, чтобы ограничить параметры шаблона make_task концептами.
Вырисовывалось два концепта.
Первый концепт описывает лямбду-обработчик. Ну типа ее можно вызвать с нужным параметром и она возвращает void.
template<typename H>
concept Handler_C = requires(H h)
{
{ h(Scheduler::Task::Status::Normal) } -> std::same_as<void>;
};
|
Второй концепт описывает фабрику Task-ов:
template<typename F, typename H>
concept TaskFactory_C = requires(F f)
{
requires Handler_C<H>;
{ f(H{}) } -> std::convertible_to<Scheduler::TaskUniquePtr>;
};
|
Т.е. этот концепт зависит от параметра H, который описывает лямбду. Эта лямбда должна удовлетворять требованиям концепта Handler_C. А фабрика сама по себе должна поддерживать вызов operator() с передачей туда лямбды H и возвратом чего-то приводимого к TaskUniquePtr.
Благодаря таким концептам make_task приняла вид:
template<Handler_C H, TaskFactory_C<H> Factory>
void make_task(
Scheduler & scheduler,
Factory && factory,
H && handler)
{
scheduler.push(factory(std::forward<H>(handler)));
}
|
В общем, я получил то, что хотел.
Под катом полный код автономного примера, который всю эту кухню демонстрирует.
У меня же пока впечатления от концептов смешанные. Вероятно, вещь хорошая и, местами, полезная. Но вот ВАУ эффекта пока не случилось.
Вообще с C++ есть давняя история: сперва в нем появляется какая-то фича (в смысле описывается в стандарте языка), потом она становится доступна в каком-то из компиляторов, потом у тебя появляется возможность применять ее на практике, потом ты отрефлексируешь свой опыт ее использования и только тогда тебя догоняет понимание того, как же ее нужно использовать, чтобы это было и полезно, и не сложно, и не приводило к проблемам, о которых ты раньше и не знал. С некоторыми фичами эта история растягивается не на один год. Вот как у меня с концептами, например. А до этого похожая история была с noexcept.