Search code examples
c++c++11variadic-templatesdryperfect-forwarding

Exposing parameter types in a perfectly-forwarding function avoiding code repetition


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.


Solution

  • 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)...);
        }
    };