Const correctness of STL library with reference-wrapper? - c++

I have the following class:
class Data;
class A
{
public:
A(Data& _data) : data(_data) {}
Data& getData() {return data;}
const Data& getData() const {return data;}
private:
Data& data;
};
Now imagine I need to keep not one, but multiple instances of Data. I keep them in a vector of reference wrappers, but I would also like to keep the const correctness: pass the data as unmodifiable in const context.
class A
{
public:
void addData(Data& _data) {data.push_back(std::ref(_data));}
const std::vector<std::reference_wrapper<Data>>& getData() {return data;}
//doesn't compile
//const std::vector<std::reference_wrapper<const Data>>& getData() const {return data;}
private:
std::vector<std::reference_wrapper<Data>> data;
}
How to implement this without having physical copying of the data? I.e. I don't want to return a copy of the vector by value and I don't want to keep two separate vectors in class A. Both are performance-impacting solutions for what is basically just a semantic problem.

Here's a const propagating reference_wrapper, based on cppreference's possible implementation
#include <utility>
#include <functional>
#include <type_traits>
namespace detail {
template <class T> T& FUN(T& t) noexcept { return t; }
template <class T> void FUN(T&&) = delete;
}
template <class T>
class reference_wrapper {
public:
// types
typedef T type;
// construct/copy/destroy
template <class U, class = decltype(
detail::FUN<T>(std::declval<U>()),
std::enable_if_t<!std::is_same_v<reference_wrapper, std::remove_cvref_t<U>> && !std::is_same_v<reference_wrapper<const T>, std::remove_cvref_t<U>>>()
)>
reference_wrapper(U&& u) noexcept(noexcept(detail::FUN<T>(std::forward<U>(u))))
: _ptr(std::addressof(detail::FUN<T>(std::forward<U>(u)))) {}
reference_wrapper(reference_wrapper&) noexcept = default;
reference_wrapper(reference_wrapper&&) noexcept = default;
// assignment
reference_wrapper& operator=(reference_wrapper& x) noexcept = default;
reference_wrapper& operator=(reference_wrapper&& x) noexcept = default;
// access
operator T& () noexcept { return *_ptr; }
T& get() noexcept { return *_ptr; }
operator const T& () const noexcept { return *_ptr; }
const T& get() const noexcept { return *_ptr; }
template< class... ArgTypes >
std::invoke_result_t<T&, ArgTypes...>
operator() ( ArgTypes&&... args ) {
return std::invoke(get(), std::forward<ArgTypes>(args)...);
}
template< class... ArgTypes >
std::invoke_result_t<const T&, ArgTypes...>
operator() ( ArgTypes&&... args ) const {
return std::invoke(get(), std::forward<ArgTypes>(args)...);
}
private:
T* _ptr;
};
template <class T>
class reference_wrapper<const T> {
public:
// types
typedef const T type;
// construct/copy/destroy
template <class U, class = decltype(
detail::FUN<const T>(std::declval<U>()),
std::enable_if_t<!std::is_same_v<reference_wrapper, std::remove_cvref_t<U>> && !std::is_same_v<reference_wrapper<T>, std::remove_cvref_t<U>>>()
)>
reference_wrapper(U&& u) noexcept(noexcept(detail::FUN<const T>(std::forward<U>(u))))
: _ptr(std::addressof(detail::FUN<const T>(std::forward<U>(u)))) {}
reference_wrapper(const reference_wrapper<T>& o) noexcept
: _ptr(std::addressof(o.get())) {}
reference_wrapper(const reference_wrapper&) noexcept = default;
reference_wrapper(reference_wrapper&&) noexcept = default;
// assignment
reference_wrapper& operator=(const reference_wrapper& x) noexcept = default;
reference_wrapper& operator=(reference_wrapper&& x) noexcept = default;
// access
operator const T& () const noexcept { return *_ptr; }
const T& get() const noexcept { return *_ptr; }
template< class... ArgTypes >
std::invoke_result_t<const T&, ArgTypes...>
operator() ( ArgTypes&&... args ) const {
return std::invoke(get(), std::forward<ArgTypes>(args)...);
}
private:
const T* _ptr;
};
// deduction guides
template<class T>
reference_wrapper(T&) -> reference_wrapper<T>;
You can then add const qualified access via span.
class A
{
public:
void addData(Data& _data) {data.emplace_back(_data);}
std::span<reference_wrapper<Data>> getData() { return { data.data(), data.size() }; }
std::span<const reference_wrapper<Data>> getData() const { return { data.data(), data.size() }; }
private:
std::vector<reference_wrapper<Data>> data;
}
Note that you can't copy or move the const reference_wrapper<Data>s from the second getData, and there is only access to const Data &.

