Skip to content

Commit

Permalink
Clean-up.
Browse files Browse the repository at this point in the history
  • Loading branch information
Louis-Charles Caron committed Nov 8, 2023
1 parent ed9f254 commit 1042406
Show file tree
Hide file tree
Showing 7 changed files with 102 additions and 359 deletions.
57 changes: 18 additions & 39 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ std::mutex barMutex;

{
std::lock_guard<std::mutex> lock(fooMutex); // is this the right mutex for what I am about to do ?
foo = "Hello, World!"; // I access foo here, but I could very well access bar, yet barMutex is not locked!
bar = "Hello, World!"; // Hmm, did I just to something wrong ?
}

std::cout << bar << std::endl; // unprotected access, is this intended ?
Expand All @@ -42,7 +42,7 @@ std::string baz; // now you can see that this variable has no mutex
}

std::cout << safeBar.unsafe() << std::endl; // unprotected access: clearly expressed!
std::cout << baz << std::endl; // all good (remember, baz has no mutex!)
std::cout << baz << std::endl; // all good this is just a string!
```
## Motivation
Since C++11, the standard library provides mutexes, like std::mutex, along with tools to facilitate their usage, like std::lock_guard and std::unique_lock. These are sufficient to write safe multithreaded code, but it is all too easy to write code you think is safe but actually is not. Typical mistakes are: locking the wrong mutex and accessing the value object before locking (or after unlocking) the mutex. Other minor mistakes like unnecessary locking or keeping a mutex locked for too long can also be avoided.
Expand Down Expand Up @@ -107,17 +107,9 @@ When you build your own project, you **won't** need to append `-DCMAKE_PREFIX_PA
## Basic usage
The *safe* library defines the Safe and Access class templates. They are meant to replace the mutexes and locks in your code. *safe* does not offer much more functionality than mutexes and locks do, they simply make their usage safer.
Here is the simplest way to replace mutexes and locks by Safe objects.
### Vocabulary
* *safe*: the library.
* mutex: a mutex like std::mutex.
* value object: whatever needs to be protected by the mutex.
* Safe object: combines a value object and a mutex.
* lock: an object that manages a mutex using RAII like std::lock_guard and std::unique_lock.
* Access object: a lock object that also gives pointer-like access to a value object.
* access mode: Access objects can be created with read-write or read-only behavior. Read-only Access objects are especially useful to enforce the read-only nature of C++14's std::shared_lock and boost::shared_lock_guard.
### Include the library's single header
```c++
#include <safe/safe.h>
#include "safe/safe.h"
```
### Replace your values and mutexes by Safe objects
```c++
Expand All @@ -131,7 +123,7 @@ Access objects can either be read-write or read-only. The examples below show di
// std::lock_guard<std::mutex> lock(mutex);
safe::WriteAccess<safe::Safe<int>> value(safeValue);
safe::Safe<int>::WriteAccess<> value(safeValue); // equivalent to the above
auto value = safeValue.writeAccess(); // nicer, but only with C++17 and later
auto value = safeValue.writeLock(); // nicer, but only with C++17 and later
```
#### The problem with std::lock_guard
The last line of the above example only compiles with C++17 and later. This is because of the new rules on temporaries introduced in C++17, and because *safe* uses std::lock_guard by default. std::lock_guard is non-copiable, non-moveable so it cannot be initialized as above prior to C++17. As shown below, using std::unique_lock (which is moveable) is fine:
Expand All @@ -144,24 +136,6 @@ You can now safely access the value object *through the Access object*. As long
// value = 42;
*value = 42;
```
#### Use Safe member functions as one-liners, if suitable
If you need to peform a single access to your value, you can do this using Safe's member functions: `readAccess()`, `writeAccess()`, `copy()` and `assign()`. `readAccess()` and `writeAccess()` will return an Access object, but will let you operate on it in an expressive way. Example:
```c++
*safeValue.writeAccess() = 42;
int value = *safeValue.readAccess();
int value = *safeValue.writeAccess(); // this also works...
// *safeValue.readAccess() = 42; // but this obviously doesn't!
```
However, if all you need to do is assign a new value, then you might as well use the `assign()` function:
```c++
safeValue.assign(42);
```
And if you just want a copy, you can call the `copy()` function:
```c++
int value = safeValue.copy();
```
***Warning: avoid multiple calls to these functions, as each will lock and unlock the mutex.***
*Be aware that copy/move construction/assignment operators are deleted for Safe objects. That is because copying and moving requires the mutex to be locked, and the safe library aims at making every locking explicit.* Use the copy() and assign() functions instead.
## Main features
### Safety and clarity
No more locking the wrong mutex, no more mistaken access outside the safety of a locked mutex. No more naked shared variables, no more plain mutexes lying around and no more *mutable* keyword (ever locked a member mutex variable within a const-qualified member function ?).
Expand All @@ -184,17 +158,19 @@ safe::Safe<int&, std::mutex&>;
```
See [this section](#With-legacy-code) for an example of using reference types to deal with legacy code.
#### Flexibly construct the value object and mutex
Just remember: the first argument to a Safe constructor is used to construct the mutex, the other arguments are used for the value object.
*Note: when constructing a Safe object and the mutex is default constructed but the value object is not, you must pass the safe::default_construct_mutex tag or a set of curly brackets {} as the first constructor argument.*
The Safe constructor accepts the arguments needed to construct the value and the mute object. The last argument is forwarded to the mutex constructor and the rest to the value's.
If the last argument cannot be used used to construc the mutex, *safe* detects it and forwards everything to the value constructor.
If you want to explicitely not use the last argument to construct the mutex object, use the safe::default_construct_mutex as last argument.

Examples:
```c++
std::mutex aMutex;

safe::Safe<int, std::mutex> bothDefault; // mutex and value are default constructed
safe::Safe<int, std::mutex&> noDefault(aMutex, 42); // mutex and value are initialized
safe::Safe<int, std::mutex&> noDefault(42, aMutex); // mutex and value are initialized
safe::Safe<int, std::mutex&> valueDefault(aMutex); // mutex is initialized, and value is default constructed
safe::Safe<int, std::mutex> mutexDefaultTag(safe::default_construct_mutex, 42); // mutex is default constructed, and value is initialized
safe::Safe<int, std::mutex> mutexDefaultBraces({}, 42);
safe::Safe<int, std::mutex> mutexDefaultTag(42); // mutex is default constructed, and value is initialized
safe::Safe<int, std::mutex> mutexDefaultTag(42, safe::default_construct_mutex); // mutex is default constructed, and value is initialized
```
#### Flexibly construct the Lock objects
The Access constructors have a variadic parameter pack that is forwarded to the Lock object's constructor. This can be used to pass in standard lock tags such as std::adopt_lock, but also to construct your custom locks that may require additionnal arguments than just the mutex.
Expand All @@ -206,13 +182,13 @@ safeValue.mutex().lock(); // with the mutex already locked...
// No matter how you get your Access objects, you can pass arguments to the lock's constructor.
safe::WriteAccess<safe::Safe<int>> value(safeValue, std::adopt_lock);
safe::Safe<int>::WriteAccess<> value(safeValue, std::adopt_lock);
auto value = safeValue.writeAccess(std::adopt_lock); // again, only in C++17
auto value = safeValue.writeAccess<std::unique_lock>(std::adopt_lock);
auto value = safeValue.writeLock(std::adopt_lock); // again, only in C++17
auto value = safeValue.writeLock<std::unique_lock>(std::adopt_lock);
```
### Even more safety!
#### Choose the access mode that suits each access
You will instatiate one Safe object for every value object you want to protect. But, you will create an Access object every time you want to operate on the value object. For each of these accesses, you can choose whether the access is read-write or read-only.
#### Force read-only access with shared mutexes and shared_locks
#### Force read-only access with shared_locks
Shared mutexes and shared locks allow multiple reading threads to access the value object simultaneously. Unfortunately, using only mutexes and locks, the read-only restriction is not guaranteed to be applied. That is, it is possible to lock a mutex in shared mode and write to the shared value. With *safe*, you can enforce read-only access when using shared locking by using ReadAccess objects. See [this section](#Enforcing-read-only-access) for details.
### Compatibility
#### With legacy code
Expand Down Expand Up @@ -255,6 +231,9 @@ struct safe::AccessTraits<std::shared_lock<MutexType>>
static constexpr bool IsReadOnly = true;
};
```
### Avoid some typing by defining your own default lock types
*safe* uses std::lock_guard by default everywhere. If you know you will always use a certain lock type given some mutex type (for instance, std::unique_lock with std::timed_mutex), you can inform *safe* and it will use these locks by default. To do so, you must specialize the safe::DefaultLock class template. Have a look at the tests/test_default_locks.cpp files. You will see that you can specify a different lock type for read and write accesses.

# Acknowledgment
Thanks to all contributors, issue raisers and stargazers!
Most cmake code comes from this repo: https://github.com/bsamseth/cpp-project and Craig Scott's CppCon 2019 talk: Deep CMake for Library Authors. Many thanks to the authors!
The cmake is inspired from https://github.com/bsamseth/cpp-project and Craig Scott's CppCon 2019 talk: Deep CMake for Library Authors. Many thanks to the authors!
2 changes: 1 addition & 1 deletion include/safe/access_mode.h
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) 2019-2022 Louis-Charles Caron
// Copyright (c) 2019-2023 Louis-Charles Caron

