8

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?

Dev Null
  • 4,731
  • 1
  • 30
  • 46
Kaushik Ghose
  • 1,024
  • 12
  • 16

3 Answers3

8

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).

Dev Null
  • 4,731
  • 1
  • 30
  • 46
2

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.

Geezer
  • 5,600
  • 18
  • 31
1

The significant difference is that in the first case, you pass the mutex, and let the result-type construct the std::lock_guard<std::mutex> on its own, while in the second, you construct it yourself and then let the result-type try to move-construct it.
The latter cannot work!

Luckily, the fix is trivial, just pass the mutex directly.

As an aside, consider investing a bit more into auto to reduce the noise.

Deduplicator
  • 44,692
  • 7
  • 66
  • 118