C++ Logo

std-proposals

Advanced search

[std-proposals] Helper class to make safe a thread-unsafe class

From: Frederick Virchanza Gotham <cauldwell.thomas_at_[hidden]>
Date: Wed, 28 Jun 2023 23:37:30 +0100
A few months ago I was thinking of submitting this to Boost, but now
I'm actually thinking maybe it should be in the Standard Library.

I'm not the first person to come up with a class like this, but my own
implementation is one of very few full implementations I've been able
to find online.

You define a global object as follows:

    Reservable< std::vector<int> > g_vec;

>From this point forward, multiple threads can access the object safely
as follows:

    g_vec->push_back(42); // This is thread-safe

Or you can do:

    auto mylock = g->vec.Reserve();
    mylock->push_back(44);
    mylock->push_back(47);

Or if you want an L-value reference:

    auto mylock = g->vec.Reserve();
    auto &vec = *mylock;
    vec.push_back(44);
    vec.push_back(47);

Here's my code. It works from C++11 onwards, and with newer compilers
it takes advantage of guaranteed return value optimisation, function
attributes and concepts:

        https://godbolt.org/z/eeG6s3d18

And here it is copy-pasted:

#ifndef HEADER_INCLUSION_GUARD_RESERVATIONS_MULTITHREADED
#define HEADER_INCLUSION_GUARD_RESERVATIONS_MULTITHREADED

#ifndef __cplusplus
# error "This header file must be included in a C++ source file --
not a C or Objective C source file"
# include "no_such_header_file_see_previous_error"
#elif __cplusplus < 201100L
# error "This header file requires a compiler that supports at
least the 2011 C++ Standard for multithreading"
# include "no_such_header_file_see_previous_error"
#endif

#include <mutex> // recursive_mutex, lock_guard, unique_lock
#include <utility> // move, forward, declval
#include <type_traits> // is_pointer, is_reference, conditional,
is_same, remove_cv, remove_ref, remove_cvref, remove_pointer
#include <memory> // addressof

#if (__cplusplus >= 201700L) || (defined(__has_cpp_attribute) &&
__has_cpp_attribute(nodiscard))
# define attrib_nodiscard [[nodiscard]]
#else
# define attrib_nodiscard /* nothing */
#endif

#if (__cplusplus >= 201907L) && defined(__cpp_concepts)

template<typename T>
concept BasicLockable = std::is_same_v< T, std::remove_cvref_t<T> > && requires
  {
    std::declval<T>().lock();
    std::declval<T>().unlock();
  };

template<typename T, BasicLockable MutexType = std::recursive_mutex>
requires (!std::is_reference_v<T>)
class Reserved final {

    Reserved(void) = delete;
    Reserved(Reserved const &) = delete;
    Reserved(Reserved &&) = delete;
    Reserved &operator=(Reserved const &) = delete;
    Reserved &operator=(Reserved &&) = delete;
    Reserved const volatile *operator&(void) const volatile = delete;
    template<typename U> void operator,(U&&) = delete;

    T &m_ref;
    std::lock_guard<MutexType> m_locker;

public:
    // Constructor might throw if argM.lock() throws std::system_error
    Reserved(MutexType &argM, T &argR) noexcept( noexcept(argM.lock()) )
      : m_ref(argR), m_locker(argM) {}

    attrib_nodiscard auto operator->(void) noexcept
    {
        if constexpr ( std::is_pointer_v<T> )
            return m_ref;
        else
            return std::addressof(m_ref);
    }

    attrib_nodiscard auto operator->(void) const noexcept
    {
        // Don't dereference a nullptr inside this function! (decltype is ok)
        if constexpr ( std::is_pointer_v<T> )
            return const_cast<decltype(*m_ref) const *>(m_ref);
        else
            return std::addressof(const_cast<T const &>(m_ref));
    }

    attrib_nodiscard auto &operator*(void) noexcept
    {
        if constexpr ( std::is_pointer_v<T> )
            return *m_ref;
        else
            return m_ref;
    }

    attrib_nodiscard auto const &operator*(void) const noexcept
    {
        if constexpr ( std::is_pointer_v<T> )
            return *m_ref;
        else
            return m_ref;
    }
};

#elif (__cplusplus >= 201700L) || defined(__cpp_guaranteed_copy_elision)

template<typename T, class MutexType = std::recursive_mutex>
class Reserved final {

    static_assert( !std::is_reference_v<T>, "T cannot be a reference" );
    static_assert(
        std::is_same_v< MutexType, std::remove_cv_t<
std::remove_reference_t<MutexType> > >,
        "MutexType must be a bare type (no cvref)" );

