When I go to return a std::lock_guard
in a std::pair
from a function I get horrible errors. But when I package it in a class I have no problems (compiles and works as expected). I can't understand why. Details follow:
I devised a small template class to enable convenient locking and unlocking of shared objects. It's not particularly innovative, but C++17 allows it to be very compact and code read/write friendly:
template <typename T> class Locked {
public:
Locked(T& _object, std::mutex& _mutex)
: object(_object)
, lock(_mutex)
{
}
T& object;
std::lock_guard<std::mutex> lock;
};
template <typename T> class Lockable {
public:
Locked<T> borrow() { return Locked(object, mutex); }
Locked<const T> borrow() const { return Locked(object, mutex); }
private:
T object;
mutable std::mutex mutex;
};
It can be used like:
int main()
{
Lockable<std::vector<int>> lv;
auto [vec, lock] = lv.borrow();
std::cout << vec.size() << std::endl;
}
My question is this. The Locked
class is very thin. I thought I could use std::pair
instead of a formal class, like so:
#include <iostream>
#include <mutex>
#include <utility>
#include <vector>
template <typename T> class Lockable {
public:
std::pair<T&, std::lock_guard<std::mutex>> borrow()
{ return std::pair(object, std::lock_guard<std::mutex>(mutex)); }
std::pair<const T&, std::lock_guard<std::mutex>> borrow() const
{ return std::pair(object, std::lock_guard<std::mutex>(mutex)); }
private:
T object;
mutable std::mutex mutex;
};
int main()
{
Lockable<std::vector<int>> lv;
auto [vec, lock] = lv.borrow();
std::cout << vec.size() << std::endl;
}
But this throws up horrible, hard to parse errors. I think this has to do with std::lock_guard
not being movable, but to me, it looks exactly like my working code. Why does this second version not work?
With some massaging Lockable
does compile:
template <typename T>
class Lockable {
public:
auto borrow() { return std::pair< T&, std::lock_guard<std::mutex>>{object, mutex}; }
auto borrow() const { return std::pair<const T&, std::lock_guard<std::mutex>>{object, mutex}; }
private:
T object;
mutable std::mutex mutex;
};
Live example.
The idea is to explicitly specify std::lock_guard
as a template argument in std::pair
, but feed mutex
as the corresponding constructor argument (indeed, the second version doesn't work because std::lock_guard
isn't movable). Overload (3) of std::pair::pair
will be used in this case.
(Also, since it is C++17, I would suggest to use std::scoped_lock
instead of std::lock_guard
).
Why does this second version not work?
Among the many overloads for constructing std::pair
, your code cannot resolve to any specific one. Now, besides Dev Null's correct and straight-forward solution here, I'm leaving this for further reference: You can forward-construct your std::lock_guard
alongside passing your T&
the way you want to by using the piecewise_construct_t
version of std::pair()
's constructor:
template <typename T> class Lockable {
public:
auto borrow()
{
return std::pair<T&, std::lock_guard<std::mutex>>(
std::piecewise_construct,
std::forward_as_tuple(object), std::forward_as_tuple(mutex));
}
auto borrow() const
{
return std::pair<T&, std::lock_guard<std::mutex>>(
std::piecewise_construct,
std::forward_as_tuple(object), std::forward_as_tuple(mutex));
}
private:
T object;
mutable std::mutex mutex;
};
Note: In addition, I changed return type of borrow()
to auto
, as we're being enough explicit inside of it in terms of what is being returned.
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