mentalmushroom mentalmushroom - 2 months ago 9
C++ Question

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?

Answer

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

Comments