Consider the visitor pattern :
struct ConstVisitor {
virtual ~ConstVisitor() = default;
virtual bool visit(const Data & data) = 0;//returns true if search should keep going on
};
void A::accept(ConstVisitor & visitor) const;
This way it does not matter to the outside world what kind of container Data is stored in (here a std::vector). The visitor pattern is very similar to an Enumerator in C#.

Related

How do I correctly pass converting constructor from an std::queue through to the underlying std::deque?

I have created a custom memory allocator. To use it with STL Containers, I've also created a wrapper so that it adheres to the std::allocator_traits requirements.
#include <cstdint>
#include <memory>
#include <deque>
#include <queue>
class CustomAlloc
{
public:
CustomAlloc(const std::size_t& maxMem) noexcept
:
m_maxMem(maxMem),
m_usedMem(0)
{
}
CustomAlloc(CustomAlloc&& other) noexcept
:
m_maxMem(other.m_maxMem),
m_usedMem(other.m_usedMem)
{
}
CustomAlloc& operator=(CustomAlloc&& other) noexcept
{
m_maxMem = other.m_maxMem;
m_usedMem = other.m_usedMem;
return *this;
}
CustomAlloc(const CustomAlloc&) = delete;
CustomAlloc& operator=(CustomAlloc&) = delete;
[[nodiscard]] void* Allocate(const std::size_t& size)
{
if(m_usedMem + size > m_maxMem)
{
throw std::bad_alloc();
}
void* ptr = std::malloc(size);
if(ptr == nullptr)
{
throw std::bad_alloc();
}
m_usedMem += size;
return ptr;
}
void Free(void* const ptr) const
{
return std::free(ptr);
}
const std::size_t& GetMaxMem() const noexcept
{
return m_maxMem;
}
private:
std::size_t m_maxMem;
std::size_t m_usedMem;
};
template<typename T, typename Alloc>
class STLAdaptor
{
public:
typedef T value_type;
STLAdaptor(Alloc& allocator) noexcept
:
m_allocator(allocator)
{
}
template<typename U>
STLAdaptor(const STLAdaptor<U, Alloc>& other) noexcept
:
m_allocator(other.m_allocator)
{}
[[nodiscard]] constexpr T* allocate(std::size_t n)
{
return reinterpret_cast<T*>
(m_allocator.Allocate(n * sizeof(T)));
}
constexpr void deallocate(T* p, [[maybe_unused]] std::size_t n)
{
m_allocator.Free(p);
}
std::size_t MaxAllocationSize() const
{
return m_allocator.GetMaxMem();
}
bool operator==(const STLAdaptor<T,Alloc>& rhs)
{
return m_allocator.GetStart() == rhs.m_allocator.GetStart();
}
bool operator!=(const STLAdaptor<T,Alloc>& rhs)
{
return !(*this == rhs);
}
Alloc& m_allocator;
};
template<typename T, typename Alloc>
using CustomDeque = std::deque<T, STLAdaptor<T,Alloc>>;
template<typename T, typename Alloc>
using CustomQueue = std::queue<T, CustomDeque<T, Alloc>>;
The issue I'm facing:
int main()
{
CustomAlloc customAlloc(3000000);
CustomDeque<int, CustomAlloc> customDeque(customAlloc);
CustomQueue<int, CustomAlloc> customQueue(STLAdaptor<int, CustomAlloc>{customAlloc});
return 0;
}
This works perfectly fine, but it's code-gore to have to pass in the STLAdaptor<int, CustomAlloc>{customAlloc} where all other STL Containers can rely on the converting constructor for the STLAdaptor.
What I'd like to do is:
int main()
{
CustomAlloc customAlloc(3000000);
CustomDeque<int, CustomAlloc> customDeque(customAlloc);
CustomQueue<int, CustomAlloc> customQueue(customAlloc);
return 0;
}
Where the customAlloc is somehow passed to the underlying std::deque.
If you can't change CustomAlloc code as Ted Lyngmo suggested, the alternative is to define CustomQueue as a subclass of std::queue:
template<typename T, typename Alloc>
struct CustomQueue : std::queue<T, CustomDeque<T, Alloc>>
{
CustomQueue(Alloc& alloc) :std::queue<T, CustomDeque<T, Alloc>>(STLAdaptor<T, Alloc>(alloc)) {}
};
Now you can construct it by passing merely the allocator:
int main()
{
CustomAlloc customAlloc(3000000);
CustomDeque<int, CustomAlloc> customDeque(customAlloc);
CustomQueue<int, CustomAlloc> customQueue(customAlloc);
customQueue.push(5);
return 0;
}
This code was tested on VS 2019.