    Reserved(void) = delete;
    Reserved(Reserved const &) = delete;
    Reserved(Reserved &&) = delete; // not needed because of
guaranteed return value optimisation
    Reserved &operator=(Reserved const &) = delete;
    Reserved &operator=(Reserved &&) = delete;
    Reserved const volatile *operator&(void) const volatile = delete;
    template<typename U> void operator,(U&&) = delete;

    T &m_ref;
    std::lock_guard<MutexType> m_locker;

public:
    // Constructor might throw if argM.lock() throws std::system_error
    Reserved(MutexType &argM, T &argR) noexcept( noexcept(argM.lock()) )
      : m_ref(argR), m_locker(argM) {}

    attrib_nodiscard auto operator->(void) noexcept
    {
        if constexpr ( std::is_pointer_v<T> )
            return m_ref;
        else
            return std::addressof(m_ref);
    }

    attrib_nodiscard auto operator->(void) const noexcept
    {
        // Don't dereference a nullptr inside this function! (decltype is ok)
        if constexpr ( std::is_pointer_v<T> )
            return const_cast<decltype(*m_ref) const *>(m_ref);
        else
            return std::addressof(const_cast<T const &>(m_ref));
    }

    attrib_nodiscard auto &operator*(void) noexcept
    {
        if constexpr ( std::is_pointer_v<T> )
            return *m_ref;
        else
            return m_ref;
    }

    attrib_nodiscard auto const &operator*(void) const noexcept
    {
        if constexpr ( std::is_pointer_v<T> )
            return *m_ref;
        else
            return m_ref;
    }
};

#else // just for C++11

template<typename T, class MutexType = std::recursive_mutex>
class Reserved final {

    static_assert( !std::is_reference<T>::value, "T cannot be a reference" );
    static_assert(
        std::is_same<
            MutexType,
            typename std::remove_cv<typename
std::remove_reference<MutexType>::type>::type
>::value , "MutexType must be a bare type (no cvref)" );

    Reserved(void) = delete;
    Reserved(Reserved const &) = delete;
    //Reserved(Reserved &&) = delete; - see below
    Reserved &operator=(Reserved const &) = delete;
    Reserved &operator=(Reserved &&) = delete;
    Reserved const volatile *operator&(void) const volatile = delete;
    template<typename U> void operator,(U&&) = delete;

    T &m_ref;

    std::unique_lock<MutexType> m_locker; // unique_lock is movable

public:
    // Constructor might throw if argM.lock() throws std::system_error
    Reserved(MutexType &argM, T &argR) noexcept( noexcept(argM.lock()) )
      : m_ref(argR), m_locker(argM) {}

    // C++11 doesn't have guaranteed return value optimisation, and so
    // the 'move constructor' on the next line must be accessible (even
    // if it is elided by the compiler). The move-constructor of
unique_lock is noexcept.
    Reserved(Reserved &&arg) noexcept : m_ref(arg.m_ref), m_locker(
std::move(arg.m_locker) ) {}

    template<bool is_pointer, typename U> class Pointer;

    template<typename U>
    struct Pointer<true , U> {
        static U addr(U &arg) noexcept { return arg; }
        static typename std::remove_pointer<U>::type &obj(U &arg)
noexcept { return *arg; }
    };

    template<typename U>
    struct Pointer<false, U> {
        static U *addr(U &arg) noexcept { return std::addressof(arg); }
        static U &obj (U &arg) noexcept { return arg; }
    };

    typename std::conditional<std::is_pointer<T>::value, T, T*>::type
operator->(void) noexcept
    {
        return Pointer<std::is_pointer<T>::value, T>::addr(m_ref);
    }

    typename std::conditional<std::is_pointer<T>::value, T, T*>::type
operator->(void) const noexcept
    {
        return Pointer<std::is_pointer<T>::value, T>::addr(
const_cast<T const &>(m_ref) );
    }

    typename std::conditional<std::is_pointer<T>::value, typename
std::remove_pointer<T>::type, T>::type
    &operator*(void) noexcept
    {
         return Pointer<std::is_pointer<T>::value, T>::obj(m_ref);
    }

    typename std::conditional<std::is_pointer<T>::value, typename
std::remove_pointer<T>::type, T>::type
    const &operator*(void) const noexcept
    {
         return Pointer<std::is_pointer<T>::value, T>::obj(m_ref);
    }
};

#endif

#if (__cplusplus >= 201907L) && defined(__cpp_concepts)

template<typename T, BasicLockable MutexType = std::recursive_mutex>
class Reservable final {

    // Reservable(void) = delete; - see public constructor below
    Reservable(Reservable const &) = delete;
    Reservable(Reservable &&) = delete;
    Reservable &operator=(Reservable const &) = delete;
    Reservable &operator=(Reservable &&) = delete;
    Reservable const volatile *operator&(void) const volatile = delete;
    template<typename U> void operator,(U&&) = delete;

    MutexType mutable m_lock; // constructor might throw std::system_error
    T m_obj;

public:

    // The constructor on the next line might throw if the constructor
    // of m_lock or m_obj throws
    template<typename... Args>
    explicit Reservable(Args&&... args) noexcept(
noexcept(decltype(m_lock)()) &&
noexcept(T(std::forward<Args>(args)...)))
      : m_obj(std::forward<Args>(args)...) {}

    // The next four methods might throw if m_lock.lock() throws an
std::system_error
    attrib_nodiscard Reserved<T> operator->(void) noexcept(
noexcept(m_lock.lock()) )
        { return Reserved<T>(m_lock,m_obj); }
    attrib_nodiscard Reserved<T const> operator->(void) const
noexcept( noexcept(m_lock.lock()) )
        { return Reserved<T const>(m_lock,m_obj); }

    attrib_nodiscard Reserved<T> Reserve(void) noexcept(
noexcept(m_lock.lock()) )
        { return Reserved<T>(m_lock,m_obj); }
    attrib_nodiscard Reserved<T const> Reserve(void) const noexcept(
noexcept(m_lock.lock()) )
        { return Reserved<T const>(m_lock,m_obj); }

    attrib_nodiscard Reserved<T> operator*(void) noexcept(
noexcept(m_lock.lock()) )
        { return Reserved<T>(m_lock,m_obj); }
    attrib_nodiscard Reserved<T const> operator*(void) const noexcept(
noexcept(m_lock.lock()) )
        { return Reserved<T const>(m_lock,m_obj); }

    //T &GetThreadUnsafeAccess(void) { return m_obj; }
};

#else

template<typename T, class MutexType = std::recursive_mutex>
class Reservable final {

    static_assert( !std::is_reference<T>::value, "T cannot be a reference" );
    static_assert(
        std::is_same<
            MutexType,
            typename std::remove_cv<typename
std::remove_reference<MutexType>::type>::type
>::value , "MutexType must be a bare type (no cvref)" );

    // Reservable(void) = delete; - see public constructor below
    Reservable(Reservable const &) = delete;
    Reservable(Reservable &&) = delete;
    Reservable &operator=(Reservable const &) = delete;
    Reservable &operator=(Reservable &&) = delete;
    Reservable const volatile *operator&(void) const volatile = delete;
    template<typename U> void operator,(U&&) = delete;

    MutexType mutable m_lock; // constructor might throw std::system_error
    T m_obj;

public:

    // The constructor on the next line might throw if the constructor
    // of m_lock or m_obj throws
    template<typename... Args>
    explicit Reservable(Args&&... args) noexcept(
noexcept(decltype(m_lock)()) &&
noexcept(T(std::forward<Args>(args)...)))
      : m_obj(std::forward<Args>(args)...) {}

    // The next four methods might throw if m_lock.lock() throws an
std::system_error
    attrib_nodiscard Reserved<T> operator->(void) noexcept(
noexcept(m_lock.lock()) )
        { return Reserved<T>(m_lock,m_obj); }
    attrib_nodiscard Reserved<T const> operator->(void) const
noexcept( noexcept(m_lock.lock()) )
        { return Reserved<T const>(m_lock,m_obj); }

    attrib_nodiscard Reserved<T> Reserve(void) noexcept(
noexcept(m_lock.lock()) )
        { return Reserved<T>(m_lock,m_obj); }
    attrib_nodiscard Reserved<T const> Reserve(void) const noexcept(
noexcept(m_lock.lock()) )
        { return Reserved<T const>(m_lock,m_obj); }

    attrib_nodiscard Reserved<T> operator*(void) noexcept(
noexcept(m_lock.lock()) )
        { return Reserved<T>(m_lock,m_obj); }
    attrib_nodiscard Reserved<T const> operator*(void) const noexcept(
noexcept(m_lock.lock()) )
        { return Reserved<T const>(m_lock,m_obj); }

    //T &GetThreadUnsafeAccess(void) { return m_obj; }
};

#endif // check __cplusplus for concepts

#endif // HEADER_INCLUSION_GUARD_RESERVATIONS_MULTITHREADED


// =====================================================
// ===== And now here comes the test code ==============
// =====================================================

#include <vector>
#include <thread>
#include <iostream>

Reservable< std::vector<int> > g_vec;

int main(void)
{
    g_vec->push_back(5);

    std::thread mythread( [](){ g_vec->push_back(11); } );

    { // open new scope just for object lifetime
        auto mylock = g_vec.Reserve();
        mylock->push_back(7);
        mylock->push_back(8);
    }

    mythread.join();

    auto mylock = g_vec.Reserve();
    auto &vec = *mylock;

    // 'vec' is now an L-value reference to an std::vector<int>

    for ( auto &e : vec ) std::cout << e << std::endl;
}

Received on 2023-06-28 22:37:41