Отображение типов параметров в функции безупречной пересылки, избегая повторения кода
У меня есть раздражающий сценарий, когда мне нужно отложить инициализацию какого-либо state
объекта и позволить пользователю построить его по требованию. Например
// user code
context c;
// ...do something...
c.initialize_state(a, b, c);
// library code
class context
{
private:
class state
{
state(A a, B b, C c);
state(const state&) = delete;
state(state&&) = delete;
};
std::optional<state> _state; // or 'boost::optional'
public:
template <typename... Xs>
void initialize_state(Xs&&... xs)
{
_state.emplace(std::forward<Xs>(xs)...);
}
};
Как видно из приведенного выше кода, интерфейс context::initialize_state
сообщает пользователю ничего о том, как инициализировать context::_state
. Пользователь вынужден посмотреть на реализацию initialize_state
а затем посмотреть state::state
чтобы понять, что должно быть передано initialize_state
.
Я могу изменить initialize_state
на...
void initialize_state(A&& a, B&& b, C&& c)
{
_state.emplace(std::move(a), std::move(b), std::move(c));
}
... но у этого есть главный недостаток: существует дублирование кода с state::state
, которое должно поддерживаться вручную в случае изменения типов аргументов.
Есть ли способ получить лучшее из обоих миров (DRY и удобный интерфейс)? Обратите внимание, что state
не перемещается и не копируется.
Ответы
Ответ 1
Класс state
не может быть копируемым/подвижным, но оказалось, что, A
B
и C
являются. (Таким образом, я предполагаю, что есть некоторые другие внутренние данные в state
которые предотвращают возможность копирования/перемещения)
Вы можете вытащить этих членов в другой класс, который может быть введен в state
. Из-за отсутствия лучшего имени я назову его state_args
:
struct state_args
{
explicit state_args(A a, B b, C c);
A a_;
B b_;
C c_;
};
Это позволяет:
class context
{
private:
class state
{
state(state_args args);
state(const state&) = delete;
state(state&&) = delete;
};
std::optional<state> _state; // or 'boost::optional'
public:
template<class STATE_ARGS, /*enable_if to ensure STATE_ARGS is indeed state_args*/>
void initialize_state(STATE_ARGS&& internal_state)
{
_state.emplace(std::forward<STATE_ARGS>(internal_state));
}
};
Ответ 2
Вероятно, создавая больше проблем, чем решение, вы можете создать шаблон своего класса:
template <typename ... Ts>
class context_impl
{
private:
class state
{
state(Ts...);
state(const state&) = delete;
state(state&&) = delete;
};
std::optional<state> _state; // or 'boost::optional'
public:
void initialize_state(Ts&&... xs)
{
_state.emplace(std::forward<Xs>(xs)...);
}
};
using context = context_impl<A, B, C>;
Поскольку шаблон фиксируется классом, void initialize_state(Ts&&... xs)
имеет фиксированную подпись (например, intellisense может отображать ожидаемые аргументы).
Ответ 3
но это имеет большой недостаток: существует дублирование кода с состоянием :: state, которое необходимо поддерживать вручную в случае изменения типов аргументов.
Это общая проблема с инкапсулированием. Это (определение) не СУХОЙ.
Существует способ сохранить связь между перегрузками конструктора state
и интерфейсом initialize_state
, который должен использовать enable_if
вместе с is_constructible
.
class context
{
private:
class state
{
public:
state(A a, B b, C c);
state(const state&) = delete;
state(state&&) = delete;
};
std::optional<state> _state; // or 'boost::optional'
public:
template <typename... Xs>
auto
initialize_state(Xs&&... xs)
->
std::enable_if_t
<
// condition
std::is_constructible<state, Xs...>::value,
// return type
void
>
{
_state.emplace(std::forward<Xs>(xs)...);
}
};