Catch nondeterminism by error'ing on iterating pointer-keyed maps

It's been a few times we've found nondeterministic issues in the codebase I'm working on, and so far it's almost been root caused to the use of std::[unordered_]map/set<T*,U>, where the key is a pointer, combined with iteration on the map, usually in the form of a range-based for loop (since pointer values may change between executions, iteration order is nondeterministic).
I was wondering if there was some black template magic one could use to inject a static_assert when begin() is called on such a container. I think begin() is the best place to do this, or maybe iterator::operator++, since constructing iterators otherwise, such as a result of find(), is okay.
I thought I could overload std::begin, but the rules for range-based for loops state that .begin() is used if it exists. So, I'm out of ideas. Is there a clever trick to do this?
Further clarification: No custom comparator is involved, the direct value of the pointer (aka the address of the target object) is the key. This is fine for insertion and lookup, and only becomes a problem when iterating over the container since the order is based on unpredictable pointer values. I'm trying to find existing cases like this in a large existing codebase.
You can almost achieve the desired behavior with partial specializations:
20.5.4.2.1 The behavior of a C++ program is undefined if it adds declarations or definitions to namespace std or to a namespace within namespace std unless otherwise specified. A program may add a template specialization for any standard library template to namespace std only if the declaration depends on a user-defined type and the specialization meets the standard library requirements for the original template and is not explicitly prohibited.
Therefore, a simple specialization for std::map can be used to detect attempts to instantiate the template with a pointer key type:
#include <map>
namespace internal
{
// User-defined type trait
template<class Key, class T>
class DefaultAllocator
{
public:
using type = std::allocator<std::pair<const Key, T>>;
};
// Effectively the same as std::allocator, but a different type
template<class T>
class Allocator2 : public std::allocator<T> {};
}
namespace std
{
// Specialization for std::map with a pointer key type and the default allocator.
// The class inherits most of the implementation from
// std::map<Key*, T, Compare, ::internal::Allocator2<std::pair<Key*, T>>>
// to mimic the standard implementation.
template<class Key, class T, class Compare>
class map<Key*, T, Compare, typename ::internal::DefaultAllocator<Key*, T>::type> :
public map<Key*, T, Compare, ::internal::Allocator2<std::pair<Key*, T>>>
{
using base = map<Key*, T, Compare, ::internal::Allocator2<std::pair<Key*, T>>>;
using base::iterator;
using base::const_iterator;
public:
// Overload begin() and cbegin()
iterator begin() noexcept
{
static_assert(false, "OH NOES, A POINTER");
}
const_iterator begin() const noexcept
{
static_assert(false, "OH NOES, A POINTER");
}
const_iterator cbegin() const noexcept
{
static_assert(false, "OH NOES, A POINTER");
}
};
}
int main()
{
std::map<int, int> m1;
std::map<int*, int> m2;
// OK, not a specialization
m1[0] = 42;
for (auto& keyval : m1)
{
(void)keyval;
}
m2[nullptr] = 42; // Insertion is OK
for (auto& keyval : m2) // static_assert failure
{
(void)keyval;
}
}
However,
I haven't figured out a way to extend this for custom allocators: the declaration of the specialization has to depend on some user-defined type.
This is a terrible kludge, so I would only use it to find existing cases (rather than keeping as a static checker).
One approach to achieve a compile time failure for designated pointer types is to delete std::less, std::greater, std::hash, etc specializations for the specific pointer types that are susceptible to non-deterministic behavior (i.e. returned by interfaces). There are many options to provide "safe" functionality for pointer collections.
The following is a comprehensive example:
#include <cassert>
#include <memory>
#include <set>
#include <map>
#include <unordered_set>
#include <unordered_map>
#define DISABLE_NUMERIC_POINTER_SPECIALIZATIONS(T) \
namespace std { \
template <> struct hash<const T*> { std::size_t operator()(const T* obj) const = delete; }; \
template <> struct hash<T*> { std::size_t operator()(T* obj) const = delete; }; \
template <> struct less<const T*> { bool operator()(const T* lhs, const T* rhs) const = delete; }; \
template <> struct less<T*> { bool operator()(T* lhs, T* rhs) const = delete; }; \
template <> struct greater<const T*> { bool operator()(const T* lhs, const T* rhs) const = delete; }; \
template <> struct greater<T*> { bool operator()(T* lhs, T* rhs) const = delete; }; \
template <> struct less_equal<const T*> { bool operator()(const T* lhs, const T* rhs) const = delete; }; \
template <> struct less_equal<T*> { bool operator()(T* lhs, T* rhs) const = delete; }; \
template <> struct greater_equal<const T*> { bool operator()(const T* lhs, const T* rhs) const = delete; }; \
template <> struct greater_equal<T*> { bool operator()(T* lhs, T* rhs) const = delete; }; \
}
namespace NS {
class C {
public:
explicit C(int id) : m_id{id} {}
int id() const { return m_id; }
private:
int m_id;
};
inline bool operator ==(const C& lhs, const C& rhs) { return lhs.id() == rhs.id(); }
inline bool operator <(const C& lhs, const C& rhs) { return lhs.id() < rhs.id(); }
} // namespace NS
namespace std {
template <> struct hash<NS::C> { std::size_t operator()(const NS::C& obj) const { return obj.id(); } };
}
DISABLE_NUMERIC_POINTER_SPECIALIZATIONS(NS::C)
struct IndirectEqual {
template <typename T>
bool operator()(const T* lhs, const T* rhs) const {
return (lhs && rhs) ? *lhs == *rhs : lhs == rhs;
}
};
struct IndirectLess {
template <typename T>
bool operator()(const T* lhs, const T* rhs) const {
return (lhs && rhs) ? *lhs < *rhs : lhs < rhs;
}
};
struct IndirectGreater {
template <typename T>
bool operator()(const T* lhs, const T* rhs) const {
return (lhs && rhs) ? *lhs > *rhs : lhs > rhs;
}
};
struct IndirectHash {
template <typename T>
std::size_t operator()(const T* ptr) const {
return ptr ? std::hash<T>{}(*ptr) : std::numeric_limits<std::size_t>::max();
}
};
struct BuiltinLess {
template <typename T>
bool operator()(const T& lhs, const T& rhs) const { return lhs < rhs; }
};
struct SPLess {
template <typename T>
bool operator()(const std::shared_ptr<T>& lhs, const std::shared_ptr<T>& rhs) const { return lhs.get() < rhs.get(); }
};
struct BuiltinGreater {
template <typename T>
bool operator()(const T& lhs, const T& rhs) const { return lhs < rhs; };
};
struct PtrHash {
template <typename T>
std::size_t operator()(const T* ptr) const { return static_cast<std::size_t>(ptr); };
};
template <typename T>
class BasicSet : private std::set<T, BuiltinLess> {
public:
using std::set<T, BuiltinLess>::set;
using std::set<T, BuiltinLess>::find;
using std::set<T, BuiltinLess>::insert;
using std::set<T, BuiltinLess>::emplace;
using std::set<T, BuiltinLess>::end;
};
template <typename T>
class BasicSet<std::shared_ptr<T>> : private std::set<std::shared_ptr<T>, SPLess> {
public:
using std::set<std::shared_ptr<T>, SPLess>::set;
using std::set<std::shared_ptr<T>, SPLess>::find;
using std::set<std::shared_ptr<T>, SPLess>::insert;
using std::set<std::shared_ptr<T>, SPLess>::emplace;
using std::set<std::shared_ptr<T>, SPLess>::end;
};
int main()
{
// All of these decls result in a compiler error
// std::set<NS::C*> unsafe_s{new NS::C{1}, new NS::C{2}};
// std::map<NS::C*, int> unsafe_m{ {new NS::C{1}, 100} };
// std::unordered_set<NS::C*> unsafe_us{new NS::C{1}, new NS::C{2}};
// std::unordered_map<NS::C*, int> unsafe_um{ {new NS::C{1}, 123} };
std::set<NS::C*, IndirectLess> s{ new NS::C{1} };
std::unordered_set<NS::C*, IndirectHash> us1{ new NS::C{1} };
std::unordered_set<NS::C*, IndirectHash, IndirectEqual> us2{ new NS::C{1} };
auto c = new NS::C{1};
assert (s.find(c) != s.end());
assert (us1.find(c) == us1.end()); // pointers aren't equal
assert (us2.find(c) != us2.end()); // objects are equal
BasicSet<NS::C*> bs{ new NS::C{1} };
assert (bs.find(c) == bs.end()); // pointers aren't equal
auto sp1 = std::make_shared<NS::C>(10);
auto sp2 = std::make_shared<NS::C>(20);
BasicSet<std::shared_ptr<NS::C>> spset{sp1, sp2};
assert(spset.find(sp1) != spset.end());
return 0;
}
Note: This isn't perfect. E.G., one would need to disable 'volatile T*' and 'const volatile T*' variations. I'm sure there are other issues.

