Search code examples
c++templatesc++11functor

Restrict functor parameter type and constness


I am trying to implement a resource protection class which would combine data along with a shared mutex (actually, QReadWriteLock, but it's similar). The class must provide the method to apply a user-defined function to the data when the lock is acquired. I would like this apply method to work differently depending on the function parameter (reference, const reference, or value). For example, when the user passes a function like int (const DataType &) it shouldn't block exclusively as we are just reading the data and, conversely, when the function has the signature like void (DataType &) that implies data modification, hence the exclusive lock is needed.

My first attempt was to use std::function:

template <typename T>
class Resource1
{
public:
    template <typename Result>
    Result apply(std::function<Result(T &)> &&f)
    {
        QWriteLocker locker(&this->lock);   // acquire exclusive lock
        return std::forward<std::function<Result(T &)>>(f)(this->data);
    }

    template <typename Result>
    Result apply(std::function<Result(const T &)> &&f) const
    {
        QReadLocker locker(&this->lock);    // acquire shared lock
        return std::forward<std::function<Result (const T &)>>(f)(this->data);
    }

private:
    T data;
    mutable QReadWriteLock lock;
};

But std::function doesn't seem to restrict parameter constness, so std::function<void (int &)> can easily accept void (const int &), which is not what I want. Also in this case it can't deduce lambda's result type, so I have to specify it manually:

Resource1<QList<int>> resource1;
resource1.apply<void>([](QList<int> &lst) { lst.append(11); });     // calls non-const version (ok)
resource1.apply<int>([](const QList<int> &lst) -> int { return lst.size(); });  // also calls non-const version (wrong)

My second attempt was to use std::result_of and return type SFINAE:

template <typename T>
class Resource2
{
public:
    template <typename F>
    typename std::result_of<F (T &)>::type apply(F &&f)
    {
        QWriteLocker locker(&this->lock);   // lock exclusively
        return std::forward<F>(f)(this->data);
    }

    template <typename F>
    typename std::result_of<F (const T &)>::type apply(F &&f) const
    {
        QReadLocker locker(&this->lock);    // lock non-exclusively
        return std::forward<F>(f)(this->data);
    }

private:
    T data;
    mutable QReadWriteLock lock;
};

Resource2<QList<int>> resource2;
resource2.apply([](QList<int> &lst) {lst.append(12); });    // calls non-const version (ok)
resource2.apply([](const QList<int> &lst) { return lst.size(); });  // also calls non-const version (wrong)

Mainly the same thing happens: as long as the object is non-const the mutable version of apply gets called and result_of doesn't restrict anything.

Is there any way to achieve this?


Solution

  • You may do the following

    template <std::size_t N>
    struct overload_priority : overload_priority<N - 1> {};
    
    template <> struct overload_priority<0> {};
    
    using low_priority = overload_priority<0>;
    using high_priority = overload_priority<1>;
    
    template <typename T>
    class Resource
    {
    public:
        template <typename F>
        auto apply(F&& f) const
        // -> decltype(apply_impl(std::forward<F>(f), high_priority{}))
        {
            return apply_impl(std::forward<F>(f), high_priority{});
        }
    
        template <typename F>
        auto apply(F&& f)
        // -> decltype(apply_impl(std::forward<F>(f), high_priority{}))
        {
            return apply_impl(std::forward<F>(f), high_priority{});
        }
    
    private:
        template <typename F>
        auto apply_impl(F&& f, low_priority) -> decltype(f(std::declval<T&>()))
        {
            std::cout << "ReadLock\n";
            return std::forward<F>(f)(this->data);
        }
    
        template <typename F>
        auto apply_impl(F&& f, high_priority) -> decltype(f(std::declval<const T&>())) const
        {
            std::cout << "WriteLock\n";
            return std::forward<F>(f)(this->data);
        }
    
    private:
        T data;
    };
    

    Demo