У меня есть класс, и мне нужно проверить, что его вызовы функций вызываются с правильными параметрами. Сигнатура функции всегда одна и та же (без 1 типа аргумента). Поэтому, естественно, я выбрал шаблонный подход. Таким образом, обычно политика проверки будет иметь параметр шаблона для каждого типа данных, который она может обрабатывать:
using P = Policy<int, double, UserDefined>
Или что-то в этом роде.
Я получил его для компиляции, но предостережение в том, что если double
и int
(или что-то, во что double
может быть преобразовано на самом деле) являются параметрами шаблона, double
будет неявно преобразован.
Политика выглядит так:
template <typename... T>
class BasicValidationPolicy { };
template <>
class BasicValidationPolicy<>
{
public:
void RegisterSetHandler();
};
template <typename T, typename... Rest>
class BasicValidationPolicy<T, Rest...> : public BasicValidationPolicy<Rest...>
{
public:
using SetHandler = std::function<void(int, T)>;
void RegisterSetHandler(const SetHandler& handler)
{
m_setHandler = handler;
}
void Set(int n, const T& val) {
if (m_setHandler) {
m_setHandler(n, val);
}
}
private:
SetHandler m_setHandler{nullptr};
};
Класс, который его использует...
template <typename ValidatorPolicy>
class MyClass : public ValidatorPolicy {
public:
void OnSetInt(int n, int64_t v)
{
ValidatorPolicy::Set(n, v);
}
void OnSetDouble(int n, double d)
{
ValidatorPolicy::Set(n, d);
}
};
Использование:
int main()
{
using Policy = BasicValidationPolicy<int64_t, double>; // doesn't work
MyClass<Policy> m;
m.Policy::RegisterSetHandler([](int i, double value) {
// by this point value is an int64_t
std::cout << "Got double " << i << ", " << value << "\n";
});
double d{35.2135};
m.OnSetDouble(1, d);
}
Для загрузки это работает
using Policy = BasicValidationPolicy<double, int64_t>;
Так что я думаю, что я что-то упустил в выводе шаблона. Похоже, что он пытается сопоставить double
с std::int64_t
, говорит «хм, достаточно хорошо» и движется дальше. Приятно знать способ обойти это (отчасти), но похоже, что это будет очень сложно поддерживать.
Все сложно...
Прежде всего: у вас есть рекурсивный класс-шаблон BasicValidationPolicy
, в котором вы определяете два метода и хотите, чтобы все методы для всех шагов рекурсии класса были доступны.
К сожалению, определение методов в производных классах скрывает методы в базовых классах.
Чтобы скрыть унаследованные методы, вы должны явно добавить пару using
using BasicValidationPolicy<Rest...>::Set;
using BasicValidationPolicy<Rest...>::RegisterSetHandler;
На данный момент код не компилируется, потому что вам нужны Set()
и RegisterSetHandler()
в классе основного случая. Вас объявили пустышкой RegisterSetHandler()
, но не пустышкой Set()
. Вы должны добавить один, чтобы основной случай стал
template <>
class BasicValidationPolicy<>
{
public:
void RegisterSetHandler();
void Set();
};
Теперь ваш объект MyClass<Policy>
предоставляет два метода RegisterSetHandler()
(до одного): один получает std::function<void(int, std::int64_t)>
, другой (до того, как скрытый) получает std::function<void(int, double)>
.
Но когда вы передаете лямбду, возникает проблема курицы и яйца: лямбда может быть преобразована в std::function
, но не является std::function
. Поэтому его нельзя использовать для вывода параметров шаблона std::function
, потому что типы должны быть известны до их вывода.
Возможное решение - наложить преобразование лямбда/std::function
в вызов
// ..........................VVVVVVVVVVVVVV
m.Policy::RegisterSetHandler(std::function{[](int i, double value) {
// by this point value is an int64_t
std::cout << "Got double " << i << ", " << value << "\n";
}});
// ...........................^
используя также руководства по дедукции шаблонов, представленные в C++17.
Таким образом, ваш код становится
#include <iostream>
#include <functional>
template <typename... T>
class BasicValidationPolicy { };
template <>
class BasicValidationPolicy<>
{
public:
void RegisterSetHandler();
void Set();
};
template <typename T, typename... Rest>
class BasicValidationPolicy<T, Rest...> : public BasicValidationPolicy<Rest...>
{
public:
using SetHandler = std::function<void(int, T)>;
using BasicValidationPolicy<Rest...>::Set;
using BasicValidationPolicy<Rest...>::RegisterSetHandler;
void RegisterSetHandler(const SetHandler& handler)
{
m_setHandler = handler;
}
void Set(int n, const T& val) {
if (m_setHandler) {
m_setHandler(n, val);
}
}
private:
SetHandler m_setHandler{nullptr};
};
template <typename ValidatorPolicy>
class MyClass : public ValidatorPolicy {
public:
void OnSetInt(int n, int64_t v)
{
ValidatorPolicy::Set(n, v);
}
void OnSetDouble(int n, double d)
{
ValidatorPolicy::Set(n, d);
}
};
int main ()
{
using Policy = BasicValidationPolicy<int64_t, double>; // doesn't work
MyClass<Policy> m;
m.Policy::RegisterSetHandler(std::function{[](int i, double value) {
std::cout << "Got double " << i << ", " << value << "\n";
}});
double d{35.2135};
m.OnSetDouble(1, d);
}
@kiss-o-matic - «Похоже, что лямбды иногда бывают ложными богами ...» - Ламбды чрезвычайно полезны. Просто учтите, что у каждого инструмента есть свой предел.
Действительно. Я использую их часто... просто нужно помнить, что они не панацея.
Есть небольшая альтернатива рекурсивному определению, с которой может быть проще работать...
template<typename T>
class ValidationPolicy {
// Set/Register/etc
};
template <typename... Ts>
class BasicValidationPolicy : public ValidationPolicy<Ts>... {
public:
using ValidationPolicy<Ts>::Set...;
using ValidationPolicy<Ts>::RegisterSetHandler...;
};
Это может иметь некоторое влияние на время компиляции и другие аспекты разработки, хотя, скорее всего, относительно незначительное. Например, если у вас есть десятки классов, используемых в сотнях различных комбинаций политик в вашем приложении, рекурсивное определение приведет к тому, что для его поддержки будет гораздо больше различных типов и более крупных двоичных файлов. Например, в рекурсивном определении BasicValidationPolicy<T1, T2, T3>
и BasicValidationPolicy<T3, T2, T1>
использование будет генерировать 7 различных типов в иерархии (пустой используется в обоих расширениях). То же самое в более плоской иерархии будет 5 различных типов — по одному для каждого из T1, T2, T3
и по одному для каждой комбинации. Добавление BasicValidationPolicy<T2, T3, T1>
рекурсивно добавит еще 3 типа, но еще 1 тип в плоской форме.
Ответ от @ max66 не является неправильным, просто есть над чем подумать.
Действительно. Можете проверить это, но время компиляции действительно является серьезной проблемой с моей текущей кодовой базой (о чем я регулярно информируюсь в MR). Я сомневаюсь, что это подтолкнет нас к краю, но есть над чем подумать.
@kiss-o-matic - это решение более элегантное, понятное и удобное в сопровождении (ИМХО), чем рекурсивное. Требовать С++ 17, потому что вариативное using
, но вы пометили С++ 17, так что, я полагаю, это не проблема.
Так что я на самом деле немного пережевал это, и я разогреваюсь. Это немного более элегантно, и я могу немного больше рассуждать об этом. Мне было немного сложно понять, как используется многоточие (как для использования, так и для наследуемой части). Запустил его через понимание cpp, и это немного яснее. И да, я компилирую с C++ 17, так что все хорошо. Я знаю, что мне нужно продолжать работать над магией шаблонов, но я всегда думаю, что концепции сведут на нет необходимость такой тяжелой работы.
Rockstar! Я делаю все возможное, чтобы не отставать от этой ошибки, там так много чертовых деталей. Когда-нибудь я пройдусь по книге Николаиса, но боюсь, что большая ее часть не приживется. Похоже, что лямбда-выражения иногда бывают ложными богами... Я знаю, что они не совпадают с std::function, но я бы никогда не подумал о передаче std::function таким образом... теперь это имеет смысл. В любом случае, спасибо!