Interface which declares functions for iteration

I have an assignment in which I need to make template classes LinkedList and Traversible. Class Traversible needs to be a interface which declares functions for indexing and iteration of some collection class. I don't exactly know how to make an interface for iterator so LinkedList can use it. I was thinking something like
template <class T, class U>
class ITraversible {
public:
virtual U begin() noexcept = 0;
virtual U end() noexcept = 0;
virtual T& operator[](int) = 0;
};
and then in LinkedList header file I would do:
template <class T>
class LinkedList : public ITraversible<T,typename LinkedList<T>::iterator> {
struct node {
T data;
node* next, *prev;
explicit node(const T&);
void connect(node*);
};
node *head, *tail;
int n;
public:
/*************************ITERATOR************************/
class iterator : public std::iterator<std::bidirectional_iterator_tag, node*> {
typename LinkedList<T>::node* itr;
explicit iterator(node*) noexcept;
friend class LinkedList;
public:
iterator& operator++();
iterator operator++(int);
iterator& operator--();
iterator operator--(int);
bool operator==(const iterator&) const noexcept;
bool operator!=(const iterator&) const noexcept;
T& operator*() const noexcept;
T& operator->() const noexcept;
};
/**********************************************************/
LinkedList() noexcept;
LinkedList(std::initializer_list<T>);
LinkedList(const LinkedList&);
LinkedList(LinkedList&&) noexcept;
~LinkedList() noexcept;
LinkedList& operator=(LinkedList) noexcept;
template <class A>
friend void swap(LinkedList<A>&, LinkedList<A>&);
void add(const T&);
void removeAt(int);
int size() const noexcept;
bool operator==(const LinkedList&) const noexcept;
bool operator!=(const LinkedList&) const noexcept;
virtual T& operator[](int) override;
virtual iterator begin() noexcept override;
virtual iterator end() noexcept override;
};
But then Traversable template has two parameters and it should have only one.
Is this what I am supposed to do? Keep in mind I am new to templates and iterators.
When creating an interface you'll need to nail down the static types of what is being returned. These may behave dynamically different but you can't change the type other than using a subtype relation when returning pointers or references.
Personally, I think this exercise is ill-advised for a C++ context. It may make some sense when using Java or C#. However, similar behavior can be obtained. A rought sketch would be something like this (although this should work it will be rather slow):
template <typename T>
struct iterator_base {
virtual iterator_base() {}
virtual iterator_base<T>* do_clone() = 0;
virtual T& do_value() = 0;
virtual void do_next() = 0;
virtual bool do_equal() = 0;
// other operations to implement operator--, operator[], ...
};
template <typename It>
class iterator: iterator_base<typename std::iterator_traits<It>::value_type> {
typedef typename std::iterator_traits<It>::value_type> type;
It it;
iterator_base<type>* do_clone() { return new iterator<It>(*this); }
type& do_value() { return *this->it; }
void do_next() { ++this->it; }
bool do_equal(iterator_base<type>* other) {
return this->it == static_cast<iterator<It>>(other)->it;
}
};
template <typename T>
class poly_iterator {
std::unique_ptr<iterator_base<T>> ptr;
public:
poly_iterator(iterator_base<T>* ptr): ptr(ptr) {}
poly_iterator(poly_iterator const& other): ptr(other.ptr->clone()) {}
poly_iterator& operator= (poly_iterator other) {
other.swap(this);
return *this;
}
void swap(poly_iterator& other) { swap(this->ptr, other.ptr); }
T& operator*() { return this->ptr->value(); }
T* operator->() { return &this->operator*(); }
poly_iterator& operator++() { this->ptr->next(); return *this; }
poly_iterator operator++(int) {
poly_iterator rc(*this);
this->operator++();
return rc;
}
bool operator== (poly_iterator const& other) {
return this->ptr->equal(other.ptr.ptr());
}
bool operator!= (poly_iterator const& other) {
return !(*this == other);
}
// other operations
};
// define a suitable specialization of std::iterator_traits<poly_iterator<T>>
template <typename T>
class ITraversible {
virtual iterator_base<T>* do_begin() = 0;
virutal iterator_base<T>* do_end() = 0;
public:
poly_iterator<T> begin() { return this->do_begin(); }
poly_iterator<T> end() { return this->do_end(); }
// other operations
};
template <typename T>
class List: public ITraversible<T> {
std::list<T> list;
iterator_base<T>* do_begin() {
return iterator<std::list<T>::iterator>(list.begin());
}
iterator_base<T>* do_end() {
return iterator<std::list<T>::iterator>(list.end());
}
public:
// whatever is needed to fill the list
};