// This file is part of the safe library (https://github.com/LouisCharlesC/safe).

Expand Down
6 changes: 4 additions & 2 deletions include/safe/default_locks.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@

namespace safe
{
// Base template defining default lock types for all mutex types.
// Specialize this template as shown in the ReadMe and tests to define your own default locks.
template<typename MutexType, typename...>
struct DefaultLocks
{
Expand All @@ -19,7 +21,7 @@ namespace safe
};

template<typename MutexType>
using DefaultReadOnlyLock = typename DefaultLocks<MutexType>::ReadOnly;
using DefaultReadOnlyLockType = typename DefaultLocks<MutexType>::ReadOnly;
template<typename MutexType>
using DefaultReadWriteLock = typename DefaultLocks<MutexType>::ReadWrite;
using DefaultReadWriteLockType = typename DefaultLocks<MutexType>::ReadWrite;
} // namespace safe
31 changes: 31 additions & 0 deletions include/safe/last.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright (c) 2023 Louis-Charles Caron

// This file is part of the safe library (https://github.com/LouisCharlesC/safe).

// Use of this source code is governed by an MIT-style license that can be
// found in the LICENSE file or at https://opensource.org/licenses/MIT.

#pragma once

namespace safe
{
namespace impl
{
// This set of template and specializations is used to extract the type of the last argument of a paramter pack.
template <typename... Ts> struct Last; // Base template, specializations cover all uses.
template <typename First, typename Second, typename... Others> struct Last<First, Second, Others...>
{
using type = typename Last<Second, Others...>::type;
};
template <typename T> struct Last<T>
{
using type = T;
};
template <> struct Last<>
{
using type = void;
};
} // namespace impl

template <typename... Ts> using Last = typename impl::Last<Ts...>::type;
} // namespace safe
92 changes: 26 additions & 66 deletions include/safe/safe.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

