I have an annoying scenario where I need to defer the initialization of some object state
and allow the user to construct one on demand. E.g.
// 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)...);
}
};
As you can see from the code above, the interface of context::initialize_state
tells the user nothing about how to initialize context::_state
. The user is forced to look at the implementation of initialize_state
and then look at state::state
to understand what should be passed to initialize_state
.
I could change initialize_state
to...
void initialize_state(A&& a, B&& b, C&& c)
{
_state.emplace(std::move(a), std::move(b), std::move(c));
}
...but this has a major drawback: there is code duplication with state::state
, that needs to be manually maintained in case the argument types change.
Is there any way I can get the best of both worlds (DRY and user-friendly interface)? Note that state
is not movable/copyable.
but this has a major drawback: there is code duplication with state::state, that needs to be manually maintained in case the argument types change.
This is a general problem with encapsulation. It's (definition) not DRY.
There is a way to preserve the relationship between the state
constructor overloads and the interface of initialize_state
, which is to use enable_if
along with the is_constructible
type trait.
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)...);
}
};
The class state
may not be copyable/movable, but it appears that A
, B
and C
are. (So I'm assuming there is some other, internal data in state
that prevents copyability/movability)
You can pull these members out into another class that can be injected into state
. For lack of a better name, I'll call it state_args
:
struct state_args
{
explicit state_args(A a, B b, C c);
A a_;
B b_;
C c_;
};
Which enables the following:
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));
}
};
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With