From 07dbf1f5e88b1395d80f541f71dfc43c691b72d6 Mon Sep 17 00:00:00 2001 From: Louis-Charles Caron Date: Tue, 7 Nov 2023 21:28:43 +0100 Subject: [PATCH] Clean-up. --- README.md | 57 +++------ include/safe/access_mode.h | 2 +- include/safe/default_locks.h | 6 +- include/safe/last.h | 31 +++++ include/safe/safe.h | 92 +++++---------- tests/test_readme.cpp | 50 ++++---- tests/test_safe.cpp | 221 +---------------------------------- 7 files changed, 101 insertions(+), 358 deletions(-) create mode 100644 include/safe/last.h diff --git a/README.md b/README.md index e5ca267..26c673b 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ std::mutex barMutex; { std::lock_guard 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 ? @@ -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. @@ -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 +#include "safe/safe.h" ``` ### Replace your values and mutexes by Safe objects ```c++ @@ -131,7 +123,7 @@ Access objects can either be read-write or read-only. The examples below show di // std::lock_guard lock(mutex); safe::WriteAccess> value(safeValue); safe::Safe::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: @@ -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 ?). @@ -184,17 +158,19 @@ safe::Safe; ``` 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 bothDefault; // mutex and value are default constructed -safe::Safe noDefault(aMutex, 42); // mutex and value are initialized +safe::Safe noDefault(42, aMutex); // mutex and value are initialized safe::Safe valueDefault(aMutex); // mutex is initialized, and value is default constructed -safe::Safe mutexDefaultTag(safe::default_construct_mutex, 42); // mutex is default constructed, and value is initialized -safe::Safe mutexDefaultBraces({}, 42); +safe::Safe mutexDefaultTag(42); // mutex is default constructed, and value is initialized +safe::Safe 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. @@ -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> value(safeValue, std::adopt_lock); safe::Safe::WriteAccess<> value(safeValue, std::adopt_lock); -auto value = safeValue.writeAccess(std::adopt_lock); // again, only in C++17 -auto value = safeValue.writeAccess(std::adopt_lock); +auto value = safeValue.writeLock(std::adopt_lock); // again, only in C++17 +auto value = safeValue.writeLock(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 @@ -255,6 +231,9 @@ struct safe::AccessTraits> 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! diff --git a/include/safe/access_mode.h b/include/safe/access_mode.h index feead95..07bdce5 100644 --- a/include/safe/access_mode.h +++ b/include/safe/access_mode.h @@ -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). diff --git a/include/safe/default_locks.h b/include/safe/default_locks.h index 1dd3ab3..dfc785c 100644 --- a/include/safe/default_locks.h +++ b/include/safe/default_locks.h @@ -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 struct DefaultLocks { @@ -19,7 +21,7 @@ namespace safe }; template - using DefaultReadOnlyLock = typename DefaultLocks::ReadOnly; + using DefaultReadOnlyLockType = typename DefaultLocks::ReadOnly; template - using DefaultReadWriteLock = typename DefaultLocks::ReadWrite; + using DefaultReadWriteLockType = typename DefaultLocks::ReadWrite; } // namespace safe \ No newline at end of file diff --git a/include/safe/last.h b/include/safe/last.h new file mode 100644 index 0000000..724601a --- /dev/null +++ b/include/safe/last.h @@ -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 struct Last; // Base template, specializations cover all uses. +template struct Last +{ + using type = typename Last::type; +}; +template struct Last +{ + using type = T; +}; +template <> struct Last<> +{ + using type = void; +}; +} // namespace impl + +template using Last = typename impl::Last::type; +} // namespace safe \ No newline at end of file diff --git a/include/safe/safe.h b/include/safe/safe.h index e4cfa09..cf2f7c2 100644 --- a/include/safe/safe.h +++ b/include/safe/safe.h @@ -9,6 +9,7 @@ #include "access_mode.h" #include "default_locks.h" +#include "last.h" #include "mutable_ref.h" #include @@ -26,30 +27,15 @@ namespace safe { namespace impl { -template struct Last; -template struct Last -{ - using type = typename Last::type; -}; -template struct Last -{ - using type = T; -}; -template <> struct Last<> -{ - using type = void; -}; - struct DefaultConstructMutex { }; } // namespace impl -template using Last = typename impl::Last::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. @@ -144,25 +130,6 @@ template 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