#include "access_mode.h"
#include "default_locks.h"
#include "last.h"
#include "mutable_ref.h"

#include <type_traits>
Expand All @@ -26,30 +27,15 @@ namespace safe
{
namespace impl
{
template <typename... Ts> struct Last;
template <typename First, typename Second, typename... Others> struct Last<First, Second, Others...>
{
using type = typename Last<Second, Others...>::type;
};
template <typename T> struct Last<T>
{
using type = T;
};
template <> struct Last<>
{
using type = void;
};

struct DefaultConstructMutex
{
};
} // namespace impl
template <typename... Ts> using Last = typename impl::Last<Ts...>::type;

/**
* @brief Use this tag to default construct the mutex when constructing a Safe object.
*/
static constexpr impl::DefaultConstructMutex default_construct_mutex;
constexpr impl::DefaultConstructMutex default_construct_mutex;

/**
* @brief Wraps a value together with a mutex.
Expand Down Expand Up @@ -144,25 +130,6 @@ template <typename ValueType, typename MutexType = std::mutex> class Safe
{
}

/**
* @brief Construct an Access object from another one. OtherLockType must implement release() like
* std::unique_lock does.
*
* @tparam OtherLockType Deduced from otherAccess.
* @tparam OtherMode Deduced from otherAccess.
* @tparam OtherLockArgs Deduced from otherLockArgs.
* @param otherAccess The Access object to construct from.
* @param otherLockArgs Other arguments needed to construct the lock object.
*/
template <template <typename> class OtherLockType, AccessMode OtherMode, typename... OtherLockArgs>
EXPLICIT_IF_CPP17 Access(Access<OtherLockType, OtherMode> &otherAccess, OtherLockArgs &&...otherLockArgs)
: Access(*otherAccess, *otherAccess.lock.release(), std::adopt_lock,
std::forward<OtherLockArgs>(otherLockArgs)...)
{
static_assert(OtherMode == AccessMode::ReadWrite || OtherMode == Mode,
"Cannot construct a ReadWrite Access object from a ReadOnly one!");
}

/**
* @brief Const accessor to the value.
* @return ConstPointerType Const pointer to the protected value.
Expand Down Expand Up @@ -219,9 +186,9 @@ template <typename ValueType, typename MutexType = std::mutex> class Safe

public:
/// Aliases to ReadAccess and WriteAccess classes for this Safe class.
template <template <typename> class LockType = DefaultReadOnlyLock>
template <template <typename> class LockType = DefaultReadOnlyLockType>
using ReadAccess = Access<LockType, AccessMode::ReadOnly>;
template <template <typename> class LockType = DefaultReadWriteLock>
template <template <typename> class LockType = DefaultReadWriteLockType>
using WriteAccess = Access<LockType, AccessMode::ReadWrite>;

/**
Expand All @@ -231,8 +198,8 @@ template <typename ValueType, typename MutexType = std::mutex> class Safe
/**
* @brief Construct a Safe object, forwarding the last argument to construct the mutex and the other arguments to
* construct the value object. This constructor will be selected if the mutex can be constructed from the last
* argument of the parameter pack. To avoid using the last argument to construct the mutex, use the
* default_construct_mutex tag.
* argument of the parameter pack. To avoid using the last argument to construct the mutex, add the
* default_construct_mutex tag as last argument.
*
* @tparam Args Deduced from args.
* @tparam SFINAE constraint.
Expand Down Expand Up @@ -261,9 +228,8 @@ template <typename ValueType, typename MutexType = std::mutex> class Safe

}
/**
* @brief Construct a Safe object, forwarding all arguments but the first (the default_construct_mutex tag) to
* construct the value object. This constructor will be selected even if the mutex can be constructed from the last
* argument of the parameter pack.
* @brief Construct a Safe object, forwarding all arguments but the last (the default_construct_mutex tag) to
* construct the value object.
*
* @tparam Args Deduced from args.
* @param default_construct_mutex tag.
Expand All @@ -279,43 +245,37 @@ template <typename ValueType, typename MutexType = std::mutex> class Safe
}

/// Delete all copy/move construction/assignment, as these operations require locking the mutex under the covers.
/// Use copy(), assign() and other defined constructors to get the behavior you need with an explicit syntax.
Safe(const Safe &) = delete;
Safe(Safe &&) = delete;
Safe &operator=(const Safe &) = delete;
Safe &operator=(Safe &&) = delete;

template <template <typename> class LockType = DefaultReadOnlyLock, typename... LockArgs>
ReadAccess<LockType> readAccess(LockArgs &&...lockArgs) const
/**
* @brief Lock the Safe object to get a ReadAccess object.
*
* @tparam Args Deduced from args.
* @param args Perfect forwarding arguments to construct the lock object.
*/
template <template <typename> class LockType = DefaultReadOnlyLockType, typename... LockArgs>
ReadAccess<LockType> readLock(LockArgs &&...lockArgs) const
{
using ReturnType = ReadAccess<LockType>;
return EXPLICITLY_CONSTRUCT_RETURN_TYPE_IF_CPP17{*this, std::forward<LockArgs>(lockArgs)...};
}

template <template <typename> class LockType = DefaultReadWriteLock, typename... LockArgs>
WriteAccess<LockType> writeAccess(LockArgs &&...lockArgs)
/**
* @brief Lock the Safe object to get a WriteAccess object.
*
* @tparam Args Deduced from args.
* @param args Perfect forwarding arguments to construct the lock object.
*/
template <template <typename> class LockType = DefaultReadWriteLockType, typename... LockArgs>
WriteAccess<LockType> writeLock(LockArgs &&...lockArgs)
{
using ReturnType = WriteAccess<LockType>;
return EXPLICITLY_CONSTRUCT_RETURN_TYPE_IF_CPP17{*this, std::forward<LockArgs>(lockArgs)...};
}

template <template <typename> class LockType = DefaultReadOnlyLock, typename... LockArgs>
RemoveRefValueType copy(LockArgs &&...lockArgs) const
{
return *readAccess<LockType>(std::forward<LockArgs>(lockArgs)...);
}

template <template <typename> class LockType = DefaultReadWriteLock, typename... LockArgs>
void assign(ConstValueReferenceType value, LockArgs &&...lockArgs)
{
*writeAccess<LockType>(std::forward<LockArgs>(lockArgs)...) = value;
}
template <template <typename> class LockType = DefaultReadWriteLock, typename... LockArgs>
void assign(RemoveRefValueType &&value, LockArgs &&...lockArgs)
{
*writeAccess<LockType>(std::forward<LockArgs>(lockArgs)...) = std::move(value);
}

/**
* @brief Unsafe const accessor to the value. If you use this function, you exit the realm of safe!
*
Expand Down Expand Up @@ -373,7 +333,7 @@ template <typename ValueType, typename MutexType = std::mutex> class Safe
* @tparam SafeType The type of Safe object to give read-only access to.
* @tparam LockType The type of lock.
*/
template <typename SafeType, template <typename> class LockType = DefaultReadOnlyLock>
template <typename SafeType, template <typename> class LockType = DefaultReadOnlyLockType>
using ReadAccess = typename SafeType::template ReadAccess<LockType>;

/**
Expand All @@ -382,7 +342,7 @@ using ReadAccess = typename SafeType::template ReadAccess<LockType>;
* @tparam SafeType The type of Safe object to give read-write access to.
* @tparam LockType The type of lock.
*/
template <typename SafeType, template <typename> class LockType = DefaultReadWriteLock>
template <typename SafeType, template <typename> class LockType = DefaultReadWriteLockType>
using WriteAccess = typename SafeType::template WriteAccess<LockType>;
} // namespace safe

Expand Down

0 comments on commit 1042406

Please sign in to comment.