Home > Enterprise >  How to implement an 'Observer' class, using std::invoke for redirecting function calls?
How to implement an 'Observer' class, using std::invoke for redirecting function calls?

Time:02-03

so I'm trying to design a class Observed<T>, which stores a T and bool, with the boolean value indicating whether the T member variable has been updated since it was last set. To demonstrate, my initial implementation was something like

    template <std::copyable T>
    class Observed
    {
    public:
        Observed() 
            : m_data{}, m_updated{false} {};
        Observed(const T &data) 
            : m_data{data}, m_updated{true} {};

        // .. move, copy, assignment ctors

        // setting the data leaves it 'updated'
        template <typename U>
            requires(std::same_as<T, U>)
        auto write(U &&value) -> void
        {
            m_data = std::forward<U>(value);
            m_updated = true;
        }

        // to test if it has since been modified (i.e. written without reading)
        auto modified() -> bool
        {
            return m_updated;
        }

        // reading the value means it has no longer been updated
        auto value() -> T &
        {
            m_updated = false;
            return m_data;
        }

    private:
        T m_data;
        bool m_updated;
    };

However, the problem became that for an example Observed<std::vector<int>> v, if I needed to add an element to the vector i'd call v.value().push_back(...), which would surpass the write() function, and subsequently m_updated would not be set correctly.

My slightly janky approach was to therefore introduce a call() function as follows:

    template <std::copyable T>
    class Observed
    {
    public:
        // ctors
        Observed() 
            : m_data{}, m_updated{false} {};
        Observed(const T &data) 
            : m_data{data}, m_updated{true} {};
        
        // the new 'call' function
        template<typename Func, typename... Ts>
        auto call(Func&& func, Ts... args) -> std::invoke_result_t<Func, Ts...>
        {
            m_updated = true;
            return std::invoke(func, m_data, args...);
        }

        // .. rest of class

    private:
        T m_data;
        bool m_updated;
    };

so that you'd be able to write v.call(&std::vector<int>::push_back, 1).

However, doing so doesn't exactly work, and debugging it is a bit of a nightmare. I just wondered whether anyone could give it a look over and help me get the correct functionality. I hope my code above has shown enough of my intent with the class.

I'm also a little unclear on how you would form the function pointer to std::vector<T>::push_back given it takes an allocator as a second template argument.

Thanks.

CodePudding user response:

What about two value() methods?

A const one, only for reading

    T const & read_value() const
    {
        m_updated = false;
        return m_data;
    }

and a second one, to modify the content

    T & write_value()
    {
        m_updated = true;
        return m_data;
    }

CodePudding user response:

I have found a solution! (I think!)

So the first problem is that given vector<T>::push_back is overloaded, when we pass the function point to call we must cast it to the exact function pointer type we want i.e.

void (std::vector<int>::*f_ptr)(const int&) = &std::vector<int>::push_back;

and then we can implement the call() function as follows:

template<typename Ret, typename... Args, typename... UArgs>
auto call(Ret (T::*func)(Args...), UArgs... args) 
    -> std::invoke_result_t<Ret(T::*)(Args...), T, UArgs...>
{
    m_updated = true;
    return std::invoke(func, m_data, args...);
}

However, this then has the problem that if T of the original class is not a class itself, say T = int then T::* leads to a compilation error. If anyone has thoughts on how to resolve this, feel free to suggest some. SFINAE?

Edit: found a solution - template the function pointer on U

// if you want to update the value via a class method of T
template<typename U, typename Ret, typename... Args, typename... UArgs, 
   std::enable_if<!std::is_integral<U>::value, bool>::type = true
>
auto call(Ret (U::*func)(Args...), UArgs... args) 
    -> std::invoke_result_t<Ret(U::*)(Args...),  U, UArgs...>
{
    return std::invoke(func, m_data, args...);
}

And then an improved version using concepts:

template<typename U, typename Ret, typename... Args, typename... UArgs>
requires(std::same_as<T, U> && !std::is_integral_v<U>)
auto call(Ret (U::*func)(Args...), UArgs... args) 
    -> std::invoke_result_t<Ret(U::*)(Args...),  U, UArgs...>
{
    return std::invoke(func, m_data, args...);
}

https://godbolt.org/z/98ffE39Te

  • Related