Repository: LouisCharlesC/safe
Branch: master
Commit: ab21513971c5
Files: 21
Total size: 57.9 KB
Directory structure:
gitextract_h1y9d_x0/
├── .clang-format
├── .github/
│ └── workflows/
│ └── all_platforms.yml
├── .gitignore
├── CMakeLists.txt
├── LICENSE
├── README.md
├── cmake/
│ ├── InstallTarget.cmake
│ └── safeConfig.cmake.in
├── include/
│ └── safe/
│ ├── access_mode.h
│ ├── default_locks.h
│ ├── meta.h
│ ├── mutable_ref.h
│ └── safe.h
└── tests/
├── CMakeLists.txt
├── cmake/
│ ├── Warnings.cmake
│ └── add_package.cmake
├── safe_with_custom_defaults.h
├── test_default_locks.cpp
├── test_main.cpp
├── test_readme.cpp
└── test_safe.cpp
================================================
FILE CONTENTS
================================================
================================================
FILE: .clang-format
================================================
---
Language: Cpp
# BasedOnStyle: Microsoft
AccessModifierOffset: -2
AlignAfterOpenBracket: Align
AlignArrayOfStructures: None
AlignConsecutiveMacros: None
AlignConsecutiveAssignments: None
AlignConsecutiveBitFields: None
AlignConsecutiveDeclarations: None
AlignEscapedNewlines: Right
AlignOperands: Align
AlignTrailingComments: true
AllowAllArgumentsOnNextLine: true
AllowAllConstructorInitializersOnNextLine: true
AllowAllParametersOfDeclarationOnNextLine: true
AllowShortEnumsOnASingleLine: false
AllowShortBlocksOnASingleLine: Never
AllowShortCaseLabelsOnASingleLine: false
AllowShortFunctionsOnASingleLine: None
AllowShortLambdasOnASingleLine: All
AllowShortIfStatementsOnASingleLine: Never
AllowShortLoopsOnASingleLine: false
AlwaysBreakAfterDefinitionReturnType: None
AlwaysBreakAfterReturnType: None
AlwaysBreakBeforeMultilineStrings: false
AlwaysBreakTemplateDeclarations: MultiLine
AttributeMacros:
- __capability
BinPackArguments: true
BinPackParameters: true
BraceWrapping:
AfterCaseLabel: false
AfterClass: true
AfterControlStatement: Always
AfterEnum: true
AfterFunction: true
AfterNamespace: true
AfterObjCDeclaration: true
AfterStruct: true
AfterUnion: false
AfterExternBlock: true
BeforeCatch: true
BeforeElse: true
BeforeLambdaBody: false
BeforeWhile: false
IndentBraces: false
SplitEmptyFunction: true
SplitEmptyRecord: true
SplitEmptyNamespace: true
BreakBeforeBinaryOperators: None
BreakBeforeConceptDeclarations: true
BreakBeforeBraces: Custom
BreakBeforeInheritanceComma: false
BreakInheritanceList: BeforeColon
BreakBeforeTernaryOperators: true
BreakConstructorInitializersBeforeComma: false
BreakConstructorInitializers: BeforeColon
BreakAfterJavaFieldAnnotations: false
BreakStringLiterals: true
ColumnLimit: 120
CommentPragmas: '^ IWYU pragma:'
CompactNamespaces: false
ConstructorInitializerAllOnOneLineOrOnePerLine: false
ConstructorInitializerIndentWidth: 4
ContinuationIndentWidth: 4
Cpp11BracedListStyle: true
DeriveLineEnding: true
DerivePointerAlignment: false
DisableFormat: false
EmptyLineAfterAccessModifier: Never
EmptyLineBeforeAccessModifier: LogicalBlock
ExperimentalAutoDetectBinPacking: false
FixNamespaceComments: true
ForEachMacros:
- foreach
- Q_FOREACH
- BOOST_FOREACH
IfMacros:
- KJ_IF_MAYBE
IncludeBlocks: Preserve
IncludeCategories:
- Regex: '^"(llvm|llvm-c|clang|clang-c)/'
Priority: 2
SortPriority: 0
CaseSensitive: false
- Regex: '^(<|"(gtest|gmock|isl|json)/)'
Priority: 3
SortPriority: 0
CaseSensitive: false
- Regex: '.*'
Priority: 1
SortPriority: 0
CaseSensitive: false
IncludeIsMainRegex: '(Test)?$'
IncludeIsMainSourceRegex: ''
IndentAccessModifiers: false
IndentCaseLabels: false
IndentCaseBlocks: false
IndentGotoLabels: true
IndentPPDirectives: None
IndentExternBlock: AfterExternBlock
IndentRequires: false
IndentWidth: 4
IndentWrappedFunctionNames: false
InsertTrailingCommas: None
JavaScriptQuotes: Leave
JavaScriptWrapImports: true
KeepEmptyLinesAtTheStartOfBlocks: true
LambdaBodyIndentation: Signature
MacroBlockBegin: ''
MacroBlockEnd: ''
MaxEmptyLinesToKeep: 1
NamespaceIndentation: None
ObjCBinPackProtocolList: Auto
ObjCBlockIndentWidth: 2
ObjCBreakBeforeNestedBlockParam: true
ObjCSpaceAfterProperty: false
ObjCSpaceBeforeProtocolList: true
PenaltyBreakAssignment: 2
PenaltyBreakBeforeFirstCallParameter: 19
PenaltyBreakComment: 300
PenaltyBreakFirstLessLess: 120
PenaltyBreakString: 1000
PenaltyBreakTemplateDeclaration: 10
PenaltyExcessCharacter: 1000000
PenaltyReturnTypeOnItsOwnLine: 1000
PenaltyIndentedWhitespace: 0
PointerAlignment: Right
PPIndentWidth: -1
ReferenceAlignment: Pointer
ReflowComments: true
ShortNamespaceLines: 1
SortIncludes: CaseSensitive
SortJavaStaticImport: Before
SortUsingDeclarations: true
SpaceAfterCStyleCast: false
SpaceAfterLogicalNot: false
SpaceAfterTemplateKeyword: true
SpaceBeforeAssignmentOperators: true
SpaceBeforeCaseColon: false
SpaceBeforeCpp11BracedList: false
SpaceBeforeCtorInitializerColon: true
SpaceBeforeInheritanceColon: true
SpaceBeforeParens: ControlStatements
SpaceAroundPointerQualifiers: Default
SpaceBeforeRangeBasedForLoopColon: true
SpaceInEmptyBlock: false
SpaceInEmptyParentheses: false
SpacesBeforeTrailingComments: 1
SpacesInAngles: Never
SpacesInConditionalStatement: false
SpacesInContainerLiterals: true
SpacesInCStyleCastParentheses: false
SpacesInLineCommentPrefix:
Minimum: 1
Maximum: -1
SpacesInParentheses: false
SpacesInSquareBrackets: false
SpaceBeforeSquareBrackets: false
BitFieldColonSpacing: Both
Standard: Latest
StatementAttributeLikeMacros:
- Q_EMIT
StatementMacros:
- Q_UNUSED
- QT_REQUIRE_VERSION
TabWidth: 4
UseCRLF: false
UseTab: Never
WhitespaceSensitiveMacros:
- STRINGIZE
- PP_STRINGIZE
- BOOST_PP_STRINGIZE
- NS_SWIFT_NAME
- CF_SWIFT_NAME
...
================================================
FILE: .github/workflows/all_platforms.yml
================================================
name: All Platforms
on: [push]
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, ubuntu-20.04, macos-latest, macos-13, windows-latest, windows-2019]
build_type: ['Release', 'Debug']
cxx_std: [11, 20]
steps:
- uses: actions/checkout@v4
- name: Build with cxx standard specified
run: |
cmake -B ${{github.workspace}}/build/${{matrix.build_type}} -DCMAKE_CXX_STANDARD=${{matrix.cxx_std}} -DCMAKE_INSTALL_PREFIX="${{github.workspace}}/install/${{matrix.build_type}}" -DDEPS=REMOTE
cmake --build ${{github.workspace}}/build/${{matrix.build_type}} --config ${{matrix.build_type}} --target install
- name: Build tests with install space
run: |
cmake -S ${{github.workspace}}/tests -B ${{github.workspace}}/build-tests-standalone/${{matrix.build_type}} -DCMAKE_CXX_STANDARD=${{matrix.cxx_std}} -DCMAKE_PREFIX_PATH="${{github.workspace}}/install/${{matrix.build_type}}" -DDEPS=REMOTE
cmake --build ${{github.workspace}}/build-tests-standalone/${{matrix.build_type}} --config ${{matrix.build_type}}
- name: Test
env:
CTEST_OUTPUT_ON_FAILURE: 1
run: ctest -C ${{matrix.build_type}} --test-dir ${{github.workspace}}/build/${{matrix.build_type}}
================================================
FILE: .gitignore
================================================
build*/
install*/
compile_commands.json
.vscode/
.cache/
Testing/
================================================
FILE: CMakeLists.txt
================================================
cmake_minimum_required(VERSION 3.9.2)
cmake_policy(SET CMP0048 NEW)
project(safe VERSION 1.1.0 LANGUAGES CXX)
option(BUILD_TESTING "Build tests" ON)
set(DEPS "AUTO" CACHE STRING "Fetch git repos or use local packages (AUTO/REMOTE/LOCAL)")
set(DEPS_LIST AUTO REMOTE LOCAL)
set_property(CACHE DEPS PROPERTY STRINGS ${DEPS_LIST})
if(NOT DEPS IN_LIST DEPS_LIST)
message(FATAL_ERROR "DEPS must be one of ${DEPS_LIST}")
endif()
include(GNUInstallDirs)
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
add_library(${PROJECT_NAME} INTERFACE)
target_include_directories(${PROJECT_NAME} INTERFACE
$<BUILD_INTERFACE:${${PROJECT_NAME}_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
)
target_compile_features(${PROJECT_NAME} INTERFACE cxx_std_11)
add_library(safe::safe ALIAS safe)
if(BUILD_TESTING)
enable_testing()
add_subdirectory(tests)
endif()
include(InstallTarget)
install(DIRECTORY ${PROJECT_SOURCE_DIR}/include/${PROJECT_NAME} DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2019-2023 Louis-Charles Caron
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# *Every variable protected by a mutex should be wrapped with safe.*
[](https://github.com/LouisCharlesC/safe/actions/workflows/all_platforms.yml)
## Contents
*safe* is a header-only library that makes code with mutexes safer and easier to understand.
This readme will walk you through the important features of the library using several code examples. Read on, and enjoy safe mutexes!
- [Overview](#overview)
- [Motivation](#motivation)
- [Main features](#main-features)
- [Installation](#installation)
- [Advanced usage](#advanced-usage)
## Overview
Two class templates are at the core of *safe*: Safe and Access. Safe objects pack a mutex and a value object together. Access objects act as a lock (e.g. std::lock_guard) for the mutex and provide pointer-like access to the value object.
Here is why you want to use *safe*:
### Mutex code without *safe*
```c++
std::string foo; // do I need to lock a mutex to safely access this variable ?
std::string bar;
std::string baz; // what about this one ?
std::mutex fooMutex; // don't forget to change the name of this variable if foo's name changes!
std::mutex barMutex;
{
std::lock_guard<std::mutex> lock(fooMutex); // is this the right mutex for what I am about to do ?
bar = "Hello, World!"; // Hmm, did I just to something wrong ?
}
std::cout << bar << std::endl; // unprotected access, is this intended ?
std::cout << baz << std::endl; // what about this access ?
```
### Mutex code with *safe*
```c++
#include "safe/safe.h"
safe::Safe<std::string> safeFoo; // std::string and mutex packaged together!
safe::Safe<std::string> safeBar;
std::string baz; // now you can see that this variable has no mutex
{
safe::WriteAccess<safe::Safe<std::string>> foo(safeFoo); // this locks the mutex and gives you access to foo
*foo = "Hello, World!"; // access the value using pointer dereference: * and ->
}
std::cout << safeBar.unsafe() << std::endl; // unprotected access: clearly expressed!
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.
*safe* prevents common mutex usage mistakes by providing tools that complement the C++ standard library. Using *safe*, you will find it much easier to protect a variable using a mutex, and your code will be easier to understand. 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 ?).
## Main features
### Flexibility
#### Choose the mutex and lock that fit your need
The Safe class template has a template parameter for the mutex:
- use std::mutex, std::shared_mutex (C++17), name it!
The Access class template has a template parameter for the lock object:
- use std::lock_guard, boost::shared_lock_guard, anything you want!
- you can use the lock you need for every Access object you construct.
#### Store the value object and mutex inside the Safe object, or refer to existing objects
You can use any combination of reference and non-reference types for your Safe objects:
```c++
safe::Safe<int, std::mutex>;
safe::Safe<int>; // equivalent to the above, as the second template parameter defaults to std::mutex
safe::Safe<int&, std::mutex>;
safe::Safe<int, std::mutex&>;
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
The Safe constructor accepts the arguments needed to construct the value and the mutex object. The last argument is forwarded to the mutex constructor and the rest to the value's.
If the last argument cannot be used to construct the mutex, *safe* detects it and forwards everything to the value constructor.
If you explicitely do not want to use the last argument to construct the mutex object, use the safe::default_construct_mutex tag as last argument.
Examples:
```c++
std::mutex mutex;
safe::Safe<std::pair<int,int>, std::mutex> bothDefault; // mutex and value are default constructed
safe::Safe<std::pair<int,int>, std::mutex &> noDefault(42, 24, mutex); // mutex and value are initialized
safe::Safe<std::pair<int,int>, std::mutex &> valueDefault(mutex); // mutex is initialized, and value is default constructed
safe::Safe<std::pair<int,int>, std::mutex> mutexDefault(42, 24); // mutex is default constructed, and value is initialized
safe::Safe<std::pair<int,int>, std::mutex> mutexDefaultTag(42, 24, 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.
There are many equivalent ways to get an Access object from a Safe object. Choose the syntax you prefer:
```c++
safe::Safe<int> safeValue;
const safe::ReadAccess<safe::Safe<int>> value(safeValue);
const safe::Safe<int>::ReadAccess<> value(safeValue); // the empty <> are unfortunately necessary.
const auto value = safeValue.readLock(); // nicer, but only with C++17 and later
const auto value = safeValue.readLock<std::unique_lock>(); // ok even pre-C++17, using a std::unique_lock rather than the default std::lock_guard
const safe::Safe<int>::ReadAccess<std::unique_lock> value(safeValue); // equivalent to the above
// All of the above exist in read-write versions, simply replace "read" by "write" (and remove the const).
```
See [this section](#with-stdlock_guard) to understand why the nicer version doesn't work pre-C++17.
Examples passing arguments to the Lock object:
```c++
std::mutex aLockedMutex;
aLockedMutex.lock();
safe::Safe<int, std::mutex&> safeValue(aLockedMutex); // given a Safe object with an already locked mutex.
// Because the mutex is already locked, you need to pass the std::adopt_lock tag to std::lock_guard when you construct your Access object.
// No matter how you get your Access objects, you can pass arguments to the lock's constructor.
// Again, all the lines below are equivalent but here we need to specify the mutex type for the Safe type, because it is not the default (notice the &):
safe::WriteAccess<safe::Safe<int, std::mutex&>> value(safeValue, std::adopt_lock);
safe::Safe<int, std::mutex&>::WriteAccess<> value(safeValue, 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); // using a std::unique_lock still works
```
```c++
// Here's a safe int using a timed_mutex
safe::Safe<int, std::timed_mutex> safeValue;
// Pass in a time-out when you lock the mutex, and it will try to lock for that duration!
// safe does not do anything here, std::unique_lock does it all. safe simply forwards any argument
// it gets, and when std::unique_lock receives a std::duration, it try to lock before giving up.
auto value = safeValue.writeLock<std::unique_lock>(std::chrono::seconds(1));
// You can reach the lock through the Access object and check whether or not the locking was successful:
assert(value.lock.owns_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 in every scope where 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_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
You can use *safe* with old-style unsafe code that uses the soon-to-be out-of-fashion separate-mutex-and-value idiom. Imagine you are provided with the typical mutex and int. *safe* allows you to wrap these variables, without having to modify the existing code. Enjoy the safety and avoid the headaches:
```c++
std::mutex lousyMutex;
int unsafeValue;
// Wrap the existing variables
safe::Safe<int&, std::mutex&> safeValue(lousyMutex, unsafeValue);
// do not use lousyMutex and unsafeValue directly from here on!
```
#### With code from the future
*safe* is written in C++11, but it is fully compatible with mutexes and locks from different sources like C++14's std::shared_lock and C++17's std::shared_mutex, thanks to template parameters. Of course, you can also use boost::shared_lock_guard and your own custom mutexes and locks.
#### With standard uses of mutexes and locks
The mutex is accessible from the Safe object through an accessor functions, and the lock object is a public member of the Access class. Anything you can do with your typical mutexes and locks you can do with *safe*.
For example, *safe* can seamlessly be used with std::condition_variable:
```c++
std::condition_variable cv;
safe::Safe<int> safeValue;
safe::Safe<int>::WriteAccess<std::unique_lock> value(safeValue);
cv.wait(value.lock, [](){return true;});
// use `value` as you usually would.
```
#### With std::lock_guard
Some code shown in this readme does not compile with C++ versions prior C++17. 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 nicely as other locks prior to C++17. If you don't like using std::lock_guard, check out the [Advanced Usage](#advanced-usage) section.
## Installation
### Method 1: Copy the source files in your project
*safe* is a header-only library. Using the library can simply mean copy the contents of the include/ folder to some place of your convenience. This is the most straightforward installation method.
### Method 2: Via CMake FetchContent (CMake > 3.14)
```CMake
cmake_minimum_required(VERSION 3.14)
project(my_project)
FetchContent_Declare(
safe
GIT_REPOSITORY https://github.com/LouisCharlesC/safe.git
GIT_TAG v1.1.0
)
FetchContent_MakeAvailable(safe)
add_executable(my_project my_project.cc)
target_link_library(my_project safe::safe)
```
NOTE: `find_package(safe CONFIG REQUIRED)` is not needed with this method.
### Method 3: Install locally via CMake
```bash
git clone https://github.com/louischarlescaron/safe
cd safe
cmake -B safe-build -DCMAKE_INSTALL_PREFIX="$(pwd)/safe-install"
cmake --build safe-build --config Release --target install
```
Then you can use `find_package` in your project:
```cmake
cmake_minimum_required(VERSION 3.11)
project(my_project)
find_package(safe CONFIG REQUIRED)
add_executable(my_project my_project.cc)
target_link_library(my_project safe::safe)
```
And build with:
```bash
cd my_project
cmake -B build -DCMAKE_PREFIX_PATH="path/to/safe-install"
cmake --build build --config Release
```
NOTE: `CMAKE_PREFIX_PATH` is used to tell `find_package()` where to look for libraries. `path/to/safe-install` is not a standard path but it's easier to remove when needed.
### Method 4: Install system-wide via CMake (not recommended)
```bash
git clone https://github.com/LouisCharlesC/safe
cd safe
cmake -B build
sudo cmake --build build --config Release --target install
```
*safe* will be installed into your OS's standard intallation path. Be aware that system-wide installation make it hard to deal with multilpe library versions, and can cause collisions if you happen to install another library called safe!
When you build your own project, you **won't** need to append `-DCMAKE_PREFIX_PATH="path/to/safe-install"`.
## Advanced usage
### Enforcing read-only access
You can inform the *safe* library that some locks that you use are read-only (e.g. std::shared_lock, boost::shared_lock_guard). If you do so, trying to instantiate a WriteAccess object with these locks will trigger a compilation error. Use the trait class safe::AccessTraits to customize this behavior.
Here is how the trait works:
- If no specialization of the type trait exists for a lock type, the lock can be used with read-write and read-only Access objects.
- If a specialization exists, it must declare the IsReadOnly boolean variable.
- If IsReadOnly is true, the lock is read-only: constructinfg an Access object with this lock using Mode = AccessMode::ReadWrite will fail to compile.
- If IsReadOnly is false, the result is the same as if no specialization exists.
As an example, here is how to specialize the trait for std::shared_lock (you will find this exact code snippet in safe/access_mode.h):
```c++
template<typename MutexType>
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 this lock by default with this mutex type.
To do so, you must specialize the safe::DefaultLocks class template. Have a look at the tests/test_default_locks.cpp files. As you can see, you can specify a different lock type for read and write accesses:
```c++
template<>
struct safe::impl::DefaultLocks<std::timed_mutex>
{
using ReadOnly = std::unique_lock<std::timed_mutex>;
using ReadWrite = std::lock_guard<std::timed_mutex>;
};
```
# Acknowledgment
Thanks to all contributors, issue raisers and stargazers!
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!
================================================
FILE: cmake/InstallTarget.cmake
================================================
include(CMakePackageConfigHelpers)
write_basic_package_version_file(
"${PROJECT_NAME}ConfigVersion.cmake"
VERSION ${PROJECT_VERSION}
COMPATIBILITY SameMajorVersion
)
configure_package_config_file(
"${CMAKE_CURRENT_LIST_DIR}/${PROJECT_NAME}Config.cmake.in"
"${PROJECT_BINARY_DIR}/${PROJECT_NAME}Config.cmake"
INSTALL_DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/${PROJECT_NAME}/cmake
)
install(FILES
${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}Config.cmake
${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}ConfigVersion.cmake
DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/${PROJECT_NAME}/cmake
)
install(TARGETS ${PROJECT_NAME}
EXPORT ${PROJECT_NAME}Targets
INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
COMPONENT ${PROJECT_NAME}_RunTime
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
COMPONENT ${PROJECT_NAME}_RunTime
# NAMELINK_COMPONENT ${PROJECT_NAME}_Development
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
COMPONENT NAME}_Development
)
install(EXPORT ${PROJECT_NAME}Targets
DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/${PROJECT_NAME}/cmake
NAMESPACE ${PROJECT_NAME}::
FILE ${PROJECT_NAME}Targets.cmake
COMPONENT ${PROJECT_NAME}_Development
)
================================================
FILE: cmake/safeConfig.cmake.in
================================================
@PACKAGE_INIT@
include("${CMAKE_CURRENT_LIST_DIR}/@PROJECT_NAME@Targets.cmake")
check_required_components("@PROJECT_NAME@")
================================================
FILE: include/safe/access_mode.h
================================================
// Copyright (c) 2019-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
#include <mutex>
#if __cplusplus >= 201402L
#include <shared_mutex>
#endif // __cplusplus >= 201402L
namespace safe
{
enum class AccessMode
{
ReadOnly,
ReadWrite
};
// Base template: most LockTypes are not read only.
// Specialize this template if you want to force a LockType to be read only.
// If you specialize this template, consider using the pattern described in the
// test_default_locks.cpp file to avoid ill-formed programs!
template <typename LockType> struct AccessTraits
{
static constexpr bool IsReadOnly = false;
};
// Partial specialization for lock_guard: not read only.
template <typename MutexType> struct AccessTraits<std::lock_guard<MutexType>>
{
static constexpr bool IsReadOnly = false;
};
// Partial specialization for unique_lock: not read only.
template <typename MutexType> struct AccessTraits<std::unique_lock<MutexType>>
{
static constexpr bool IsReadOnly = false;
};
// Partial specialization for shared_lock: read only!
#if __cplusplus >= 201402L
template <typename MutexType> struct AccessTraits<std::shared_lock<MutexType>>
{
static constexpr bool IsReadOnly = true;
};
#endif // __cplusplus >= 201402L
} // namespace safe
================================================
FILE: include/safe/default_locks.h
================================================
// Copyright (c) 2019-2022 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
#include <mutex>
namespace safe
{
namespace impl
{
// 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.
// This struct only uses the first template parameter (MutexType), the parameter pack is there
// only to allow one to partially specialize the template using a single template parameter. Doing
// so overrides the default types for any MutexType. See the test_default_locks.cpp file for examples.
template<typename MutexType, typename...>
struct DefaultLocks
{
using ReadOnly = std::lock_guard<MutexType>;
using ReadWrite = std::lock_guard<MutexType>;
};
} // namespace impl
template<typename MutexType>
using DefaultReadOnlyLockType = typename impl::DefaultLocks<MutexType>::ReadOnly;
template<typename MutexType>
using DefaultReadWriteLockType = typename impl::DefaultLocks<MutexType>::ReadWrite;
} // namespace safe
================================================
FILE: include/safe/meta.h
================================================
// 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
#include <cstddef>
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;
};
template<std::size_t... Is>
struct index_sequence {};
template<std::size_t N, std::size_t... Is>
struct index_sequence<N, Is...>
{
using type = typename index_sequence<N-1, N-1, Is...>::type;
};
template<std::size_t... Is>
struct index_sequence<0, Is...>
{
using type = index_sequence<Is...>;
};
template<std::size_t N>
constexpr typename index_sequence<N>::type make_index_sequence()
{
return {};
}
} // namespace impl
template <typename... Ts> using Last = typename impl::Last<Ts...>::type;
} // namespace safe
================================================
FILE: include/safe/mutable_ref.h
================================================
// Copyright (c) 2019-2022 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
#include <utility>
namespace safe
{
namespace impl
{
/**
* @brief A helper class that defines a member variable of type Type. The variable is defined "mutable Type" if Type is
* not a reference, the variable is "Type&" if Type is a reference.
*
* @tparam Type The type of the variable to define.
*/
template <typename Type> struct MutableIfNotReference
{
/// Mutable Type object.
mutable Type get;
};
/**
* @brief Specialization of MutableIfNotReference for references.
*
* @tparam Type The type of the reference to define.
*/
template <typename Type> struct MutableIfNotReference<Type &>
{
/// Reference to a Type object.
Type &get;
};
} // namespace impl
} // namespace safe
================================================
FILE: include/safe/safe.h
================================================
// Copyright (c) 2019-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
#include "access_mode.h"
#include "default_locks.h"
#include "meta.h"
#include "mutable_ref.h"
#include <type_traits>
#include <utility>
#if __cplusplus >= 201703L
#define EXPLICIT_IF_CPP17 explicit
#define EXPLICITLY_CONSTRUCT_RETURN_TYPE_IF_CPP17 ReturnType
#else
#define EXPLICIT_IF_CPP17
#define EXPLICITLY_CONSTRUCT_RETURN_TYPE_IF_CPP17
#endif
namespace safe
{
namespace impl
{
struct DefaultConstructMutex
{
};
} // namespace impl
/**
* @brief Use this tag to default construct the mutex when constructing a Safe object.
*/
constexpr impl::DefaultConstructMutex default_construct_mutex;
/**
* @brief Wraps a value together with a mutex.
*
* @tparam ValueType The type of the value to protect.
* @tparam MutexType The type of the mutex.
*/
template <typename ValueType, typename MutexType = std::mutex> class Safe
{
private:
/// Type ValueType with reference removed, if present
using RemoveRefValueType = typename std::remove_reference<ValueType>::type;
/// Type MutexType with reference removed, if present
using RemoveRefMutexType = typename std::remove_reference<MutexType>::type;
/**
* @brief Manages a mutex and gives pointer-like access to a value object.
*
* @tparam LockType The type of the lock object that manages the mutex, example: std::lock_guard.
* @tparam Mode Determines the access mode of the Access object. Can be either AccessMode::ReadOnly or
* AccessMode::ReadWrite.
*/
template <template <typename> class LockType, AccessMode Mode> class Access
{
// Make sure AccessMode is ReadOnly if a read-only lock is used
static_assert(!(AccessTraits<LockType<RemoveRefMutexType>>::IsReadOnly && Mode == AccessMode::ReadWrite),
"Cannot have ReadWrite access mode with ReadOnly lock. "
"Check the value of "
"AccessTraits<LockType>::IsReadOnly if it exists.");
/// ValueType with const qualifier if AccessMode is ReadOnly.
using ConstIfReadOnlyValueType =
typename std::conditional<Mode == AccessMode::ReadOnly, const RemoveRefValueType, RemoveRefValueType>::type;
public:
/// Pointer-to-const ValueType
using ConstPointerType = const ConstIfReadOnlyValueType *;
/// Pointer-to-const ValueType if Mode is ReadOnly, pointer to ValueType otherwise.
using PointerType = ConstIfReadOnlyValueType *;
/// Reference-to-const ValueType
using ConstReferenceType = const ConstIfReadOnlyValueType &;
/// Reference-to-const ValueType if Mode is ReadOnly, reference to ValueType otherwise.
using ReferenceType = ConstIfReadOnlyValueType &;
/**
* @brief Construct an Access object from a possibly const reference to the value object and any additionnal
* argument needed to construct the Lock object.
*
* @tparam LockArgs Deduced from lockArgs.
* @param value Reference to the value.
* @param lockArgs Arguments needed to construct the lock object.
*/
template <typename... OtherLockArgs>
EXPLICIT_IF_CPP17 Access(ReferenceType value, MutexType &mutex, OtherLockArgs &&...otherLockArgs)
: lock(mutex, std::forward<OtherLockArgs>(otherLockArgs)...), m_value(value)
{
}
/**
* @brief Construct a read-only Access object from a const safe::Safe object and any additionnal argument needed
* to construct the Lock object.
*
* If needed, you can provide additionnal arguments to construct the lock object (such as std::adopt_lock). The
* mutex from the safe::Locakble object is already passed to the lock object's constructor though, you must not
* provide it.
*
* @tparam OtherLockArgs Deduced from otherLockArgs.
* @param safe The const Safe object to give protected access to.
* @param otherLockArgs Other arguments needed to construct the lock object.
*/
template <typename... OtherLockArgs>
EXPLICIT_IF_CPP17 Access(const Safe &safe, OtherLockArgs &&...otherLockArgs)
: Access(safe.m_value, safe.m_mutex.get, std::forward<OtherLockArgs>(otherLockArgs)...)
{
}
/**
* @brief Construct a read-write Access object from a safe::Safe object and any additionnal argument needed to
* construct the Lock object.
*
* If needed, you can provide additionnal arguments to construct the lock object (such as std::adopt_lock). The
* mutex from the safe object is already passed to the lock object's constructor though, you must not provide
* it.
*
* @tparam OtherLockArgs Deduced from otherLockArgs.
* @param safe The Safe object to give protected access to.
* @param otherLockArgs Other arguments needed to construct the lock object.
*/
template <typename... OtherLockArgs>
EXPLICIT_IF_CPP17 Access(Safe &safe, OtherLockArgs &&...otherLockArgs)
: Access(safe.m_value, safe.m_mutex.get, std::forward<OtherLockArgs>(otherLockArgs)...)
{
}
/**
* @brief Const accessor to the value.
* @return ConstPointerType Const pointer to the protected value.
*/
ConstPointerType operator->() const noexcept
{
return &m_value;
}
/**
* @brief Accessor to the value.
* @return ValuePointerType Pointer to the protected value.
*/
PointerType operator->() noexcept
{
return &m_value;
}
/**
* @brief Const accessor to the value.
* @return ConstValueReferenceType Const reference to the protected value.
*/
ConstReferenceType operator*() const noexcept
{
return m_value;
}
/**
* @brief Accessor to the value.
* @return ValueReferenceType Reference to the protected.
*/
ReferenceType operator*() noexcept
{
return m_value;
}
/// The lock that manages the mutex.
mutable LockType<RemoveRefMutexType> lock;
private:
/// The protected value.
ReferenceType m_value;
};
/// Reference-to-const ValueType.
using ConstValueReferenceType = const RemoveRefValueType &;
/// Reference to ValueType.
using ValueReferenceType = RemoveRefValueType &;
/// Reference to MutexType.
using MutexReferenceType = RemoveRefMutexType &;
struct UseLastArgumentForMutex
{
};
struct LastArgumentIsATag
{
};
public:
/// Aliases to ReadAccess and WriteAccess classes for this Safe class.
template <template <typename> class LockType = DefaultReadOnlyLockType>
using ReadAccess = Access<LockType, AccessMode::ReadOnly>;
template <template <typename> class LockType = DefaultReadWriteLockType>
using WriteAccess = Access<LockType, AccessMode::ReadWrite>;
/**
* @brief Construct a Safe object
*/
Safe() = default;
/**
* @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, add the
* default_construct_mutex tag as last argument.
*
* @tparam Args Deduced from args.
* @tparam SFINAE constraint.
* @param args Perfect forwarding arguments to split between the value and mutex.
*/
template <typename... Args,
typename std::enable_if<std::is_constructible<MutexType, Last<Args...>>::value, bool>::type = true>
explicit Safe(Args &&...args)
: Safe(UseLastArgumentForMutex(), std::forward_as_tuple(std::forward<Args>(args)...),
safe::impl::make_index_sequence<sizeof...(args) - 1>()) // delegate to a private constructor to split the
// parameter pack
{
}
/**
* @brief Construct a Safe object, forwarding all arguments to construct the value object. This constructor will be
* selected if the mutex cannot be constructed from the last argument of the parameter pack.
*
* @tparam Args Deduced from args.
* @param args Perfect forwarding arguments to construct the value object.
*/
template <typename... Args,
typename std::enable_if<!std::is_same<const impl::DefaultConstructMutex &, Last<Args...>>::value &&
!std::is_constructible<MutexType, Last<Args...>>::value,
bool>::type = true>
explicit Safe(Args &&...args) : m_mutex{}, m_value(std::forward<Args>(args)...)
{
}
/**
* @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.
* @param args Perfect forwarding arguments to construct the value object.
*/
template <typename... Args,
typename std::enable_if<std::is_same<const impl::DefaultConstructMutex &, Last<Args...>>::value,
bool>::type = true>
explicit Safe(Args &&...args)
: Safe(LastArgumentIsATag(), std::forward_as_tuple(std::forward<Args>(args)...),
safe::impl::make_index_sequence<sizeof...(args) - 1>()) // delegate to a private constructor to split the
// parameter pack
{
}
/// Delete all copy/move construction/assignment, as these operations require locking the mutex under the covers.
Safe(const Safe &) = delete;
Safe(Safe &&) = delete;
Safe &operator=(const Safe &) = delete;
Safe &operator=(Safe &&) = delete;
/**
* @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)...};
}
/**
* @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)...};
}
/**
* @brief Unsafe const accessor to the value. If you use this function, you exit the realm of safe!
*
* @return ConstValueReferenceType Const reference to the value object.
*/
ConstValueReferenceType unsafe() const noexcept
{
return m_value;
}
/**
* @brief Unsafe accessor to the value. If you use this function, you exit the realm of safe!
*
* @return ValueReferenceType Reference to the value object.
*/
ValueReferenceType unsafe() noexcept
{
return m_value;
}
/**
* @brief Accessor to the mutex.
*
* @return MutexReferenceType Reference to the mutex.
*/
MutexReferenceType mutex() const noexcept
{
return m_mutex.get;
}
private:
// The next two constructors are helper constructors to split the input arguments between the value and mutex
// constructors
template <typename ArgsTuple, size_t... AllButLast>
explicit Safe(const UseLastArgumentForMutex, ArgsTuple &&args, safe::impl::index_sequence<AllButLast...>)
: m_mutex{std::get<sizeof...(AllButLast)>(
std::forward<ArgsTuple>(args))}, // use the last argument to constuct the mutex
m_value(std::get<AllButLast>(std::forward<ArgsTuple>(args))...) // and the rest to construct the value
{
}
template <typename ArgsTuple, size_t... AllButLast>
explicit Safe(const LastArgumentIsATag, ArgsTuple &&args, safe::impl::index_sequence<AllButLast...>)
: m_mutex{}, // default construct the mutex since the last argument is a tag
m_value(std::get<AllButLast>(std::forward<ArgsTuple>(args))...) // use the rest to construct the value
{
}
/// The helper object that holds the mutable mutex, or a reference to a mutex.
impl::MutableIfNotReference<MutexType> m_mutex;
/// The value to protect.
ValueType m_value;
};
/**
* @brief Type alias for read-only Access.
*
* @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 = DefaultReadOnlyLockType>
using ReadAccess = typename SafeType::template ReadAccess<LockType>;
/**
* @brief Type alias for read-write Access.
*
* @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 = DefaultReadWriteLockType>
using WriteAccess = typename SafeType::template WriteAccess<LockType>;
} // namespace safe
#undef EXPLICIT_IF_CPP17
#undef EXPLICITLY_CONSTRUCT_RETURN_TYPE_IF_CPP17
================================================
FILE: tests/CMakeLists.txt
================================================
cmake_minimum_required(VERSION 3.9.2)
project(safe_tests LANGUAGES CXX)
# Detect if used in add_subdirectory() or install space
if(CMAKE_SOURCE_DIR STREQUAL PROJECT_SOURCE_DIR)
find_package(safe CONFIG REQUIRED)
endif()
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
include(Warnings)
include(add_package)
add_package(
doctest
doctest
https://github.com/onqtam/doctest
v2.4.9
scripts/cmake
)
add_executable(safe_tests test_main.cpp test_readme.cpp test_safe.cpp test_default_locks.cpp)
target_link_libraries(safe_tests PRIVATE safe::safe doctest::doctest)
target_set_warnings(safe_tests ENABLE ALL AS_ERROR ALL DISABLE Annoying)
target_compile_features(safe_tests INTERFACE cxx_std_17)
include(CTest)
include(doctest)
doctest_discover_tests(safe_tests)
================================================
FILE: tests/cmake/Warnings.cmake
================================================
# MIT License
# Copyright (c) 2017 Lectem
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
function(target_set_warnings)
if(NOT ENABLE_WARNINGS_SETTINGS)
return()
endif()
if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "MSVC")
set(WMSVC TRUE)
elseif ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU")
set(WGCC TRUE)
elseif ("${CMAKE_CXX_COMPILER_ID}" MATCHES "Clang")
set(WCLANG TRUE)
endif()
set(multiValueArgs ENABLE DISABLE AS_ERROR)
cmake_parse_arguments(this "" "" "${multiValueArgs}" ${ARGN})
list(FIND this_ENABLE "ALL" enable_all)
list(FIND this_DISABLE "ALL" disable_all)
list(FIND this_AS_ERROR "ALL" as_error_all)
if(NOT ${enable_all} EQUAL -1)
if(WMSVC)
# Not all the warnings, but WAll is unusable when using libraries
# Unless you'd like to support MSVC in the code with pragmas, this is probably the best option
list(APPEND WarningFlags "/W4")
elseif(WGCC)
list(APPEND WarningFlags "-Wall" "-Wextra" "-Wpedantic")
elseif(WCLANG)
list(APPEND WarningFlags "-Wall" "-Weverything" "-Wpedantic")
endif()
elseif(NOT ${disable_all} EQUAL -1)
set(SystemIncludes TRUE) # Treat includes as if coming from system
if(WMSVC)
list(APPEND WarningFlags "/w" "/W0")
elseif(WGCC OR WCLANG)
list(APPEND WarningFlags "-w")
endif()
endif()
list(FIND this_DISABLE "Annoying" disable_annoying)
if(NOT ${disable_annoying} EQUAL -1)
if(WMSVC)
# bounds-checked functions require to set __STDC_WANT_LIB_EXT1__ which we usually don't need/want
list(APPEND WarningDefinitions -D_CRT_SECURE_NO_WARNINGS)
# disable C4514 C4710 C4711... Those are useless to add most of the time
#list(APPEND WarningFlags "/wd4514" "/wd4710" "/wd4711")
#list(APPEND WarningFlags "/wd4365") #signed/unsigned mismatch
#list(APPEND WarningFlags "/wd4668") # is not defined as a preprocessor macro, replacing with '0' for
elseif(WGCC OR WCLANG)
list(APPEND WarningFlags -Wno-switch-enum)
if(WCLANG)
list(APPEND WarningFlags -Wno-unknown-warning-option -Wno-padded -Wno-undef -Wno-reserved-id-macro -fcomment-block-commands=test,retval)
if(NOT CMAKE_CXX_STANDARD EQUAL 98)
list(APPEND WarningFlags -Wno-c++98-compat -Wno-c++98-compat-pedantic)
endif()
if ("${CMAKE_CXX_SIMULATE_ID}" STREQUAL "MSVC") # clang-cl has some VCC flags by default that it will not recognize...
list(APPEND WarningFlags -Wno-unused-command-line-argument)
endif()
endif(WCLANG)
endif()
endif()
if(NOT ${as_error_all} EQUAL -1)
if(WMSVC)
list(APPEND WarningFlags "/WX")
elseif(WGCC OR WCLANG)
list(APPEND WarningFlags "-Werror")
endif()
endif()
foreach(target IN LISTS this_UNPARSED_ARGUMENTS)
if(WarningFlags)
target_compile_options(${target} PRIVATE ${WarningFlags})
endif()
if(WarningDefinitions)
target_compile_definitions(${target} PRIVATE ${WarningDefinitions})
endif()
if(SystemIncludes)
set_target_properties(${target} PROPERTIES
INTERFACE_SYSTEM_INCLUDE_DIRECTORIES $<TARGET_PROPERTY:${target},INTERFACE_INCLUDE_DIRECTORIES>)
endif()
endforeach()
endfunction(target_set_warnings)
================================================
FILE: tests/cmake/add_package.cmake
================================================
macro(add_package name depname git_repo git_rev mod_dir)
unset(${name}_FOUND CACHE) # needed for correct behaviour on rebuilds
if (DEPS STREQUAL "AUTO")
find_package (${name} CONFIG QUIET)
elseif (DEPS STREQUAL "LOCAL")
find_package (${name} REQUIRED CONFIG)
endif()
if (NOT ${name}_FOUND)
include(FetchContent)
FetchContent_Declare (${depname}
GIT_REPOSITORY ${git_repo}
GIT_TAG ${git_rev})
FetchContent_MakeAvailable (${depname})
FetchContent_GetProperties(${depname})
if(NOT ${depname}_POPULATED)
FetchContent_Populate(${depname})
add_subdirectory(${${depname}_SOURCE_DIR} ${${depname}_BINARY_DIR} EXCLUDE_FROM_ALL) # prevent installing of dependencies
endif()
set (${name}_POPULATED TRUE) # this is supposed to be done in MakeAvailable but it seems not to?!?
list(APPEND CMAKE_MODULE_PATH "${${depname}_SOURCE_DIR}/${mod_dir}")
endif()
endmacro()
================================================
FILE: tests/safe_with_custom_defaults.h
================================================
#pragma once
#include "safe/safe.h"
// Specialize DefaultLocks for any mutex types. This effectively specifies new default locks.
template<typename MutexType>
struct safe::impl::DefaultLocks<MutexType>
{
using ReadOnly = std::lock_guard<MutexType>;
using ReadWrite = std::unique_lock<MutexType>;
};
// Specialize DefaultLocks for a specific mutex type (here: std::timed_mutex).
template<>
struct safe::impl::DefaultLocks<std::timed_mutex>
{
using ReadOnly = std::unique_lock<std::timed_mutex>;
using ReadWrite = std::lock_guard<std::timed_mutex>;
};
================================================
FILE: tests/test_default_locks.cpp
================================================
// 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.
// To avoid any problem with specialized templates not being visible from every compilation unit, you should use the
// pattern shown here: create your own header file in which you include safe and define your template specializations.
// Only ever include your own header in your project.
#include "safe_with_custom_defaults.h"
#include <doctest/doctest.h>
#include <type_traits>
TEST_CASE("First specialization works for all mutexes")
{
static_assert(std::is_same<safe::DefaultReadOnlyLockType<std::mutex>, std::lock_guard<std::mutex>>::value, "Specialization did not work!");
static_assert(std::is_same<safe::DefaultReadWriteLockType<std::mutex>, std::unique_lock<std::mutex>>::value, "Specialization did not work!");
static_assert(std::is_same<safe::DefaultReadOnlyLockType<std::recursive_mutex>, std::lock_guard<std::recursive_mutex>>::value, "Specialization did not work!");
static_assert(std::is_same<safe::DefaultReadWriteLockType<std::recursive_mutex>, std::unique_lock<std::recursive_mutex>>::value, "Specialization did not work!");
}
TEST_CASE("Second specialization overrides even the first one")
{
static_assert(std::is_same<safe::DefaultReadOnlyLockType<std::timed_mutex>, std::unique_lock<std::timed_mutex>>::value, "Specialization did not work!");
static_assert(std::is_same<safe::DefaultReadWriteLockType<std::timed_mutex>, std::lock_guard<std::timed_mutex>>::value, "Specialization did not work!");
}
================================================
FILE: tests/test_main.cpp
================================================
#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
#include "doctest/doctest.h"
================================================
FILE: tests/test_readme.cpp
================================================
// Copyright (c) 2019-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.
#include "safe/safe.h"
#include <doctest/doctest.h>
#include <cassert>
#include <condition_variable>
#include <iostream>
#include <mutex>
#include <string>
TEST_CASE("Readme without safe example")
{
std::string foo; // do I need to lock a mutex to safely access this variable ?
std::string bar;
std::string baz; // what about this one ?
std::mutex fooMutex; // don't forget to change the name of this variable if foo's name changes!
std::mutex barMutex;
{
std::lock_guard<std::mutex> lock(fooMutex); // is this the right mutex for what I am about to do ?
bar = "Hello, World!"; // Hmm, did I just to something wrong ?
}
std::cout << bar << std::endl; // unprotected access, is this intended ?
std::cout << baz << std::endl; // what about this access ?
}
TEST_CASE("Readme with safe example")
{
#include "safe/safe.h"
safe::Safe<std::string> safeFoo; // std::string and mutex packaged together!
safe::Safe<std::string> safeBar;
std::string baz; // now you can see that this variable has no mutex
{
safe::WriteAccess<safe::Safe<std::string>> foo(safeFoo); // this locks the mutex and gives you access to foo
*foo = "Hello, World!"; // access the value using pointer dereference: * and ->
}
std::cout << safeBar.unsafe() << std::endl; // unprotected access: clearly expressed!
std::cout << baz << std::endl; // all good this is just a string!
}
TEST_CASE("Readme ref and non ref")
{
std::mutex mutex;
int value;
safe::Safe<int, std::mutex> valmut;
safe::Safe<int> valdef; // equivalent to the above, as the second template parameter defaults to std::mutex
safe::Safe<int &, std::mutex> refmut(value, safe::default_construct_mutex);
safe::Safe<int, std::mutex &> valref(42, mutex);
safe::Safe<int &, std::mutex &> refref(value, mutex);
CHECK_EQ(&refmut.unsafe(), &value);
CHECK_EQ(valref.unsafe(), 42);
CHECK_EQ(&valref.mutex(), &mutex);
CHECK_EQ(&refref.unsafe(), &value);
CHECK_EQ(&refref.mutex(), &mutex);
}
TEST_CASE("Readme default construct mutex tag")
{
std::mutex mutex;
safe::Safe<std::pair<int,int>, std::mutex> bothDefault; // mutex and value are default constructed
safe::Safe<std::pair<int,int>, std::mutex &> noDefault(42, 24, mutex); // mutex and value are initialized
safe::Safe<std::pair<int,int>, std::mutex &> valueDefault(mutex); // mutex is initialized, and value is default constructed
safe::Safe<std::pair<int,int>, std::mutex> mutexDefault(42, 24); // mutex is default constructed, and value is initialized
safe::Safe<std::pair<int,int>, std::mutex> mutexDefaultTag(42, 24, safe::default_construct_mutex); // mutex is default constructed, and value is initialized
CHECK_EQ(noDefault.unsafe(), std::pair<int,int>(42, 24));
CHECK_EQ(&noDefault.mutex(), &mutex);
CHECK_EQ(&valueDefault.mutex(), &mutex);
CHECK_EQ(mutexDefaultTag.unsafe(), std::pair<int,int>(42, 24));
}
TEST_CASE("Readme equivalent ways to construct lock")
{
safe::Safe<int> safeValue;
{
const safe::ReadAccess<safe::Safe<int>> value(safeValue);
}
{
const safe::Safe<int>::ReadAccess<> value(safeValue); // the empty <> are unfortunately necessary.
}
#if __cplusplus >= 201703L
{
const auto value = safeValue.readLock(); // nicer, but only with C++17 and later
}
#endif
{
const auto value = safeValue.readLock<std::unique_lock>(); // ok even pre-C++17, using a std::unique_lock rather than the default std::lock_guard
}
{
const safe::Safe<int>::ReadAccess<std::unique_lock> value(safeValue); // equivalent to the above
}
// All of the above exist in read-write versions, simply replace "read" by "write" (and remove the const).
}
TEST_CASE("Readme already locked mutex")
{
std::mutex aLockedMutex;
aLockedMutex.lock();
safe::Safe<int, std::mutex&> safeValue(aLockedMutex); // given a Safe object with an already locked mutex.
// Because the mutex is already locked, you need to pass the std::adopt_lock tag to std::lock_guard when you construct your Access object.
// No matter how you get your Access objects, you can pass arguments to the lock's constructor.
// Again, all the lines below are equivalent:
{
safe::WriteAccess<safe::Safe<int, std::mutex&>> value(safeValue, std::adopt_lock);
}
aLockedMutex.lock();
{
safe::Safe<int, std::mutex&>::WriteAccess<> value(safeValue, std::adopt_lock);
}
#if __cplusplus >= 201703L
aLockedMutex.lock();
{
auto value = safeValue.writeLock(std::adopt_lock); // again, only in C++17
}
#endif
aLockedMutex.lock();
{
auto value = safeValue.writeLock<std::unique_lock>(std::adopt_lock); // using a std::unique_lock still works
}
}
TEST_CASE("Readme timed mutex")
{
// Here's a safe int using a timed_mutex
safe::Safe<int, std::timed_mutex> safeValue;
// Pass in a time-out when you lock the mutex, and it will try to lock for that duration!
// safe does not do anything here, std::unique_lock does it all. safe simply forwards any argument
// it gets, and when std::unique_lock receives a std::duration, it try to lock before giving up.
auto value = safeValue.writeLock<std::unique_lock>(std::chrono::seconds(1));
// You can reach the lock through the Access object and check whether or not the locking was successful:
assert(value.lock.owns_lock());
}
TEST_CASE("Readme legacy")
{
std::mutex lousyMutex;
int unsafeValue;
// Wrap the existing variables
safe::Safe<int &, std::mutex &> safeValue(unsafeValue, lousyMutex);
// do not use lousyMutex and unsafeValue directly from here on!
}
TEST_CASE("Readme condition variable")
{
std::condition_variable cv;
safe::Safe<int> safeValue;
safe::Safe<int>::WriteAccess<std::unique_lock> value(safeValue);
cv.wait(value.lock, []() { return true; });
CHECK_EQ(&*value, &safeValue.unsafe());
CHECK_EQ(value.lock.mutex(), &safeValue.mutex());
}
================================================
FILE: tests/test_safe.cpp
================================================
// Copyright (c) 2018-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.
#include "safe/safe.h"
#include <doctest/doctest.h>
#include <mutex>
TEST_CASE("If mutex is constructible, construct with the last argument even it if could construct the value")
{
safe::Safe<int, int> weirdSafeInt(42);
CHECK_EQ(weirdSafeInt.unsafe(), 0);
CHECK_EQ(weirdSafeInt.mutex(), 42);
}
TEST_CASE("If tag is passed, do not use the argument to construct the mutex")
{
safe::Safe<int, int> weirdSafeInt(42, safe::default_construct_mutex);
CHECK_EQ(weirdSafeInt.unsafe(), 42);
CHECK_EQ(weirdSafeInt.mutex(), 0);
}
TEST_CASE("If tag is passed, do not use the argument to construct the mutex")
{
safe::Safe<int, std::mutex> weirdSafeInt(42);
CHECK_EQ(weirdSafeInt.unsafe(), 42);
}
gitextract_h1y9d_x0/
├── .clang-format
├── .github/
│ └── workflows/
│ └── all_platforms.yml
├── .gitignore
├── CMakeLists.txt
├── LICENSE
├── README.md
├── cmake/
│ ├── InstallTarget.cmake
│ └── safeConfig.cmake.in
├── include/
│ └── safe/
│ ├── access_mode.h
│ ├── default_locks.h
│ ├── meta.h
│ ├── mutable_ref.h
│ └── safe.h
└── tests/
├── CMakeLists.txt
├── cmake/
│ ├── Warnings.cmake
│ └── add_package.cmake
├── safe_with_custom_defaults.h
├── test_default_locks.cpp
├── test_main.cpp
├── test_readme.cpp
└── test_safe.cpp
SYMBOL INDEX (6 symbols across 6 files) FILE: include/safe/access_mode.h function namespace (line 15) | namespace safe FILE: include/safe/default_locks.h function namespace (line 12) | namespace safe FILE: include/safe/meta.h function namespace (line 12) | namespace safe FILE: include/safe/mutable_ref.h function namespace (line 12) | namespace safe FILE: include/safe/safe.h function namespace (line 26) | namespace safe FILE: tests/safe_with_custom_defaults.h function timed_mutex (line 15) | struct safe::impl::DefaultLocks<std::timed_mutex>
Condensed preview — 21 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (62K chars).
[
{
"path": ".clang-format",
"chars": 5002,
"preview": "---\nLanguage: Cpp\n# BasedOnStyle: Microsoft\nAccessModifierOffset: -2\nAlignAfterOpenBracket: Align\nAlignArrayOfSt"
},
{
"path": ".github/workflows/all_platforms.yml",
"chars": 1292,
"preview": "name: All Platforms\n\non: [push]\n\njobs:\n build:\n runs-on: ${{ matrix.os }}\n strategy:\n matrix:\n os: [u"
},
{
"path": ".gitignore",
"chars": 66,
"preview": "build*/\ninstall*/\ncompile_commands.json\n.vscode/\n.cache/\nTesting/\n"
},
{
"path": "CMakeLists.txt",
"chars": 1018,
"preview": "cmake_minimum_required(VERSION 3.9.2)\n\ncmake_policy(SET CMP0048 NEW)\nproject(safe VERSION 1.1.0 LANGUAGES CXX)\noption(BU"
},
{
"path": "LICENSE",
"chars": 1081,
"preview": "MIT License\n\nCopyright (c) 2019-2023 Louis-Charles Caron\n\nPermission is hereby granted, free of charge, to any person ob"
},
{
"path": "README.md",
"chars": 14754,
"preview": "# *Every variable protected by a mutex should be wrapped with safe.*\n[\nwrite_basic_package_version_file(\n\t\"${PROJECT_NAME}ConfigVersion.cmake\"\n\tVERSION ${PR"
},
{
"path": "cmake/safeConfig.cmake.in",
"chars": 124,
"preview": "@PACKAGE_INIT@\n\ninclude(\"${CMAKE_CURRENT_LIST_DIR}/@PROJECT_NAME@Targets.cmake\")\ncheck_required_components(\"@PROJECT_NAM"
},
{
"path": "include/safe/access_mode.h",
"chars": 1475,
"preview": "// Copyright (c) 2019-2023 Louis-Charles Caron\n\n// This file is part of the safe library (https://github.com/LouisCharle"
},
{
"path": "include/safe/default_locks.h",
"chars": 1233,
"preview": "// Copyright (c) 2019-2022 Louis-Charles Caron\n\n// This file is part of the safe library (https://github.com/LouisCharle"
},
{
"path": "include/safe/meta.h",
"chars": 1350,
"preview": "// Copyright (c) 2023 Louis-Charles Caron\n\n// This file is part of the safe library (https://github.com/LouisCharlesC/sa"
},
{
"path": "include/safe/mutable_ref.h",
"chars": 995,
"preview": "// Copyright (c) 2019-2022 Louis-Charles Caron\n\n// This file is part of the safe library (https://github.com/LouisCharle"
},
{
"path": "include/safe/safe.h",
"chars": 14144,
"preview": "// Copyright (c) 2019-2023 Louis-Charles Caron\n\n// This file is part of the safe library (https://github.com/LouisCharle"
},
{
"path": "tests/CMakeLists.txt",
"chars": 785,
"preview": "cmake_minimum_required(VERSION 3.9.2)\n\nproject(safe_tests LANGUAGES CXX)\n\n# Detect if used in add_subdirectory() or inst"
},
{
"path": "tests/cmake/Warnings.cmake",
"chars": 4407,
"preview": "# MIT License\n\n# Copyright (c) 2017 Lectem\n\n# Permission is hereby granted, free of charge, to any person obtaining a co"
},
{
"path": "tests/cmake/add_package.cmake",
"chars": 1005,
"preview": "macro(add_package name depname git_repo git_rev mod_dir)\n unset(${name}_FOUND CACHE) # needed for correct behaviour o"
},
{
"path": "tests/safe_with_custom_defaults.h",
"chars": 569,
"preview": "#pragma once\n\n#include \"safe/safe.h\"\n\n// Specialize DefaultLocks for any mutex types. This effectively specifies new def"
},
{
"path": "tests/test_default_locks.cpp",
"chars": 1721,
"preview": "// Copyright (c) 2023 Louis-Charles Caron\n\n// This file is part of the safe library (https://github.com/LouisCharlesC/sa"
},
{
"path": "tests/test_main.cpp",
"chars": 71,
"preview": "#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN\n#include \"doctest/doctest.h\""
},
{
"path": "tests/test_readme.cpp",
"chars": 5970,
"preview": "// Copyright (c) 2019-2023 Louis-Charles Caron\n\n// This file is part of the safe library (https://github.com/LouisCharle"
},
{
"path": "tests/test_safe.cpp",
"chars": 999,
"preview": "// Copyright (c) 2018-2023 Louis-Charles Caron\n\n// This file is part of the safe library (https://github.com/LouisCharle"
}
]
About this extraction
This page contains the full source code of the LouisCharlesC/safe GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 21 files (57.9 KB), approximately 14.8k tokens, and a symbol index with 6 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.