reference_wrapper implementation details

How does this templated operator() work in reference_wrapper implementation
template <class T>
class reference_wrapper {
public:
// types
typedef T type;
// construct/copy/destroy
reference_wrapper(T& ref) noexcept : _ptr(std::addressof(ref)) {}
reference_wrapper(T&&) = delete;
reference_wrapper(const reference_wrapper&) noexcept = default;
// assignment
reference_wrapper& operator=(const reference_wrapper& x) noexcept = default;
// access
operator T& () const noexcept { return *_ptr; }
T& get() const noexcept { return *_ptr; }
here it goes:
template< class... ArgTypes >
typename std::result_of<T&(ArgTypes&&...)>::type
operator() ( ArgTypes&&... args ) const {
return std::invoke(get(), std::forward<ArgTypes>(args)...);
}
why do we need operator() anyway? how it works?
what is the return content "result_of::type"?
what is (ArgTypes && ..) ??
invoke(get) ???
this code looks like C++ from another planet :)
private:
T* _ptr;
};
why do we need operator() anyway? how it works?
Suppose following context
int foo(int bar)
{
return bar + 5;
}
int main()
{
std::reference_wrapper<int(int)> ref = foo;
ref(5);
}
ref(5) calls operator() of reference wrapper. If it wouldn't be there, it would not work, because user defined conversion wouldn't happen in this case.
operator() returns std::result_of<T&(ArgTypes&&...), which is return value of the function stored and std::invoke call such function and forwards parameters to it.

