Skip to content

Commit d5c9493

Browse files
authored
Merge pull request #2 from cschreib/wasm
Introduce `observable_sealed_ptr`, improve memory footprint in C++17
2 parents b7721b1 + d8c7e3f commit d5c9493

15 files changed

+1970
-258
lines changed

.github/workflows/cmake.yml

+30-10
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,26 @@ name: CI
33
on: [push, pull_request]
44

55
env:
6-
BUILD_TYPE: Release
6+
EM_VERSION: 2.0.16
7+
EM_CACHE_FOLDER: 'emsdk-cache'
78

89
jobs:
910
build:
1011
strategy:
1112
fail-fast: false
1213
matrix:
1314
platform:
14-
- { name: Ubuntu GCC, os: ubuntu-latest, compiler: g++, arch: "64", flags: ""}
15-
- { name: Ubuntu Clang, os: ubuntu-latest, compiler: clang++, arch: "64", flags: ""}
16-
- { name: Windows 32, os: windows-latest, compiler: vs2019, arch: "32", flags: "-A Win32"}
17-
- { name: Windows 64, os: windows-latest, compiler: vs2019, arch: "64", flags: "-A x64"}
18-
- { name: MacOS, os: macos-latest, compiler: clang++, arch: "64", flags: ""}
15+
- { name: Ubuntu GCC, os: ubuntu-latest, compiler: g++, arch: "64", cmakepp: "", flags: ""}
16+
- { name: Ubuntu Clang, os: ubuntu-latest, compiler: clang++, arch: "64", cmakepp: "", flags: ""}
17+
- { name: Windows 32, os: windows-latest, compiler: vs2019, arch: "32", cmakepp: "", flags: "-A Win32"}
18+
- { name: Windows 64, os: windows-latest, compiler: vs2019, arch: "64", cmakepp: "", flags: "-A x64"}
19+
- { name: MacOS, os: macos-latest, compiler: clang++, arch: "64", cmakepp: "", flags: ""}
20+
- { name: WebAssembly, os: ubuntu-latest, compiler: em++, arch: "32", cmakepp: "emcmake", flags: "-DCMAKE_CXX_FLAGS=\"-s DISABLE_EXCEPTION_CATCHING=0\" -DCMAKE_CROSSCOMPILING_EMULATOR=node"}
21+
build-type:
22+
- Release
23+
- Debug
1924

20-
name: ${{matrix.platform.name}}
25+
name: ${{matrix.platform.name}} ${{matrix.build-type}}
2126
runs-on: ${{matrix.platform.os}}
2227

2328
steps:
@@ -30,21 +35,36 @@ jobs:
3035
if: runner.os == 'Linux'
3136
run: export CXX=${{matrix.platform.compiler}}
3237

38+
- name: Setup Emscripten cache
39+
if: matrix.platform.compiler == 'em++'
40+
id: cache-system-libraries
41+
uses: actions/cache@v2
42+
with:
43+
path: ${{env.EM_CACHE_FOLDER}}
44+
key: ${{env.EM_VERSION}}-${{ runner.os }}
45+
46+
- name: Setup Emscripten
47+
if: matrix.platform.compiler == 'em++'
48+
uses: mymindstorm/setup-emsdk@v7
49+
with:
50+
version: ${{env.EM_VERSION}}
51+
actions-cache-folder: ${{env.EM_CACHE_FOLDER}}
52+
3353
- name: Create Build Environment
3454
run: cmake -E make_directory ${{github.workspace}}/build
3555

3656
- name: Configure CMake
3757
shell: bash
3858
working-directory: ${{github.workspace}}/build
39-
run: cmake .. -DCMAKE_BUILD_TYPE=${BUILD_TYPE} ${{matrix.platform.flags}} -DOUP_DO_TEST=1
59+
run: ${{matrix.platform.cmakepp}} cmake .. -DCMAKE_BUILD_TYPE=${{matrix.build-type}} ${{matrix.platform.flags}} -DOUP_DO_TEST=1
4060

