Я пытаюсь предоставить интерфейс — через файл конфигурации — для того, чтобы мои пользователи могли выбирать дистрибутив для некоторых параметров, которые они используют. Для этой цели я хотел бы использовать алгоритмы генератора случайных чисел STL.
Предположим, что моя программа читает JSON из командной строки. Для приведенного ниже JSON программа должна понимать, что она должна генерировать случайное число из нормального распределения с заданным средним значением и стандартной вариацией. (Я использую те же имена параметров, что и библиотека STL для очистки.)
{
"dist": "normal_distribution",
"mean": 0.1,
"stddev": 0.5
}
Пока что я могу легко разобрать JSON и использовать param_type
каждого дистрибутива для инициализации дистрибутива. Я использую имя, чтобы решить, какой дистрибутив выбрать param_type
и дистрибутив.
Чего я не знаю, так это как это красиво реализовать. Я знаю, что я должен предоставить какой-то фабричный метод для этого, передать JSON и выплюнуть функцию или класс. Если я хочу вернуть экземпляр класса, скажем, unique_ptr
генератора, мне нужно определить абстрактный класс, например, RandDist
, и написать какой-то адаптер для включения моего ввода, .... Обычно я этого не делаю. нужно много от класса, достаточно только метода gen()
.
Мне интересно, есть ли у кого мысли по этому поводу. Или, если кто-нибудь знает библиотеку, которая может это сделать.
P.S. Входные данные не обязательно должны быть объектом JSON, любая хэш-таблица будет работать сама по себе.
@Quentin Я думаю, что мне не очень нравится мой подход, так это количество адаптеров, которые мне нужно написать для всего алгоритма. Я почему-то надеюсь, что есть более приятный способ добиться этого.
Вы описали довольно стандартный способ справиться с этой ситуацией — иметь абстрактный RandomGenerator
класс всего с одним виртуальным методом gen()
.
Затем у него будут реализации, такие как NormalDistributionGenerator
, UniformDistributionGenerator
и т. д., с конструкторами, принимающими соответствующий набор параметров распределения и инициализирующими элементы STL в качестве членов.
Эти конкретные классы будут использоваться непосредственно только в процедуре создания генератора и использоваться в других местах как абстрактные RandomGenerator
.
Итак, процедура создания будет выглядеть примерно так
std::unique_ptr<RandomGenerator> CreateRandomGenerator(const Info& info) {
switch (info.type) {
case Type::Normal:
return std::make_unique<NormalDistributionGenerator>(info.mean(), info.stddev());
case Type::Uniform:
return std::make_unique<UniformDistributionGenerator>(info.a(), info.b());
// ...
}
}
Info
- это класс, который содержит информацию о распространении (некоторая оболочка JSON, map/hash_table - что лучше работает в вашем случае).
Таким образом, вам определенно нужно будет написать шаблонный код немного, чтобы он заработал, но это сделает использование вашего RandomGenerator
простым и понятным, а добавление новых типов дистрибутивов будет достаточно простым и потребует модификации кода только в одном месте - фабричный метод.
Я постарался свести шаблон к минимуму. Предположения:
Вы заранее знаете тип вашего генератора (это легко переключается, если вам нужно, чтобы ваши генераторы также были динамическими)
Все дистрибутивы генерируют double
s (это в значительной степени встроено, поскольку API должен возвращать бетон что-то, чтобы его можно было прилично использовать)
Все дистрибутивы создаются из параметров double
(это также можно настроить с помощью прокси-объекта, но в зависимости от вашей фактической библиотеки JSON работа может быть уже выполнена там)
Я использовал расширение препроцессора GCC для обработки случая с нулевым параметром, но макрос, безусловно, можно переписать, чтобы он не нуждался в нем.
using Generator = std::mt19937;
using Distribution = std::function<double(Generator &)>;
using Json = std::map<std::string, std::string>;
template <class DistributionType, class... Parameters>
Distribution make_distribution_impl(Json const &json, Parameters... parameters) {
return DistributionType{std::stod(json.at(parameters))...};
}
Distribution make_distribution(Json const &json) {
auto const &distributionName = json.at("dist");
#define generate_distribution_factory(name_, ...) \
if (distributionName == #name_) \
return make_distribution_impl<std::name_<double>>(json, ## __VA_ARGS__)
generate_distribution_factory(uniform_real_distribution, "a", "b");
generate_distribution_factory(normal_distribution, "mean", "stddev");
// ...
#undef generate_distribution_factory
throw std::runtime_error{"Unknown distribution " + distributionName};
}
Смотрите в прямом эфире на Wandbox
Это выглядит потрясающе! Я думаю, что это то, что я искал, но я должен признать, что у меня есть проблема с пониманием этого. Я думаю, что по большей части я понимаю логику этого, но у меня есть вопрос. Некоторые дистрибутивы, например std::piecewise_linear_distribution
, используют контейнер в качестве параметров, интересно, как вы справляетесь с этим в своем подходе. Я использую nlohmann-json, чтобы справиться с этим. Моя идея состояла в том, чтобы написать парсер для param_type каждого дистрибутива и прочитать параметры, а затем передать их. Но я не понимаю, как это сделать здесь. Спасибо :-)
Я думаю, что удаление std::stod
подойдет, так как json-библиотека nlohmann использует тот же синтаксис и выполняет преобразование в типы stl, ... но я спрашиваю, так как я не полностью понимаю код для части generate_dis....
.
@ Амир ... черт возьми! То, что вы используете nlohmann-json, — это отличная новость, так как в нем настроена автоматическая конвертация — вы можете просто удалить std::stod()
из make_distribution_impl
, и в простых случаях сработают волшебные преобразования. std::piecewise_linear_distribution
это своего рода рутинная работа, так как для этого нужны итераторы... Самое простое решение — просто не объявлять его через макрос, а добавить отдельную ветку для его обработки внутри make_distribution
.
Я слежу, но не совсем честно. Я понимаю, что делает макрос, и понимаю, как его расширить, но я не знаю, что мне нужно для поддержки внешнего std::piecewise_linear_distribution
:\
Должно быть не более if (distributionName == "piecewise_linear_distribution ") { /* read in values, handle errors */ return std::piecewise_linear_distribution<double>{ /* what you just parsed */ }; }
Я попробую посмотреть, смогу ли я заставить его работать :-) Думаю, я вижу, что мне следует делать. Большое спасибо...
Я думаю, что понял, но не буду врать, у меня голова болит после прочтения и понимания вашего кода. Для меня это был другой уровень :D Но теперь у меня есть причина изучать эти вещи больше.
Мне удалось получить все раздачи, кроме bernoulli_distribution
и discrete_distribution
! Я изменил макрос на этот, #define generate_distribution_factory(name_, type_, ...)\ if (distributionName == #name_) return make_distribution_impl<std::name_<type_>>(j, ## __VA_ARGS__);
чтобы я мог указать тип и быть более гибким. Однако по какой-то причине это не помогает bool
! Интересно, есть ли у вас какие-либо представления об этом?
Ах, bernoulli_distribution
- единственная раздача, которая не является шаблонной... Что за тур! Я заставил это работать, подняв пару <>
в аргумент макроса: #define generate_distribution_factory(name_, suffix_, ...) if (distributionName == #name_) return make_distribution_impl<std::name_ suffix_>(json, ## __VA_ARGS__)
, называемый generate_distribution_factory(bernoulli_distribution, /* empty */, "p");
; но вы также можете просто указать это в особом случае. Я не уверен, как вы застряли с discrete_distribution
и о каком bool
вы говорите.
Мне кажется, вы довольно ясно представляете, в каком направлении вам следует двигаться. В качестве пищи для размышлений интерфейс с одной виртуальной функцией можно рассматривать как функтор, поэтому здесь может пригодиться функция стирания типов
std::function
.