Return locked resource from class with automatic unlocking

I would like to have a class member function which returns a pointer to a resource. The resource should be locked and unlocked automatically. I think of creating a non-copyable object which handles the locking.
Do you think the following is a good solution? Is it thread-safe? Are there already tools in the STL for this use case?
template<typename T, typename M>
struct LockedResource
{
private:
T* data_;
std::unique_lock<std::mutex> lock_;
public:
LockedRessource(T* data, M& mtx) : data_{data}, lock_{mtx} {}
T* data() const { return data_; }
};
Use case example:
#include <iostream>
#include <mutex>
#include <thread>
#include <vector>
class Foo {
private:
std::vector<int> data_;
std::mutex mtx_;
public:
LockedResource<std::vector<int>,std::mutex> data()
{ return LockedResource<std::vector<int>,std::mutex>{&data_, mtx_}; }
};
Foo foo;
void worker(int worker, int iterations, int dt) {
for(int i=0; i<iterations; i++) {
std::this_thread::sleep_for(std::chrono::milliseconds(dt));
auto res = foo.data();
// we now have a unique_lock until the end of the scope
std::cout << "Worker " << worker << " adding " << i << std::endl;
res.data()->push_back(i);
}
}
int main() {
std::thread t1{worker, 1, 10, 173};
std::thread t2{worker, 2, 20, 87};
t1.join();
t2.join();
}
This idea - of providing a handle that encapsulates both access to a synchronized object and the necessary locking - is not new: see Enforcing Correct Mutex Usage with Synchronized Values.
I liked Rook's idea to use a unique_ptr with a custom deleter for the handle, so I came up with one:
template <typename BasicLockable>
class unlock_deleter {
std::unique_lock<BasicLockable> lock_;
public:
unlock_deleter(BasicLockable& mtx) : lock_{mtx} {}
unlock_deleter(BasicLockable& mtx, std::adopt_lock_t a) noexcept : lock_{mtx, a} {}
template <typename T>
void operator () (T*) const noexcept {
// no-op
}
};
template <typename T, typename M = std::mutex>
using locked_ptr = std::unique_ptr<T, unlock_deleter<M>>;
which is used in the implementation of a template class synchronized that wraps an object and a mutex:
template <typename T, typename M = std::mutex>
class synchronized {
T item_;
mutable M mtx_;
// Implement Copy/Move construction
template <typename Other, typename N>
synchronized(Other&& other, const std::lock_guard<N>&) :
item_{std::forward<Other>(other).item_} {}
// Implement Copy/Move assignment
template <typename Other>
void assign(Other&& other) {
std::lock(mtx_, other.mtx_);
std::lock_guard<M> _{mtx_, std::adopt_lock};
std::lock_guard<decltype(other.mtx_)> _o{other.mtx_, std::adopt_lock};
item_ = std::forward<Other>(other).item_;
}
public:
synchronized() = default;
synchronized(const synchronized& other) :
synchronized(other, std::lock_guard<M>(other.mtx_)) {}
template <typename N>
synchronized(const synchronized<T, N>& other) :
synchronized(other, std::lock_guard<N>(other.mtx_)) {}
synchronized(synchronized&& other) :
synchronized(std::move(other), std::lock_guard<M>(other.mtx_)) {}
template <typename N>
synchronized(synchronized<T, N>&& other) :
synchronized(std::move(other), std::lock_guard<N>(other.mtx_)) {}
synchronized& operator = (const synchronized& other) & {
if (&other != this) {
assign(other);
}
return *this;
}
template <typename N>
synchronized& operator = (const synchronized<T, N>& other) & {
assign(other);
return *this;
}
synchronized& operator = (synchronized&& other) & {
if (&other != this) {
assign(std::move(other));
}
return *this;
}
template <typename N>
synchronized& operator = (synchronized<T, N>&& other) & {
assign(std::move(other));
return *this;
}
template <typename N>
void swap(synchronized<T, N>& other) {
if (static_cast<void*>(&other) != static_cast<void*>(this)) {
std::lock(mtx_, other.mtx_);
std::lock_guard<M> _{mtx_, std::adopt_lock};
std::lock_guard<N> _o{other.mtx_, std::adopt_lock};
using std::swap;
swap(item_, other.item_);
}
}
locked_ptr<T, M> data() & {
return locked_ptr<T, M>{&item_, mtx_};
}
locked_ptr<const T, M> data() const& {
return locked_ptr<const T, M>{&item_, mtx_};
}
};
template <typename T, typename M, typename N>
void swap(synchronized<T, M>& a, synchronized<T, N>& b) {
a.swap(b);
}
Taking care to synchronize copies/moves/swaps properly as well. Here's your sample program live at Coliru.