4161
- name: Build
4262
shell: bash
4363
working-directory: ${{github.workspace}}/build
44-
run: cmake --build . --config ${BUILD_TYPE} --parallel 2
64+
run: cmake --build . --config ${{matrix.build-type}} --parallel 2
4565

4666
- name: Test
4767
shell: bash
4868
working-directory: ${{github.workspace}}/build
49-
run: ctest
69+
run: ctest --output-on-failure
5070

README.md

+90-18
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,29 @@
1-
# observable_unique_ptr<T, Deleter>
1+
# observable_unique_ptr<T, Deleter>, observable_sealed_ptr<T>, observer_ptr<T>
2+
3+
![Build Status](https://github.com/cschreib/observable_unique_ptr/actions/workflows/cmake.yml/badge.svg) ![Docs Build Status](https://github.com/cschreib/observable_unique_ptr/actions/workflows/doc.yml/badge.svg)
4+
5+
Built and tested on:
6+
- Linux (GCC/clang)
7+
- Windows (MSVC 32/64)
8+
- MacOS (clang)
9+
- WebAssembly (Emscripten)
210

3-
![Build Status](https://github.com/cschreib/observable_unique_ptr/actions/workflows/cmake.yml/badge.svg) ![Build Status](https://github.com/cschreib/observable_unique_ptr/actions/workflows/doc.yml/badge.svg)
411

512
## Introduction
613

7-
This is a small header-only library, providing a unique-ownership smart pointer `observable_unique_ptr` that can be observed with non-owning pointers `observer_ptr`. It is a mixture of `std::unique_ptr` and `std::shared_ptr`: it borrows the unique-ownership semantic of `std::unique_ptr` (movable, non-copiable), but allows creating `observer_ptr` to monitor the lifetime of the pointed object (like `std::weak_ptr` for `std::shared_ptr`).
14+
This is a small header-only library, providing the unique-ownership smart pointers `observable_unique_ptr` and `observable_sealed_ptr` that can be observed with non-owning pointers `observer_ptr`. This is a mixture of `std::unique_ptr` and `std::shared_ptr`: it borrows the unique-ownership semantic of `std::unique_ptr` (movable, non-copiable), but allows creating `observer_ptr` to monitor the lifetime of the pointed object (like `std::weak_ptr` for `std::shared_ptr`).
815

9-
This is useful for cases where the shared-ownership of `std::shared_ptr` is not desirable, e.g., when lifetime must be carefully controlled and not be allowed to extend, yet non-owning/weak/observer references to the object may exist after the object has been deleted.
16+
The only difference between `observable_unique_ptr` and `observable_sealed_ptr` is that the former can release ownership, while the latter cannot. Disallowing release of ownership enables allocation optimizations. Therefore, the recommendation is to use `observable_sealed_ptr` unless release of ownership is required.
1017

11-
Note: Because of the unique ownership model, observer pointers cannot extend the lifetime of the pointed object, hence `observable_unique_ptr`/`observer_ptr` provides less thread-safety compared to `std::shared_ptr`/`std::weak_ptr`. This is also true of `std::unique_ptr`, and is a fundamental limitation of unique ownership. If this is an issue, you will need either to add your own explicit locking logic, or use `std::shared_ptr`/`std::weak_ptr`.
18+
These pointers are useful for cases where the shared-ownership of `std::shared_ptr` is not desirable, e.g., when lifetime must be carefully controlled and not be allowed to extend, yet non-owning/weak/observer references to the object may exist after the object has been deleted.
19+
20+
Note: Because of the unique ownership model, observer pointers cannot extend the lifetime of the pointed object, hence this library provides less thread-safety compared to `std::shared_ptr`/`std::weak_ptr`. This is also true of `std::unique_ptr`, and is a fundamental limitation of unique ownership. If this is an issue, you will need either to add your own explicit locking logic, or use `std::shared_ptr`/`std::weak_ptr`.
1221

1322

1423
## Usage
1524

1625
This is a header-only library requiring a C++17-compliant compiler. You have multiple ways to set it up:
17-
- just include this repository as a submodule in your own git repository and use CMake `add_subdirectory`,
18-
- use CMake `FetchContent`,
26+
- just include this repository as a submodule in your own git repository and use CMake `add_subdirectory` (or use CMake `FetchContent`), then link with `target_link_libraries(<your-target> PUBLIC oup::oup)`.
1927
- download the header and include it in your own sources.
2028

2129
From there, include the single header `<oup/observable_unique_ptr.hpp>`, and directly use the smart pointer in your own code:
@@ -33,7 +41,7 @@ int main() {
3341

3442
{
3543
// Unique pointer that owns the object
36-
auto owner_ptr = oup::make_observable_unique<std::string>("hello");
44+
auto owner_ptr = oup::make_observable_sealed<std::string>("hello");
3745

3846
// Make the observer pointer point to the object
3947
obs_ptr = owner_ptr;
@@ -64,20 +72,84 @@ int main() {
6472

6573
## Limitations
6674

67-
The follownig limitations are features that were not implemented simply because of lack of motivation.
68-
69-
- `observable_unique_ptr` does not support pointers to arrays, but `std::unique_ptr` and `std::shared_ptr` both do.
70-
- `observable_unique_ptr` does not support custom allocators, but `std::shared_ptr` does.
75+
The following limitations are features that were not implemented simply because of lack of motivation.
76+
77+
- this library does not support pointers to arrays, but `std::unique_ptr` and `std::shared_ptr` both do.
78+
- this library does not support custom allocators, but `std::shared_ptr` does.
79+
80+
81+
## Comparison spreadsheet
82+
83+
In this comparison spreadsheet, the raw pointer `T*` is assumed to never be owning, and used only to observe an existing object (which may or may not have been deleted). The stack and heap sizes were measured with gcc 9.3.0 and libstdc++.
84+
85+
Labels:
86+
- raw: `T*`
87+
- unique: `std::unique_ptr<T>`
88+
- weak: `std::weak_ptr<T>`
89+
- shared: `std::shared_ptr<T>`
90+
- observer: `oup::observer_ptr<T>`
91+
- obs_unique: `oup::observable_unique_ptr<T>`
92+
- obs_sealed: `oup::observable_sealed_ptr<T>`
93+
94+
| Pointer | raw | weak | observer | unique | shared | obs_unique | obs_sealed |
95+
|--------------------------|------|--------|----------|--------|--------|------------|------------|
96+
| Owning | no | no | no | yes | yes | yes | yes |
97+
| Releasable | N/A | N/A | N/A | yes | no | yes | no |
98+
| Observable deletion | no | yes | yes | yes | yes | yes | yes |
99+
| Thread-safe deletion | no | yes | no(1) | yes(2) | yes | yes(2) | yes(2) |
100+
| Atomic | yes | no(3) | no | no | no(3) | no | no |
101+
| Support arrays | yes | yes | no | yes | yes | no | no |
102+
| Support custom allocator | N/A | yes | no | yes | yes | no | no |
103+
| Support custom deleter | N/A | N/A | N/A | yes | yes(4) | yes | no |
104+
| Number of heap alloc. | 0 | 0 | 0 | 1 | 1/2(5) | 2 | 1 |
105+
| Size in bytes (64 bit) | | | | | | | |
106+
| - Stack (per instance) | 8 | 16 | 16 | 8 | 16 | 16 | 16 |
107+
| - Heap (shared) | 0 | 0 | 0 | 0 | 24 | 8 | 8 |
108+
| - Total | 8 | 16 | 16 | 8 | 40 | 24 | 24 |
109+
| Size in bytes (32 bit) | | | | | | | |
110+
| - Stack (per instance) | 4 | 8 | 8 | 4 | 8 | 8 | 8 |
111+
| - Heap (shared) | 0 | 0 | 0 | 0 | 16 | 8 | 8 |
112+
| - Total | 4 | 8 | 8 | 4 | 24 | 16 | 16 |
113+
114+
Notes:
115+
116+
- (1) If `expired()` returns true, the pointer is guaranteed to remain `nullptr` forever, with no race condition. If `expired()` returns false, the pointer could still expire on the next instant, which can lead to race conditions.
117+
- (2) By construction, only one thread can own the pointer, therefore deletion is thread-safe.
118+
- (3) Yes if using `std::atomic<std::shared_ptr<T>>` and `std::atomic<std::weak_ptr<T>>`.
119+
- (4) Not if using `std::make_shared()`.
120+
- (5) 2 by default, or 1 if using `std::make_shared()`.
121+
122+
123+
## Speed benchmarks
124+
125+
Labels are the same as in the comparison spreadsheet. The speed benchmarks were compiled with gcc 9.3.0 and libstdc++, with all optimizations turned on (except LTO), and run on a linux (5.1.0-89) machine with a Ryzen 5 2600 CPU. Speed is measured relative to `std::unique_ptr<T>` used as owner pointer, and `T*` used as observer pointer, which should be the fastest possible implementation (but obviously the one with least safety).
126+
127+
You can run the benchmarks yourself, they are located in `tests/speed_benchmark.cpp`. The benchmark executable runs tests for three object types: `int`, `float`, `std::string`, and `std::array<int,65'536>`, to simulate objects of various allocation cost. The timings below are the worst-case values measured across all object types, which should be most relevant to highlight the overhead from the pointer itself (and erases flukes from the benchmarking framework). In real life scenarios, the actual measured overhead will be substantially lower, as actual business logic is likely to dominate the time budget.
128+
129+
| Pointer | raw/unique | weak/shared | observer/obs_unique | observer/obs_sealed |
130+
|--------------------------|------------|-------------|---------------------|---------------------|
131+
| Create owner empty | 1 | 1.1 | 1.1 | 1.1 |
132+
| Create owner | 1 | 2.2 | 1.9 | N/A |
133+
| Create owner factory | 1 | 1.3 | 1.8 | 1.3 |
134+
| Dereference owner | 1 | 1 | 1 | 1 |
135+
| Create observer empty | 1 | 1.2 | 1.2 | 1.3 |
136+
| Create observer | 1 | 1.5 | 1.6 | 1.6 |
137+
| Create observer copy | 1 | 1.7 | 1.7 | 1.7 |
138+
| Dereference observer | 1 | 4.8 | 1.2 | 1.3 |
139+
140+
Detail of the benchmarks:
141+
- Create owner empty: default-construct an owner pointer (to nullptr).
142+
- Create owner: construct an owner pointer by taking ownership of an object (for `oup::observer_sealed_ptr`, this is using `oup::make_observable_sealed()`).
143+
- Create owner factory: construct an owner pointer using `std::make_*` or `oup::make_*` factory functions.
144+
- Dereference owner: get a reference to the underlying owned object from an owner pointer.
145+
- Create observer empty: default-construct an observer pointer (to nullptr).
146+
- Create observer: construct an observer pointer from an owner pointer.
147+
- Create observer copy: construct a new observer pointer from another observer pointer.
148+
- Dereference observer: get a reference to the underlying object from an observer pointer.
71149

72150

73151
## Notes
74152

75-
76153
### Alternative implementation
77154

78155
An alternative implementation of an "observable unique pointer" can be found [here](https://www.codeproject.com/articles/1011134/smart-observers-to-use-with-unique-ptr). It does not compile out of the box with gcc unfortunately, but it does contain more features (like creating an observer pointer from a raw `this`) and lacks others (their `make_observable` always performs two allocations). Have a look to check if this better suits your needs.
79-
80-
81-
### ABI compatibility
82-
83-
When compiled in C++20 mode, by default the implementation will attempt to optimize empty deleters. This is not ABI-compatible with previous versions of C++, which lack the `[[no_unique_address]]` attribute (introduced in C++20). If ABI compatibility with previous versions of C++ is a concern to you, please define the macro `OUP_CPP17_ABI_COMPAT` before including the header of this library. This will disable the empty deleter optimisation, and enable binary compatibility with older C++ versions.

0 commit comments

Comments
 (0)