Oct 2024
"SaferCPlusPlus" is essentially a collection of safe data types intended to facilitate memory and data race safe C++ programming. This library is intended to work with a safety assuring static analyzer like scpptool and, optionally, the Core Guidelines lifetime checker, over their various stages of development and availability. (Including situations where they are not available at all.)
The library's elements are designed, as much as possible, to seamlessly integrate with all manner of existing and future C++ code. It includes things like:
-
Drop-in replacements for
std::vector<>
,std::array<>
andstd::string
. -
Replacements for
std::string_view
andstd::span
. -
Drop-in replacements for
int
,size_t
andbool
that ensure against the use of uninitialized values and address the "signed-unsigned mismatch" issues. -
Data types for safe sharing of objects among concurrently executing threads.
-
Replacements for native pointers/references with various flexibility and performance trade-offs.
While a "static safety analyzer/enforcer" like the scpptool would be required to ensure complete safety, the SaferCPlusPlus library elements have a lot of safety enforcement already built in. The library is extensive enough that most existing uses of unsafe C++ elements can be replaced.
Besides zero-overhead pointers that enforce some of the necessary restrictions that would be imposed by a complete "static safety analyzer/enforcer", the library provides a reference counting pointer that's smaller and faster than std::shared_ptr<>
, and an unrestricted pointer that ensures memory safety via run-time checks. The latter two being not (yet) provided by the Guidelines Support Library, but valuable in the context of having to work around the somewhat draconian restrictions imposed by the (eventual completed) lifetime checker.
To see the library in action, you can check out some benchmark code. There you can compare traditional C++ and (high-performance) SaferCPlusPlus implementations of the same algorithms. Also, the msetl_example.cpp and msetl_example2.cpp files contain usage examples of the library's elements. But at this point, there are a lot of them, so it might be more effective to peruse the documentation first, then search those files for the element(s) your interested in. (An online interactive version of these examples is also available, but the whole collection is large enough that the build will likely time-out. Often, the documentation for individual library elements will include a link to a more specific interactive example that should build fine.)
Elements in this library are currently based on the C++17 version of their counterpart APIs. (C++14 is still supported.)
Tested with the microsoft compiler (v.19.41.34120) (Windows 10), g++13.2.0 and clang++18.1.3 (Ubuntu 24.04.1). Versions of g++ prior to version 5 are not supported. Last tested with Apple clang++ version 12.0.0 (macOS Catalina v10.15.7) on Aug 18, 2021. Apple clang++ is not currently a regular test target. Some (generally "less commonly used") features are not available with Apple clang++ (for example, some uses of std::tie
). With the microsoft compiler, compiling in "conformance" mode (/permissive-) (which is not the default when using C++17 or lower) is recommended.
-
Comparisons
-
Norad pointers
-
Single owner pointer
-
Multithreading
- Overview
- TAsyncPassableObj
- thread
- async()
- Asynchronously shared objects
- Scope threads
- access controlled objects
- xscope_thread_carrier
- xscope_future_carrier
- make_xscope_asyncsharedv2acoreadwrite()
- make_xscope_aco_locker_for_sharing()
- make_xscope_exclusive_strong_pointer_store_for_sharing()
- TXScopeExclusiveStrongPointerStoreForAccessControl
- exclusive writer objects
- scope atomics
- TXScopeACORASectionSplitter and TXScopeAsyncACORASectionSplitter
- static and global variables
-
Tuples
The library was designed to help reduce or eliminate the potential for invalid memory accesses and data races in general C++ code. The general strategy is simply to substitute potentially unsafe C++ elements with compatible safe replacements from the library. The library does not impose any particular paradigm or code structure. (Though more modern coding styles that de-emphasize explicit use of iterators may result in better performance.)
When using a static safety enforcer (like the scpptool and/or the Core Guidelines lifetime checker, when completed), some of the most used elements of the library (namely the "scope" pointer elements) may be rendered largely redundant. Code using the pointer/reference types in this library should, unlike "regular" C++ code, already be compliant with the restrictions that will be imposed by such a completed static safety enforcer. So you can think of the use of this library as a method of "future-proofing" your code for a time when it may become standard practice to automatically reject C++ code that isn't approved by a static safety enforcer.
While using the library can incur a modest performance penalty, because the library elements are largely compatible with their native counterparts, they can be easily "disabled" (automatically replaced with their native counterparts) with a compile-time directive, allowing them, for example, to be used to help catch bugs in debug/test/beta builds while incurring no overhead in release builds.
And note that the safe components of this library can be adopted completely incrementally. New code written with these safe elements will play nicely with existing code, and native C++ elements can be replaced selectively without breaking the existing code. So there is really no excuse for not using the library in pretty much any situation. Though for real time embedded applications, note the dependence on the standard library.
Using this (header-only) library generally involves copying the include files you want to use into your project, and that's it. Outside of the standard library, there are no other dependencies.
Building the example: For those using msvc, project files are included. Otherwise, just create a new project and add all the .cpp
and .h
files.
A couple of notes about compiling: With g++ and clang++, you'll need to enable thread support (-pthread). With the microsoft compiler, compiling in "conformance" mode (/permissive-) (which is not the default when using C++17 or lower) is recommended. Additionally defining the MSE_MSC_CONFORMANCE_MODE
preprocessor symbol suppresses work-arounds used by the library to accommodate permissive mode when using the microsoft compiler with C++17 or lower. With 64-bit builds you may get a "fatal error C1128: number of sections exceeded object file format limit: compile with /bigobj". Just add the "/bigobj" compile flag. For more help you can try the questions and comments section. Also related: Versioning and deployment philosophy.
The Clang/LLVM compiler provides a set of "sanitizers" (adopted by gcc) that address C/C++ "code safety" issues. While they address many of the same bugs, the solutions provided by the SaferCPlusPlus library and the Clang/LLVM sanitizers differ in significant ways. Namely (as of Sep 2016):
- The Clang/LLVM sanitizers require modifications to the build process, not the code, whereas with SaferCPlusPlus it's the other way around.
- SaferCPlusPlus can more completely solve the problem of invalid memory access, but in doing so does not support certain potentially dangerous language features (like pointer arithmetic).
- When encountering an invalid memory operation at run-time, the Clang/LLVM sanitizers terminate the executable, where SaferCPlusPlus, by default, throws a (catchable) exception, but supports any user-defined action, including program termination.
- SaferCPlusPlus is portable C++ code that works on any platform, whereas Clang/LLVM sanitizers are available/maintained on a finite (but at the moment, ample) set of OS-architecture combinations.
- The Clang/LLVM sanitizers cost more in terms of run-time performance. ~2x slowdown for the AddressSanitizer, for example. SaferCPlusPlus generally has substantially lower performance costs, mainly because the Clang/LLVM sanitizers cannot assume any cooperation from the source code, so they have to instrument pretty much every allocated piece of memory and check pretty well every pointer dereference.
- SaferCPlusPlus supports the mixing of "safe" and (high-performance) "unsafe" code at a granular level, where Clang/LLVM Sanitizers apply to entire modules, or as in the case of the MemorySanitizer, all modules, requiring recompilation of any linked libraries.
- Clang's ThreadSanitizer tries to detect data race bugs, while SaferCPlusPlus provides data types that eliminate the possibility of data race bugs (and a superset we call "object race" bugs).
Clang/LLVM Sanitizers are intended for debugging purposes, not to be used in deployed executables. As such, by design, some of their debugging convenience features themselves introduce opportunities for malicious exploitation. SaferCPlusPlus on the other hand, is designed to be used in deployed executables, as well as for debugging and testing. And that's reflected in its performance, security and "completeness of solution". But SaferCPlusPlus and the Clang/LLVM Sanitizers are not incompatible, and there's no reason you couldn't use both simultaneously, although there would be significant redundancies.
C++ and Rust differ significantly in many ways, but SaferCPlusPlus is primarily concerned with addressing memory safety so here we'll consider only that aspect. Given that, what's most notable is the similarities between SaferCPlusPlus and Rust, considering they were developed independently.
Probably the main issue when it comes to memory safety is the relationship between pointers/references and object deallocation. So it's notable that Rust and SaferCPlusPlus decided on roughly corresponding sets of (safe) pointer/reference types:
Rust | SaferCPlusPlus |
---|---|
non-reassignable reference | scope pointer |
reassignable (mut) reference | registered pointer |
Box<> | scope owner pointer |
Rc<> | reference counting pointer |
Arc<> | shared immutable pointer |
Arc< RwLock<> > | access requester |
Indeed, if you are a Rust programmer you might be more comfortable using SaferCPlusPlus than traditional C++.
The most commonly used reference type, the non-reassignable (non-mut) reference in Rust and the scope pointer in SaferCPlusPlus, incurs no run-time overhead in both cases, which is a primary reason for the excellent performance of both solutions.
Reassignable (mut) references occur much less frequently, but currently, safe, reassignable, zero-overhead pointers in C++ are even more restricted than their counterpart in Rust. However, the SaferCPlusPlus library also provides pointers with run-time safety mechanisms and greater flexibility, for which there are no Rust counterparts (due to Rust's lack of move constructors). So for example, "self-referencing" data types (that are movable and can be allocated on the stack) are straightforward to implement in C++'s memory safe subset in a way that's not really available in Rust.
Probably the most noticeable difference though, is that SaferCPlusPlus does not (universally) restrict the number and type of references to an object that can exist at one time (i.e. the "exclusivity of mutable references") the way Rust does. With respect to memory safety, the benefit of this restriction is that it ensures that objects with "arbitrary lifespan" (like an element in a (resizable) vector) are not decommissioned while other references to that object still exist. But most objects do not have "arbitrary lifespan" (both Rust and SaferCPlusPlus encourage most objects to have "scope lifespan"), so most of the time, from a memory safety perspective, this restriction is not necessary. So SaferCPlusPlus (and the lifetime checker too) are more selective about what restrictions are applied and when.
There are situations where restrictions on object accessibility can be beneficial or essential for reasons other than (single-threaded) memory safety. (To help ensure data race safety of asynchronously shared objects, for example.) For those cases, the library does provide facilities for the enforcement of "access control" restrictions, including essentially the equivalent of Rust's RefCell
wrapper.
Overall though, there's probably more commonality than difference between the Rust and the SaferCPlusPlus memory safety strategies. At least compared to other current languages. So, perhaps as expected, you could think of the comparison between SaferCPlusPlus and Rust as essentially the comparison between C++ and Rust, with diminished discrepancies in memory safety and performance.
"Checked C", like SaferCPlusPlus, takes the approach of extending the language with safer elements that can directly substitute for unsafe native elements. In chapter 9 of their spec, there is an extensive survey of existing (and historical) efforts to address C/C++ memory safety. There they make the argument for the (heretofore neglected) "language extension" approach (basically citing performance, compatibility and the support for granular mixing of safe and unsafe code), that applies to SaferCPlusPlus as well.
Checked C and SaferCPlusPlus are more complementary than competitive. Checked C targets low-level system C code and basically only addresses the array bounds checking issue, including pointer arithmetic, where SaferCPlusPlus skews more toward C++ code and legacy code that would benefit from being converted to modern C++. It seems that Checked C is not yet ready for deployment (as of Sep 2016), but one could imagine both solutions being used, with little contention, in projects that have both low-level system type code and higher-level application type code.
The elements in this library are straightforward enough that a separate tutorial, beyond the examples given in the documentation, is probably not necessary. But if you're wondering how best to start, probably the easiest and most effective thing to do is to replace the vectors and arrays in your code (that aren't being shared between threads) with mse::mstd::vector<>
and mse::mstd::array<>
. You can substitute std::string_view
with mse::nrp_string_view
, and your std::string
s with mse::mstd::string
.
Statistically speaking, doing this should already catch a significant chunk of potential memory bugs. By default, an exception will be thrown upon any attempt to access invalid memory (or the program will terminate if compiled with support for exceptions disabled). This behavior can be customized (for example, to notify a custom logger of any invalid memory access attempts) by defining the MSE_CUSTOM_THROW_DEFINITION()
preprocessor function macro.
Achieving a more comprehensive degree of safety (while maintaining efficiency) takes a bit more effort. First, you'll want to get acquainted with scope pointers and objects. You'll want to, for example, understand how to obtain a scope iterator, and in turn, a scope pointer to a vector element (as demonstrated, for example, in the xscope_borrowing_fixed_nii_vector<>
example). You'll want to understand how to obtain a scope pointer to a class/struct member from a scope pointer to the class/struct. You'll want to understand how to obtain a scope pointer to the target of a strong/owning pointer.
Once you've embraced the scope pointer lifestyle, you can use a conformance helper tool like scpptool to analyze your source files and flag parts of your code that it can't verify to be safe. Appeasing the tool largely involves simply obtaining scope pointers to the objects in question.
We start off by introducing a non-owning (smart) pointer type that is actually intended to be used infrequently. But we want to demonstrate up front that (safe) smart pointers don't necessarily have to "own" their target objects (i.e. control how and when they are allocated and deallocated like std::unique_ptr<>
and std::shared_ptr<>
do), or otherwise be tied to a restrictive, intrusive allocation/deallocation mechanism (like std::weak_ptr<>
is).
"Registered" pointers behave much like native C pointers, except that their value is (automatically) set to a null value (nullptr
) when the target object is destroyed. And by default they will throw an exception upon any attempt to dereference a pointer with a null value. Like raw pointers (and unlike the standard smart pointers), they can point to, for example, local variables or elements of a container, and are generally agnostic to when/where/how their target objects are allocated and deallocated.
Two types of registered pointers are provided - TRegisteredPointer<>
and TCRegisteredPointer<>
. They are functionally equivalent, but TRegisteredPointer<>
is optimized for better average performance, while TCRegisteredPointer<>
is a little more optimized for better "worst-case" performance. (Specifically, the operation of retargeting (or "detargeting") a TRegisteredPointer<>
in the worst case is O(n), where n is the number of other pointers targeting the same original target object. With TCRegisteredPointer<>
it's always O(1).)
Their flexibility in terms of the allocation/deallocation of their target objects makes them amenable to being used as a sort of "safe from dangling dereferences" one-for-one replacement for raw pointers. This can be useful for updating legacy code (to be safer).
Lets look at a simple example of how to use them before we get into their limitations.
usage example: (link to interactive version)
#include "mseregistered.h"
int main(int argc, char* argv[]) {
class CA {
public:
CA(int x) : m_x(x) {}
int m_x;
};
mse::TRegisteredPointer<CA> a_regptr;
CA a2_obj(2);
{
// mse::TRegisteredObj<CA> is a class publicly derived from CA
mse::TRegisteredObj<CA> a_regobj(1); // a_regobj is entirely on the stack
a_regptr = &a_regobj;
a2_obj = (*a_regptr);
}
if (a_regptr) {
assert(false);
} else {
try {
a2_obj = (*a_regptr);
}
catch (...) {
// expected exception
}
}
a_regptr = mse::registered_new<CA>(3); // heap allocation
mse::registered_delete<CA>(a_regptr);
/* You can also use the make_registered() function to obtain a registered object from a given value. */
auto a3_regobj = mse::make_registered(CA(5));
}
(Note that using mse::registered_delete()
to delete an object through a base class pointer will result in a failed assert / thrown exception. In such cases use (the not quite as safe) mse::us::registered_delete()
instead.)
Ok, but let's now consider the limitations of the safety mechanism. While registered pointers' built-in mechanisms ensure that they don't dereference to invalid memory, those mechanisms do not ensure the safety of any (named) raw pointers or references implicitly or explicitly derived from the dereference of a registered pointer. This includes any implicit this
pointer derived from the dereference of a registered pointer. So for example, invoking a non-static member function or operator on the object reference resulting from a dereference of a registered pointer would not be ensured to be safe. This would include, for example, (non-trivial) copy constructors.
This might not be an issue when converting from legacy C code, as C doesn't have any implicit this
pointers. But if the goal is complete memory safety for C++ code, it would be the job of a static analyzer/enforcer like scpptool to disallow such invocation of member functions or operators.
The strategies for working around this limitation might include the avoidance of non-static member functions in favor of static member or free functions that can accept a registered pointer to the object in question. There does seem to be somewhat of a trend of preferring free functions over member functions.
Another option that may often be more practical, is to add a level of indirection and instead of targeting the object directly, use registered pointers to target safe "strong" references from which safe raw references (including implicit this
pointers) to the object can be obtained. Examples of such safe strong references include, for example, reference counting (shared ownership) pointers, norad (trust-but-verify) pointers, and scope pointers (which include raw pointers themselves). Using registered pointers to target (statically verified) raw pointers has the benefit of being "unintrusive" in the sense of not requiring any modifications to the target object's type. In fact there is a version of registered pointer, "registered proxy pointer", provided specifically to make this use case more convenient.
And note that these registered pointers cannot be shared among asynchronous threads (enforced by the type system and scpptool). When you need to share objects between asynchronous threads, you can use the safe sharing data types in this library.
TRegisteredNotNullPointer<>
is a version of TRegisteredPointer<>
that cannot be constructed to a null value. Note that TRegisteredPointer<>
does not implicitly convert to TRegisteredNotNullPointer<>
. When needed, the conversion can be done with the mse::not_null_from_nullable()
function.
TRegisteredFixedPointer<>
is a version of TRegisteredNotNullPointer<>
that cannot be retargeted after construction (basically a "const TRegisteredNotNullPointer<>
"). It is essentially a functional equivalent of a C++ reference.
usage example:
#include "mseregistered.h"
int main(int argc, char* argv[]) {
class CA {
public:
CA(std::string str) : m_str(str) {}
std::string m_str;
};
class CB {
public:
static void foo(mse::TRegisteredFixedConstPointer<CA> input1_fc_ptr, mse::TRegisteredFixedConstPointer<CA>
input2_fc_ptr, mse::TRegisteredFixedPointer<CA> output_f_ptr) {
output_f_ptr->m_str = "output from " + input1_fc_ptr->m_str + " and " + input2_fc_ptr->m_str;
return;
}
};
mse::TRegisteredObj<CA> in1_regobj("input1");
mse::TRegisteredPointer<CA> in2_regptr = mse::registered_new<CA>("input2");
auto out_regobj = mse::make_registered(CA("")); /* Alternative way to make a registered object. */
CB::foo(&in1_regobj, &(*in2_regptr), &out_regobj);
mse::registered_delete<CA>(in2_regptr);
}
TRegisteredPointer<X>
does implicitly convert to TRegisteredPointer<const X>
. But some prefer to think of the pointer giving "const" access to the object rather than giving access to a "const object".
Just a registered version of std::reference_wrapper<>
.
usage example:
#include "mseprimitives.h"
#include "mseregistered.h"
int main(int argc, char* argv[]) {
/* This example originally comes from http://www.cplusplus.com/reference/functional/reference_wrapper/. */
mse::TRegisteredObj<mse::CInt> a(10), b(20), c(30);
// an array of "references":
mse::TRegisteredRefWrapper<mse::CInt> refs[] = { a,b,c };
std::cout << "refs:";
for (mse::CInt& x : refs) std::cout << ' ' << x;
std::cout << '\n';
mse::TRegisteredObj<mse::CInt> foo(10);
auto bar = mse::registered_ref(foo);
++(mse::CInt&)bar;
std::cout << foo << '\n';
}
usage example: (link to interactive version)
#include "msecregistered.h"
int main(int argc, char* argv[]) {
class C;
class D {
public:
virtual ~D() {}
mse::TCRegisteredPointer<C> m_c_ptr;
};
class C {
public:
mse::TCRegisteredPointer<D> m_d_ptr;
};
mse::TCRegisteredObj<C> regobjfl_c;
mse::TCRegisteredPointer<D> d_ptr = mse::cregistered_new<D>();
regobjfl_c.m_d_ptr = d_ptr;
d_ptr->m_c_ptr = ®objfl_c;
mse::cregistered_delete<D>(d_ptr);
}
As with TRegisteredPointer<>
, if deleting a cregistered object via a pointer to its base class you'll need to use the mse::us::cregistered_delete<>()
function instead.
When pointing to a valid object, TRegisteredPointer<>
and TCRegisteredPointer<>
essentially behave like raw pointers. So when in "disabled" mode, they are just aliased to raw pointers. However, in cases when their target object becomes invalid (i.e. is destroyed), the behavior of registered pointers is not the same as raw pointers. Specifically, registered pointers are automatically set to null when their target object is destroyed. So any code that relies on this behavior might not work properly when the registered pointers are substituted with raw pointers.
So for those cases, TNDRegisteredPointer<>
and TNDCRegisteredPointer
are just versions of registered pointers that are not aliased to raw pointers in "disabled" mode. In fact, when not in "disabled" mode, TRegisteredPointer<>
and TCRegisteredPointer<>
are just aliases for TNDRegisteredPointer<>
and TNDCRegisteredPointer
.
"Norad" pointers, like registered pointers, behave similar to native pointers. But where registered pointers are automatically set to nullptr
when their target is destroyed, the destruction of an object while a "norad" pointer is still targeting it results in program termination. This drastic consequence allows norad pointers' run-time safety mechanism to be even more lightweight than registered pointers. And unlike registered pointers, you can obtain a scope pointer from any norad pointer (using the make_xscope_strong_pointer_store()
function).
usage example: (link to interactive version)
#include "msenorad.h"
int main(int argc, char* argv[]) {
class C;
class D {
public:
virtual ~D() {}
mse::TNoradPointer<C> m_c_ptr;
};
class C {
public:
mse::TNoradPointer<D> m_d_ptr;
};
mse::TNoradObj<C> noradobj_c;
mse::TNoradPointer<D> d_ptr = mse::norad_new<D>();
noradobj_c.m_d_ptr = d_ptr;
d_ptr->m_c_ptr = &noradobj_c;
/* We must make sure that there are no other references to the target of d_ptr before deleting it. Registered pointers don't
have the same requirement. */
noradobj_c.m_d_ptr = nullptr;
mse::norad_delete<D>(d_ptr);
/* You can also use the make_norad() function to obtain a norad object from a given value. */
auto noradobj_c2 = mse::make_norad(C());
}
TNoradNotNullPointer<>
is a version of TNoradPointer<>
that cannot be constructed to or assigned a null value. Because a TNoradNotNullPointer<>
cannot outlive its target, it should be always safe to assume that it points to a validly allocated object. Note that TNoradPointer<>
does not implicitly convert to TNoradNotNullPointer<>
. When needed, the conversion can be done with the mse::not_null_from_nullable()
function.
Just some simple microbenchmarks of the pointers. (Some less "micro" benchmarks of the library in general can be found here.) The source code for these benchmarks can be found in the file msetl_example.cpp. (Search for "benchmark" in the file.)
Pointer Type | Time |
---|---|
native pointer (stack) | 0.049 seconds |
mse::TCRegisteredPointer (stack) | 0.057 seconds |
mse::TRegisteredPointer (stack) | 0.058 seconds |
mse::TNoradPointer (stack) | 0.059 seconds |
native pointer (heap) | 0.384 seconds |
mse::TNoradPointer (heap) | 0.394 seconds |
mse::TRefCountingPointer (heap) | 0.402 seconds |
mse::TCRegisteredPointer (heap) | 0.414 seconds |
mse::TRegisteredPointer (heap) | 0.417 seconds |
std::shared_ptr (heap) | 0.524 seconds |
Pointer Type | Time |
---|---|
native pointer | 0.046 seconds |
mse::TRefCountingPointer | 0.090 seconds |
mse::TNoradPointer | 0.119 seconds |
mse::TRegisteredPointer | 0.145 seconds |
mse::TCRegisteredPointer | 0.160 seconds |
std::shared_ptr | 0.284 seconds |
Pointer Type | Time |
---|---|
native pointer | 0.106 seconds |
native pointer + nullptr check | 0.107 seconds |
mse::TNoradPointer | 0.160 seconds |
mse::TCRegisteredPointer | 0.162 seconds |
mse::TRefCountingPointer | 0.220 seconds |
std::weak_ptr | 1.364 seconds |
Take these results with a grain of salt. The benchmarks were run on a noisy machine, and anyway don't represent realistic usage scenarios. But they give you a rough idea of the relative performances.
You can see that the library's safe pointers are quite fast compared to, say, std::shared_ptr
/std::weak_ptr
. But in some sense, as long as it's not egregiously bad, their performance is kind of moot because performance sensitive programs would generally stick to using scope pointers in critical inner loops. And by default, scope pointers have identical performance to native pointers.
If you're going to use pointers, then to ensure they won't be used to access invalid memory you basically have two options - detect any attempt to do so and throw an exception, or, alternatively, ensure that the pointer targets a validly allocated object. Registered pointers rely on the former, and so-called "reference counting" pointers can be used to achieve the latter. The most famous reference counting pointer is std::shared_ptr<>
, which is notable for its thread-safe reference counting that can be handy when you're sharing an object among asynchronous threads, but is unnecessarily costly when you aren't. So we provide fast reference counting pointers that forego any thread safety mechanisms. In addition to being substantially faster (and smaller) than std::shared_ptr<>
, they are a bit more safety oriented in that they they don't support construction from raw pointers. (Use mse::make_refcounting<>()
instead.) "Const", "not null" and "fixed" (non-retargetable) flavors are also provided with proper conversions between them.
usage example: (link to interactive version)
#include "mserefcounting.h"
int main(int argc, char* argv[]) {
class A {
public:
A() {}
A(const A& _X) : b(_X.b) {}
virtual ~A() {
int q = 3; /* just so you can place a breakpoint if you want */
}
A& operator=(const A& _X) { b = _X.b; return (*this); }
int b = 3;
};
typedef std::vector<mse::TRefCountingFixedPointer<A>> CRCFPVector;
class B {
public:
static int foo1(mse::TRefCountingPointer<A> A_refcounting_ptr, CRCFPVector& rcfpvector_ref) {
rcfpvector_ref.clear();
int retval = A_refcounting_ptr->b;
A_refcounting_ptr = nullptr; /* Target object is destroyed here. */
return retval;
}
protected:
~B() {}
};
{
CRCFPVector rcfpvector;
{
mse::TRefCountingFixedPointer<A> A_refcountingfixed_ptr1 = mse::make_refcounting<A>();
rcfpvector.push_back(A_refcountingfixed_ptr1);
/* Just to demonstrate conversion between refcounting pointer types. */
mse::TRefCountingConstPointer<A> A_refcountingconst_ptr1 = A_refcountingfixed_ptr1;
}
B::foo1(rcfpvector.front(), rcfpvector);
}
}
Same as TRefCountingPointer<>
, but cannot be constructed to or assigned a null value. Because TRefCountingNotNullPointer<>
controls the lifetime of its target, it should be always safe to assume that it points to a validly allocated object. Note that TRefCountingPointer<>
does not implicitly convert to TRefCountingNotNullPointer<>
. When needed, the conversion can be done with the mse::not_null_from_nullable()
function.
And also note that the mse::make_refcounting<>()
function actually returns a TRefCountingNotNullPointer<>
. When you want a regular TRefCountingPointer<>
, you can use the mse::make_nullable_refcounting<>()
function instead.
Same as TRefCountingNotNullPointer<>
, but cannot be retargeted after construction (basically a "const TRefCountingNotNullPointer<>
").
TRefCountingPointer<X>
actually does implicitly convert to TRefCountingPointer<const X>
. But some prefer to think of the pointer giving "const" access to the object rather than giving access to a "const object".
TRefCountingPointer<>
does not have a specific associated weak pointer like std::shared_ptr<>
does. But registered pointers can be thought of as sort of independent, universal weak pointers. Note that we're talking about targeting objects "in" the same thread here. Sharing objects between threads is done through the library's data types for asynchronous sharing.
Generally you're going to want to obtain a "strong" pointer from the weak pointer, so rather than targeting the registered pointer directly at the object of interest, you'd target a/the strong owning pointer of the object.
#include "mserefcounting.h"
#include "mseregistered.h"
#include <iostream>
int main(int argc, char* argv[]) {
typedef mse::TRefCountingFixedPointer<std::string> str_rc_ptr_t; // owning pointer of a string
typedef mse::TNDRegisteredObj<str_rc_ptr_t> str_rc_ptr_regobj_t; // registered version of above so that you can obtain a (weak)
// registered pointer to it
/* str_rc_rc_ptr1 is a "shared" owner of an owning pointer of a string */
auto str_rc_rc_ptr1 = mse::make_nullable_refcounting<str_rc_ptr_regobj_t>(str_rc_ptr_regobj_t(mse::make_refcounting<std::string>("some text")));
/* You need to double dereference it to access the string value. */
std::cout << **str_rc_rc_ptr1 << std::endl;
/* Here we're obtaining a (weak) registered pointer to the owning pointer of the string. */
auto str_rc_reg_ptr1 = &(*str_rc_rc_ptr1);
/* Here you also need to double dereference it to access the string value. */
std::cout << **str_rc_reg_ptr1 << std::endl;
{
/* We can obtain a (strong) owning pointer of the string from the (weak) registered pointer. */
auto str_rc_ptr2 = *str_rc_reg_ptr1;
std::cout << *str_rc_ptr2 << std::endl;
}
assert(str_rc_reg_ptr1); // just asserting the str_rc_reg_ptr1 is not null here
/* Here we're releasing ownership of the string owning pointer. Since this was its only owner, the string owning
pointer (and consequently the string) will be destroyed. */
str_rc_rc_ptr1 = nullptr;
assert(!str_rc_reg_ptr1); // here we're asserting that str_rc_reg_ptr1 has been (automatically) set to null
}
This next example demonstrates using TNDCRegisteredPointer<>
as a safe "weak_ptr" to prevent cyclic references from becoming memory leaks. This isn't much different from using std::weak_ptr<>
in terms of functionality, but there can be performance and safety advantages.
#include "mserefcounting.h"
#include "mseoptional.h"
#include "mseregistered.h"
int main(int argc, char* argv[]) {
class CRCNode;
typedef mse::TRefCountingFixedPointer<CRCNode> rcnode_strongptr_t; // owning pointer of a CRCNode
typedef mse::TNDRegisteredObj<rcnode_strongptr_t> rcnode_strongptr_regobj_t; // registered version of above so that you can obtain a (weak)
// registered pointer to it
typedef mse::TNDRegisteredPointer<rcnode_strongptr_t> rcnode_strongptr_weakptr_t; // (weak) registered pointer to owning pointer of a CRCNode
class CRCNode {
public:
CRCNode(mse::TRegisteredPointer<mse::CInt> node_count_ptr
, rcnode_strongptr_weakptr_t root_ptr_ptr) : m_node_count_ptr(node_count_ptr), m_root_ptr_ptr(root_ptr_ptr) {
(*node_count_ptr) += 1;
}
CRCNode(mse::TRegisteredPointer<mse::CInt> node_count_ptr) : m_node_count_ptr(node_count_ptr) {
(*node_count_ptr) += 1;
}
virtual ~CRCNode() {
(*m_node_count_ptr) -= 1;
}
static rcnode_strongptr_regobj_t MakeRoot(mse::TRegisteredPointer<mse::CInt> node_count_ptr) {
auto retval = rcnode_strongptr_regobj_t{ mse::make_refcounting<CRCNode>(node_count_ptr) };
(*retval).m_root_ptr_ptr = &retval;
return retval;
}
static auto MaybeStrongChildPtr(const rcnode_strongptr_regobj_t this_ptr) { return this_ptr->m_maybe_child_ptr; }
static rcnode_strongptr_regobj_t MakeChild(const rcnode_strongptr_regobj_t this_ptr) {
this_ptr->m_maybe_child_ptr.emplace(rcnode_strongptr_regobj_t{ mse::make_refcounting<CRCNode>(this_ptr->m_node_count_ptr, this_ptr->m_root_ptr_ptr) });
return this_ptr->m_maybe_child_ptr.value();
}
static void DisposeOfChild(const rcnode_strongptr_regobj_t this_ptr) {
this_ptr->m_maybe_child_ptr.reset();
}
private:
mse::TRegisteredPointer<mse::CInt> m_node_count_ptr;
rcnode_strongptr_weakptr_t m_root_ptr_ptr;
mse::mstd::optional<rcnode_strongptr_regobj_t> m_maybe_child_ptr;
};
mse::TRegisteredObj<mse::CInt> node_counter = 0;
{
auto root_owner_ptr = CRCNode::MakeRoot(&node_counter);
auto kid1 = root_owner_ptr->MakeChild(root_owner_ptr);
{
auto kid2 = kid1->MakeChild(kid1);
auto kid3 = kid2->MakeChild(kid2);
}
assert(4 == node_counter);
kid1->DisposeOfChild(kid1);
assert(2 == node_counter);
}
assert(0 == node_counter);
}
TSingleOwnerPointer<>
behaves similar to std::unique_ptr<>
. Some differences being that it does not accept raw pointer assignment or construction (use mse::make_single_owner<>()
instead), and it will throw an exception on attempted nullptr dereference.
usage example:
#include "mserefcounting.h"
int main(int argc, char* argv[]) {
class A {
public:
A() {}
virtual ~A() {
int q = 3; /* just so you can place a breakpoint if you want */
}
int b = 3;
std::string s = "some text ";
};
typedef std::vector<mse::TSingleOwnerPointer<A>> CSOPVector;
class B {
public:
static int foo1(mse::TSingleOwnerPointer<A> A_single_owner_ptr, CSOPVector& sopvector_ref) {
assert(!bool(sopvector_ref.front()));
int retval = A_single_owner_ptr->b;
A_single_owner_ptr = nullptr; /* Target object is destroyed here. */
return retval;
}
};
{
CSOPVector sopvector;
{
mse::TSingleOwnerPointer<A> A_single_owner_ptr1 = mse::make_single_owner<A>();
sopvector.push_back(std::move(A_single_owner_ptr1));
assert(!bool(A_single_owner_ptr1));
}
B::foo1(std::move(sopvector.front()), sopvector);
assert(!bool(sopvector.front()));
}
}
So far we've been introduced to some of the library's pointers that use run-time mechanisms to ensure safety. But it's also important to support pointers without run-time overhead that rely instead on compile-time enforced restrictions for safety. The strategy for these compile-time restrictions is based on the concept of execution scopes. Execution scopes (sometimes called "blocks"), are basically the sets of code within pairs of "curly braces" where you can declare local variables and invoke functions. (In contrast to declaration scopes where, for example, you can declare member fields of struct
s.) Note that execution scopes (which we'll often refer to as just "scopes"), can be nested inside other scopes, but, for example, they never partially overlap any other scope.
So "scope pointers" are pointers that live (only) to the end of the scope in which they (or their owners) are declared, and that point (only) to objects that live at least that long. Because scope pointers are only allowed to target objects that are known, at compile-time, to outlive them, they should be memory safe without need of any run-time overhead.
By default, (non-owning) scope pointers are just raw pointers and type aliases of raw pointers. As such, unlike the other library elements, they don't have any intrinsic safety features built into the type and rely entirely on a static analysis/enforcement tool like scpptool for their safety. If for some reason you are not using such a tool, for safety reasons you may want to avoid using raw pointers directly and you can define the MSE_SCOPEPOINTER_ENABLED
preprocessor symbol, which will change the (non-owning) scope pointer types from being aliases of raw pointers to actual classes that have built-in safety features, but that are more restricted than raw pointers. But even in this case, proper use of scope types would not be fully enforced in the type system. (Again, unlike other elements of the library). Full enforcement would still require the use of a static analysis tool like scpptool. Well, technically, the alternative of using run-time checking to catch unsafe misuses of scope types is (still) available by defining the MSE_SCOPEPOINTER_RUNTIME_CHECKS_ENABLED preprocessor symbol, but at a significant performance cost.
Many (but not all) of the elements presented in this section are designed to deal with the situation where a safety assuring static analysis/enforcment tool is not being used (as historically, such a tool was not available). In cases where you are using a tool like scpptool to enforce safety (which we very much recommend), many of the elements in this section will not be very useful and can be ignored. This will generally be indicated in the description/documentation of the elements.
That said, if you're using raw pointers, raw pointers are considered to be "scope pointers" and are required to conform to the restrictions of scope pointers. Scope pointers usually point to scope objects. Scope objects are objects that live to the end of the scope in which they are declared. You can designate pretty much any type to be a scope object type by wrapping it in the mse::TXScopeObj<>
(transparent) wrapper template. (If you're using scpptool, this is generally not necessary as the tool is capable of automatically recognizing which objects can be considered scope objects whether the programmer explicitly designates them as such or not.)
The rules for using scope pointers and objects are essentially as follows:
- Objects of scope type (which includes types whose name starts with "TXScope" or "xscope") must be local (non-static) automatic variables.
- Basically allocated on the stack.
- Note that scope pointers are themselves scope objects and must adhere to the same restrictions.
- Do not use scope types as members of classes or structs.
- Note that you can use the
mse::make_xscope_pointer_to_member_v2()
function to obtain a scope pointer to a member of a scope object. So it's generally not necessary for any class/struct member to be declared as a scope object. - In the uncommon cases that you really want to use a scope type as a member of a class or struct, that class or struct must itself be a scope type. User defined scope types must adhere to the rules of scope types.
- Note that you can use the
- Do not use scope types as function return types.
- In the uncommon cases that you really want to use a scope type as a function return type, it must be wrapped in the
mse::TXScopeReturnValue<>
transparent template wrapper. mse::TXScopeReturnValue<>
will not accept non-owning scope pointer types. Pretty much the only time you would legitimately want to return a non-owning pointer to a scope object is when that pointer is one of the function's input parameters. In those cases you can use thexscope_chosen()
function.
- In the uncommon cases that you really want to use a scope type as a function return type, it must be wrapped in the
Again, most inadvertent misuses of scope objects should result in compile errors. The remaining potential misuses would be caught by a companion tool like the aforementioned scpptool. But these rules should be intuitive enough that adherence should be fairly natural. Just remember that the safety of scope pointers is premised on the fact that scope objects are never deallocated before the end of the scope in which they are declared, and (non-owning) scope pointers (and any copies of them) never survive beyond the scope in which they are declared, so that a scope pointer cannot outlive its target scope object.
Generally, there are two types of scope pointers you might use, TXScopeOwnerPointer<>
and TXScopeFixedPointer<>
. TXScopeOwnerPointer<>
is kind of like std::unique_ptr<>
, but restricted by the rules of scope objects. It creates an instance of a given class on the heap and destroys that instance in its destructor.
TXScopeFixedPointer<>
is a (zero-overhead) "non-owning" pointer to objects that are known (at compile-time) to outlive it.
TXScopeFixedPointer<>
s are (zero-overhead) "non-owning", non-retargetable pointers. They may only be declared such that they do not outlive the scope in which they are declared (i.e. basically as (or members/"possessions" of) non-static local variables), and may only point to objects known (at compile-time) to live at least that long. By default (when the MSE_SCOPEPOINTER_ENABLED
preprocessor symbol hasn't been defined), TXScopeFixedPointer<>
s are just type aliases of raw pointers. In the case where the MSE_SCOPEPOINTER_ENABLED
preprocessor symbol has been defined, TXScopeFixedPointer<>
s have the additional restriction that they cannot be reassigned (after initialization to point to a different target object).
usage example:
#include "msescope.h"
int main(int argc, char* argv[]) {
class A {
public:
A(int x) : b(x) {}
A(const A& _X) : b(_X.b) {}
virtual ~A() {}
A& operator=(const A& _X) { b = _X.b; return (*this); }
int b = 3;
};
class B {
public:
static int foo2(mse::TXScopeFixedPointer<A> A_scpifptr) { return A_scpifptr->b; }
static int foo3(mse::TXScopeFixedConstPointer<A> A_scpifcptr) { return A_scpifcptr->b; }
protected:
~B() {}
};
mse::TXScopeObj<A> a_scpobj(5);
/* note that the '&' ("ampersand") operator is overloaded to return a mse::TXScopeFixedPointer<> */
int res1 = (&a_scpobj)->b;
int res2 = B::foo2(&a_scpobj);
int res3 = B::foo3(&a_scpobj);
/* You can also use the make_xscope() function to obtain a scope object from a given value. */
auto a2_scpobj = mse::make_xscope(A(7));
}
TXScopeOwnerPointer<>
is kind of like an std::unique_ptr<>
whose use is restricted by the rules of scope objects. So, it must live to the end of the scope in which it is declared (i.e. basically be declared as a non-static local (or thread_local
variable)) and can only be used as a member of objects which are themselves scope objects. You can use it when you want to give scope lifetime to objects that are too large to be declared directly on the stack. Unlike std::unique_ptr<>
s, you may not use std::move()
with TXScopeOwnerPointer<>
s. If you're not using a conformance helper tool like scpptool to enforce this, you can disable TXScopeOwnerPointer<>
's move constructor by defining the MSE_RESTRICT_TXSCOPEOWNERPOINTER_MOVABILITY
preprocessor symbol.
Instead of its constructor taking a native pointer pointing to an already allocated object, it allocates the object itself and forwards its construction arguments to the object's constructor. You may also use mse::make_xscope_owner<>()
to create a TXScopeOwnerPointer<>
in a manner akin to std::make_unique<>()
.
usage example:
#include "msescope.h"
int main(int argc, char* argv[]) {
class A {
public:
A(int x) : b(x) {}
A(const A& _X) : b(_X.b) {}
virtual ~A() {}
A& operator=(const A& _X) { b = _X.b; return (*this); }
int b = 3;
};
class B {
public:
static int foo2(mse::TXScopeFixedPointer<A> A_scpfptr) { return A_scpfptr->b; }
static int foo3(mse::TXScopeFixedConstPointer<A> A_scpfcptr) { return A_scpfcptr->b; }
protected:
~B() {}
};
/* You can either pass the object's constructor arguments to mse::TXScopeOwnerPointer<>'s constructor, */
mse::TXScopeOwnerPointer<A> xscp_a_ownerptr(7);
/* or you can use mse::make_xscope_owner<>() in a manner akin to std::make_unique<>() */
auto xscp_a_ownerptr2 = mse::make_xscope_owner<A>(7);
int res4 = B::foo2(xscp_a_ownerptr);
int res4b = B::foo2(&(*xscp_a_ownerptr));
}
make_xscope_strong_pointer_store()
returns a scope object that holds a copy of the given strong pointer and allows you to obtain a corresponding scope pointer. Supported strong pointers include ones like reference counting pointers, norad pointers and pointers to asynchronously shared objects (and scope pointers themselves for the sake of completeness).
usage example: (link to interactive version)
#include "msescope.h"
#include "mserefcounting.h"
int main(int argc, char* argv[]) {
class A {
public:
A(int x) : b(x) {}
A(const A& _X) : b(_X.b) {}
virtual ~A() {}
A& operator=(const A& _X) { b = _X.b; return (*this); }
int b = 3;
};
class B {
public:
static int foo2(mse::TXScopeFixedPointer<A> A_scpfptr) { return A_scpfptr->b; }
static int foo3(mse::TXScopeFixedConstPointer<A> A_scpfcptr) { return A_scpfcptr->b; }
protected:
~B() {}
};
/* Using mse::make_xscope_strong_pointer_store(), you can obtain a scope pointer from a refcounting pointer. */
/* Let's make it a const refcounting pointer, just for variety. */
mse::TRefCountingFixedConstPointer<A> refc_cptr1 = mse::make_refcounting<A>(11);
auto xscp_refc_cstore = mse::make_xscope_strong_pointer_store(refc_cptr1);
auto xscp_cptr1 = xscp_refc_cstore.xscope_ptr();
int res6 = B::foo3(xscp_cptr1);
mse::TXScopeFixedConstPointer<A> xscp_cptr2 = xscp_cptr1;
A res7 = *xscp_cptr2;
}
For "strong" pointers that are not copyable (but are movable, like TSingleOwnerPointer<>
), you can use mse::make_xscope_borrowing_strong_pointer_store()
to obtain a scope pointer. Note that unlike make_xscope_strong_pointer_store(), the argument is not the strong pointer itself, but rather a (scope) pointer to the strong pointer.
usage example:
#include "msescope.h"
#include "mserefcounting.h"
int main(int argc, char* argv[]) {
class A {
public:
A(int x) : b(x) {}
A(const A& _X) : b(_X.b) {}
virtual ~A() {}
A& operator=(const A& _X) { b = _X.b; return (*this); }
int b = 3;
};
class B {
public:
static int foo2(mse::TXScopeFixedPointer<A> A_scpfptr) { return A_scpfptr->b; }
static int foo3(mse::TXScopeFixedConstPointer<A> A_scpfcptr) { return A_scpfcptr->b; }
protected:
~B() {}
};
/* For "strong" pointers that are not copyable (like TSingleOwnerPointer<>), you can use
mse::make_xscope_borrowing_strong_pointer_store() to obtain a scope pointer. */
mse::TSingleOwnerPointer<A> so_ptr1 = mse::make_single_owner<A>(17);
auto xscp_so_store = mse::make_xscope_borrowing_strong_pointer_store(&so_ptr1);
auto xscp_ptr3 = xscp_so_store.xscope_ptr();
int res8 = B::foo3(xscp_ptr3);
mse::TXScopeFixedConstPointer<A> xscp_ptr4 = xscp_ptr3;
A res9 = *xscp_ptr4;
}
Scope pointers have limitations, (currently) for example, in terms of their ability to be retargeted, and their ability to be stored in dynamic containers. When necessary, you can circumvent these sorts of limitations by creating "registered proxy" pointers corresponding to given scope pointers.
Registered proxy pointers are basically just registered pointers which target scope pointers, except that (more conveniently) they dereference to the scope pointer's target object rather than the scope pointer itself. That is, a TRegisteredProxyPointer<T>
is similar to a TRegisteredConstPointer<TXScopeFixedPointer<T> >
, except that it dereferences to the object of type T
rather than the TXScopeFixedPointer<T>
. They are also convertible back to scope pointers when needed.
To be clear, a TRegisteredProxyPointer<T>
doesn't have any functionality that a TRegisteredConstPointer<TXScopeFixedPointer<T> >
does not already have, it's just more convenient in some situations.
usage example: (link to interactive version)
#include "msescope.h"
#include "mseregisteredproxy.h"
#include "msemsestring.h"
int main(int argc, char* argv[]) {
class CB {
public:
static void foo1(mse::TXScopeFixedPointer<mse::nii_string> xscope_ptr1) {
std::cout << *xscope_ptr1;
}
};
auto xscp_nstring1 = mse::make_xscope(mse::nii_string("some text"));
CB::foo1(&xscp_nstring1);
{
auto xscp_proxy_obj1 = mse::make_xscope_registered_proxy(&xscp_nstring1);
mse::TRegisteredProxyPointer<mse::nii_string> registered_proxy_ptr1 = mse::registered_proxy_fptr(xscp_proxy_obj1);
/* Registered proxy pointers implicitly convert to scope pointers. */
CB::foo1(registered_proxy_ptr1);
auto xscp_nstring2 = mse::make_xscope(mse::nii_string("some other text"));
auto xscp_proxy_obj2 = mse::make_xscope_registered_proxy(&xscp_nstring2);
/* Registered proxy pointers are retargetable. */
registered_proxy_ptr1 = mse::registered_proxy_fptr(xscp_proxy_obj2);
CB::foo1(registered_proxy_ptr1);
{
auto xscp_nstring3 = mse::make_xscope(mse::nii_string("other text"));
{
auto xscp_proxy_obj3 = mse::make_xscope_registered_proxy(&xscp_nstring3);
{
registered_proxy_ptr1 = mse::registered_proxy_fptr(xscp_proxy_obj3);
}
CB::foo1(registered_proxy_ptr1);
}
/* Attempting to dereference registered_proxy_ptr1 here would result in an exception. */
//*registered_proxy_ptr1;
}
}
}
TRegisteredProxyPointer, TRegisteredProxyNotNullPointer, TRegisteredProxyFixedPointer, TRegisteredProxyConstPointer, TRegisteredProxyNotNullConstPointer, TRegisteredProxyFixedConstPointer
"norad proxy" pointers are to "registered proxy" pointers as norad pointers are to registered pointers. That is, the difference is that the destruction of a norad proxy object while a norad proxy pointer still references it will result in program termination. So like their registered counterparts:
Norad proxy pointers are basically just norad pointers which target scope pointers, except that (more conveniently) they dereference to the scope pointer's target object rather than the scope pointer itself. That is, a TNoradProxyPointer<T>
is similar to a TNoradPointer<TXScopeFixedPointer<T> >
, except that it dereferences to the object of type T
rather than the TXScopeFixedPointer<T>
. They are also convertible back to scope pointers when needed.
To be clear, a TNoradProxyPointer<T>
doesn't have any functionality that a TNoradPointer<TXScopeFixedPointer<T> >
does not already have, it's just more convenient in some situations.
usage example: (link to interactive version)
#include "msescope.h"
#include "msenoradproxy.h"
#include "msemsestring.h"
int main(int argc, char* argv[]) {
class CB {
public:
static void foo1(mse::TXScopeFixedPointer<mse::nii_string> xscope_ptr1) {
std::cout << *xscope_ptr1;
}
};
auto xscp_nstring1 = mse::make_xscope(mse::nii_string("some text"));
CB::foo1(&xscp_nstring1);
{
auto xscp_proxy_obj1 = mse::make_xscope_norad_proxy(&xscp_nstring1);
mse::TNoradProxyPointer<mse::nii_string> norad_proxy_ptr1 = mse::norad_proxy_fptr(xscp_proxy_obj1);
/* Norad proxy pointers implicitly convert to scope pointers. */
CB::foo1(norad_proxy_ptr1);
auto xscp_nstring2 = mse::make_xscope(mse::nii_string("some other text"));
auto xscp_proxy_obj2 = mse::make_xscope_norad_proxy(&xscp_nstring2);
/* Norad proxy pointers are retargetable. */
norad_proxy_ptr1 = mse::norad_proxy_fptr(xscp_proxy_obj2);
CB::foo1(norad_proxy_ptr1);
{
auto xscp_nstring3 = mse::make_xscope(mse::nii_string("other text"));
{
auto xscp_proxy_obj3 = mse::make_xscope_norad_proxy(&xscp_nstring3);
{
norad_proxy_ptr1 = mse::norad_proxy_fptr(xscp_proxy_obj3);
}
CB::foo1(norad_proxy_ptr1);
/* Forgetting to detarget norad proxy pointers before their target object is destroyed (in this case, by
going out of scope) would result in program termination. */
norad_proxy_ptr1 = nullptr;
}
}
}
}
TNoradProxyPointer, TNoradProxyNotNullPointer, TNoradProxyFixedPointer, TNoradProxyConstPointer, TNoradProxyNotNullConstPointer, TNoradProxyFixedConstPointer
[provisional]
Note, if you are using scpptool for safety enforcement, you can(/probably should) instead use the supported lifetime constraint annotations.
For safety reasons, non-owning scope pointers (or any objects containing a scope reference) are not permitted to be used as function return values. (The return_value()
function wrapper enforces this.) Pretty much the only time you'd legitimately want to do this is when the returned pointer is one of the input parameters. An example might be a min(a, b)
function which takes two objects by reference and returns the reference to the lesser of the two objects. For these cases you could use the xscope_chosen()
function which takes two objects of the same type (in this case it will be two scope pointers) and returns (a copy of) one of the objects (scope pointers), which one depending on the value of a given "decider" function. You could use this function to implement the equivalent of a min(a, b)
function like so:
#include "msescope.h"
int main(int argc, char* argv[]) {
class A {
public:
A(int x) : b(x) {}
A(const A& _X) : b(_X.b) {}
virtual ~A() {}
bool operator<(const A& _X) const { return (b < _X.b); }
int b = 3;
};
mse::TXScopeObj<A> a_scpobj(5);
mse::TXScopeOwnerPointer<A> xscp_a_ownerptr(7);
auto xscp_a_ptr5 = &a_scpobj;
auto xscp_a_ptr6 = &(*xscp_a_ownerptr);
const auto second_arg_is_smaller_fn = [](const auto xscp_a_ptr1, const auto xscp_a_ptr2) { return (*xscp_a_ptr2) < (*xscp_a_ptr1); };
auto xscp_min_ptr1 = mse::xscope_chosen(second_arg_is_smaller_fn, xscp_a_ptr5, xscp_a_ptr6);
/* xscp_min_ptr1 should be a copy of xscp_a_ptr5 */
assert(5 == xscp_min_ptr1->b);
}
[provisional]
Note, if you are using scpptool for safety enforcement, you can(/probably should) instead use the supported lifetime constraint annotations.
Another alternative if you want to return a scope pointer (or any object containing a scope reference) function parameter is to (immediately) create a "returnable" version of it using the rsv::as_a_returnable_fparam()
function.
Normally the return_value()
function wrapper will reject (with a compile error) scope pointers as unsafe return values. But the rsv::as_a_returnable_fparam()
function can be used to (immediately) obtain a "returnable" version of a scope pointer function parameter. Because it's generally safe to return a reference to an object if that reference was passed as a parameter. Well, as long as the object is not a temporary object. So unlike rsv::as_an_fparam()
, rsv::as_a_returnable_fparam()
will not accept scope pointers to temporaries, as returning a (scope) reference to a temporary would be unsafe even if the reference was passed as a function parameter. So for scope reference parameters you have to choose between being able to use it as a return value, or supporting references to temporaries. (Or neither.)
Note that using the rsv::as_a_returnable_fparam()
function on anything other than local function parameters would be unsafe, and wouldn't be prevented by the type system. Safety enforcement is reliant on a companion tool like scpptool.
usage example:
#include "msescope.h"
#include "msemsestring.h"
class H {
public:
/* This function will be used to demonstrate using rsv::as_a_returnable_fparam() to enable template functions to
return one of their function parameters, potentially of the scope reference variety which would otherwise be
rejected (with a compile error) as an unsafe return value. */
template<class _TString1Pointer, class _TString2Pointer>
static auto longest(const _TString1Pointer& string1_ptr, const _TString2Pointer& string2_ptr) {
auto l_string1_ptr = mse::rsv::as_a_returnable_fparam(string1_ptr);
auto l_string2_ptr = mse::rsv::as_a_returnable_fparam(string2_ptr);
if (l_string1_ptr->length() > l_string2_ptr->length()) {
/* If string1_ptr were a regular TXScopeFixedPointer<mse::nii_string> and we tried to return it
directly instead of l_string1_ptr, it would have induced a compile error. */
return mse::return_value(l_string1_ptr);
}
else {
/* mse::return_value() usually just returns its input argument unmolested, but in this case, where the
argument was obtained from the mse::rsv::as_a_returnable_fparam() it will convert it back the type of
the original function parameter (thereby removing the "returnability" attribute that was added by
mse::rsv::as_a_returnable_fparam()). */
return mse::return_value(l_string2_ptr);
}
}
/* This function will be used to demonstrate nested function calls (safely) returning scope pointer/references. */
template<class _TString1Pointer, class _TString2Pointer>
static auto nested_longest(const _TString1Pointer& string1_ptr, const _TString2Pointer& string2_ptr) {
auto l_string1_ptr = mse::rsv::as_a_returnable_fparam(string1_ptr);
auto l_string2_ptr = mse::rsv::as_a_returnable_fparam(string2_ptr);
/* Note that with functions (potentially) returning a scope reference parameter (or an object derived
from a scope reference parameter), you generally want the function to be a template function with the
scope reference parameters types being (deduced) template parameters, as with this function, rather
than more explicitly specified scope reference types or template types. The reason for this is that in
the case of nested function calls, the number of nested function calls from which a scope reference
object can be (safely) returned is embedded in the scope reference object's type. That is, the exact
type of a returnable scope reference object depends on how many (levels of) nested function calls it
has been passed through. And you generally want your functions that return scope reference objects to
preserve the exact type of the scope reference passed, otherwise you may not be allowed (i.e. induced
compile error) to return the scope reference all the way back to the scope it originated from. */
return mse::return_value(longest(l_string1_ptr, l_string2_ptr));
}
struct CE {
mse::nii_string m_string1 = "abcde";
};
/* This function demonstrates scope reference objects inheriting the "returnability" trait from the reference objects
from which they were derived. */
template<class _TPointer1>
static auto xscope_string_const_section_to_member_of_CE(const _TPointer1& CE_ptr) {
auto returnable_CE_ptr = mse::rsv::as_a_returnable_fparam(CE_ptr);
/* "Pointers to members" based on returnable pointers inherit the "returnability". */
auto returnable_cpointer_to_member = mse::make_xscope_const_pointer_to_member_v2(returnable_CE_ptr, &CE::m_string1);
/* "scope nrp string const sections" based on returnable pointers (or iterators) inherit the "returnability". */
auto returnable_string_const_section = mse::make_xscope_string_const_section(returnable_cpointer_to_member);
/* Subsections of returnable sections inherit the "returnability". */
auto returnable_string_const_section2 = mse::make_xscope_subsection(returnable_string_const_section, 1, 3);
return mse::return_value(returnable_string_const_section2);
}
template<class _TPointer1>
static auto nested_xscope_string_const_section_to_member_of_CE(const _TPointer1& CE_ptr) {
auto returnable_CE_ptr = mse::rsv::as_a_returnable_fparam(CE_ptr);
return mse::return_value(xscope_string_const_section_to_member_of_CE(returnable_CE_ptr));
}
};
int main(int argc, char* argv[]) {
mse::TXScopeObj<mse::nii_string> xscope_string1 = "abc";
mse::TXScopeObj<mse::nii_string> xscope_string2 = "abcd";
auto longer_string_xscpptr = H::longest(&xscope_string1, &xscope_string2);
auto length1 = (*longer_string_xscpptr).length();
auto longer_string_xscpptr2 = H::nested_longest(&xscope_string1, &xscope_string2);
auto length2 = (*longer_string_xscpptr2).length();
mse::TXScopeObj<H::CE> e_xscpobj;
auto xscope_string_const_section1 = H::xscope_string_const_section_to_member_of_CE(&e_xscpobj);
assert(xscope_string_const_section1 == "bcd");
auto xscope_string_const_section2 = H::nested_xscope_string_const_section_to_member_of_CE(&e_xscpobj);
assert(xscope_string_const_section2 == "bcd");
}
[provisional]
Note, if you are using scpptool for safety enforcement, you can instead just use const references in the usual way.
rsv::TFParam<>
is just a transparent template wrapper for function parameter declarations. In most cases use of this wrapper is not necessary, but in some cases it enables functionality only available to variables that are function parameters. Specifically, it allows functions to support arguments that are scope pointer/references to temporary objects. For safety reasons, by default, scope pointer/references to temporaries are actually "functionally disabled" types distinct from regular scope pointer/reference types. Because it's safe to do so in the case of function parameters, the rsv::TFParam<>
wrapper enables certain scope pointer/reference types (like TXScopeFixedConstPointer<>
, and "random access const sections" scope types) to be constructed from their "functionally disabled" counterparts.
In the case of function templates, sometimes you want the parameter types to be auto-deduced, and use of the mse::rsv::TFParam<>
wrapper can interfere with that. In those cases you can instead convert parameters to their wrapped type after-the-fact using the rsv::as_an_fparam()
function. Note that using this function (or the rsv::TFParam<>
wrapper) on anything other than function parameters would be unsafe, and wouldn't be prevented by the type system. Safety enforcement is reliant on a companion tool like scpptool.
rsv::TXScopeFParam<>
and rsv::xscope_as_an_fparam()
can be used for situations when the types are necessarily scope types.
#include "msescope.h"
#include "msemsestring.h"
#include "msepoly.h"
class H {
public:
/* This function will be used to demonstrate using rsv::as_an_fparam() to enable template functions to accept scope
pointers to temporary objects. */
template<class _TPointer1, class _TPointer2>
static bool second_is_longer(_TPointer1&& string1_xscpptr, _TPointer2&& string2_xscpptr) {
auto l_string1_xscpptr = mse::rsv::as_an_fparam(std::forward<decltype(string1_xscpptr)>(string1_xscpptr));
auto l_string2_xscpptr = mse::rsv::as_an_fparam(std::forward<decltype(string2_xscpptr)>(string2_xscpptr));
return (l_string1_xscpptr->length() > l_string2_xscpptr->length()) ? false : true;
}
};
int main(int argc, char* argv[]) {
class CD {
public:
static bool second_is_longer(const mse::rsv::TXScopeFParam<mse::TXScopeFixedConstPointer<mse::nii_string> > string1_xscpptr
, mse::rsv::TXScopeFParam<const mse::TXScopeFixedConstPointer<mse::nii_string> > string2_xscpptr) {
return (string1_xscpptr->length() > string2_xscpptr->length()) ? false : true;
}
static bool second_is_longer_any(mse::rsv::TXScopeFParam<mse::TXScopeAnyConstPointer<mse::nii_string> > string1_xscpptr
, mse::rsv::TXScopeFParam<mse::TXScopeAnyConstPointer<mse::nii_string> > string2_xscpptr) {
return (string1_xscpptr->length() > string2_xscpptr->length()) ? false : true;
}
static bool second_is_longer_poly(mse::rsv::TXScopeFParam<mse::TXScopePolyConstPointer<mse::nii_string> > string1_xscpptr
, mse::rsv::TXScopeFParam<mse::TXScopePolyConstPointer<mse::nii_string> > string2_xscpptr) {
return (string1_xscpptr->length() > string2_xscpptr->length()) ? false : true;
}
};
mse::TXScopeObj<mse::nii_string> xscope_string1 = "abc";
/* Here we're using the pointer_to() function to obtain a ("caged") pointer to the temporary scope object. The '&'
(ampersand) operator would also work, but would not correspond to valid native C++, as C++ does not support taking
the address of an r-value. */
auto res1 = CD::second_is_longer(&xscope_string1, mse::pointer_to(mse::TXScopeObj<mse::nii_string>(xscope_string1 + "de")));
auto res2 = H::second_is_longer(&xscope_string1, mse::pointer_to(mse::TXScopeObj<mse::nii_string>(xscope_string1 + "de")));
auto res3 = CD::second_is_longer_any(&xscope_string1, mse::pointer_to(mse::TXScopeObj<mse::nii_string>(xscope_string1 + "de")));
auto res4 = CD::second_is_longer_poly(&xscope_string1, mse::pointer_to(mse::TXScopeObj<mse::nii_string>(xscope_string1 + "de")));
}
The safety of non-owning scope pointers is premised on the fact that they will not outlive the scope in which they are declared. So returning a non-owning scope pointer, or any object that contains or owns a non-owning scope pointer, from a function would be potentially unsafe. However, it could be safe to return a scope object if that object does not contain or own any non-owning scope pointers.
The return_value()
function just returns its argument and verifies that it is of a type that is safe to return from a function (basically, doesn't contain any scope pointers). If not it will induce a compile error. Functions that do or could return scope types should wrap their return value with this function. Note that the scpptool enforces its use when required.
usage example:
#include "msescope.h"
#include "mseregistered.h"
#include "msemstdstring.h"
#include "mseoptional.h"
class J {
public:
template<typename _TParam>
static auto foo10(_TParam param) {
_TParam l_obj = param;
auto ret_ptr = &l_obj;
/* ret_ptr may be a smart pointer if the 'operator&' is overloaded.*/
/* If ret_ptr is a scope pointer (or native pointer) it would be unsafe to return it. In those cases the
'mse::return_value(ret_ptr)' expression will induce a compile error. */
/* Functions that could return a scope type need to wrap their return value with the return_value() function. */
return mse::return_value(ret_ptr);
}
};
int main() {
class CB {
public:
/* It's generally not necessary for a function return type to be a scope type. Even if the return value
is of a scope type, you can usually just use the underlying (non-scope) type of the scope object as the
return type. */
static mse::mstd::string foo1() {
mse::TXScopeObj<mse::mstd::string> xscp_string1("some text");
return mse::return_value(xscp_string1);
}
/* In the less common case where the scope type doesn't have an underlying non-scope type, it may be safe
to return the scope object. */
static mse::xscope_optional<mse::mstd::string> foo2() {
mse::xscope_optional<mse::mstd::string> xscp_returnable_obj1(mse::mstd::string("some text"));
return mse::return_value(xscp_returnable_obj1);
}
static auto foo3() {
mse::TXScopeObj<mse::mstd::string> xscp_string1("some text");
auto xscp_ptr = &xscp_string1;
/* Returning a scope pointer (to a local variable for example) could be unsafe. So mse::return_value()
induces a compile error if the return value contains a scope pointer/reference. */
//return mse::return_value(xscp_ptr);
return mse::return_value(xscp_string1);
}
};
mse::TXScopeObj<mse::mstd::string> xscp_res1 = CB::foo1();
mse::xscope_optional<mse::mstd::string> xscp_res2 = CB::foo2();
typedef mse::TXScopeObj<mse::mstd::string> xscope_string_t;
xscope_string_t xscp_str1 = "some text";
mse::TNDRegisteredObj<mse::mstd::string> reg_str1 = "some text";
auto reg_ptr_res1 = J::foo10(reg_str1);
//auto xscp_ptr_res1 = J::foo10(xscp_str1); // <-- would induce a compile error inside J::foo10()
}
Scope types have built in protection that prevents them from being allocated dynamically. But those protections are circumvented if a scope type is used as a member of a class or struct. So TMemberObj<>
is a transparent wrapper that can be used to wrap class/struct member types to ensure that they are not scope types. This is particularly relevant in cases when the member type is, or is derived from, a template parameter. Note though that this wrapper is no longer really necessary if you're using a conformance-enforcing tool like scpptool that will detect such potential misuses.
example:
#include "msescope.h"
#include "mseoptional.h"
int main(int argc, char* argv[]) {
/* Defining your own scope types. */
/* It is (intended to be) uncommon to need to define your own scope types. In general, if you want to use a
type as a scope type, you can just wrap it with the mse::TXScopeObj<> template. */
/* But in cases where you're going to use a scope type as a member of a class or struct, that class or struct
must itself be a scope type. Improperly defining a scope type could result in unsafe code. Use of a safety
enforcement helper tool, like scpptool, is required to ensure that your scope type is defined properly.
And note that mse::xscope_tuple<> also supports scope type elements/fields. */
/* Scope types need to publicly inherit from mse::rsv::XScopeTagBase. And by convention, be named with a
prefix indicating that it's a scope type. */
class xscope_my_type1 : public mse::rsv::XScopeTagBase {
public:
xscope_my_type1(const mse::xscope_optional<mse::mstd::string>& xscp_maybe_string)
: m_xscp_maybe_string1(xscp_maybe_string) {}
mse::xscope_optional<mse::mstd::string> m_xscp_maybe_string1;
};
/* If your type contains or owns any non-owning scope pointers, then it must also publicly inherit
from mse::rsv::ContainsNonOwningScopeReferenceTagBase. If your type contains or owns any item that can be
independently targeted by scope pointers (i.e. basically has a '&' ("address of" operator) that yeilds
a scope pointer), then it must also publicly inherit from mse::rsv::ReferenceableByScopePointerTagBase.
Failure to do so could result in unsafe code. The scpptool, for example, will check for any such failure. */
class xscope_my_type2 : public mse::rsv::XScopeTagBase, public mse::rsv::ContainsNonOwningScopeReferenceTagBase
, public mse::rsv::ReferenceableByScopePointerTagBase
{
public:
typedef mse::TXScopeFixedConstPointer<mse::mstd::string> xscope_string_ptr_t;
xscope_my_type2(const mse::xscope_optional<xscope_string_ptr_t>& xscp_maybe_string_ptr) : m_xscp_maybe_string_ptr(xscp_maybe_string_ptr) {}
/* This item (potentially) contains a non-owning scope pointer. */
mse::xscope_optional<xscope_string_ptr_t> m_xscp_maybe_string_ptr;
/* This item owns an object that can be independently targeted by scope pointers. That is,
&(*m_xscp_string_owner_ptr) yields a scope pointer. */
mse::TXScopeOwnerPointer<mse::mstd::string> m_xscp_string_owner_ptr;
};
}
If you need a safe pointer to a member of a class/struct, you could declare the member itself to be a registered object (or a reference counting pointer). But often a preferable option is to use make_pointer_to_member_v2()
. This function takes a safe pointer to the containing class/struct and a "pointer-to-member" indicating the member you want to target, and combines them to create a safe pointer to the member. The actual type of the returned pointer varies depending on the types of the parameters passed.
usage example:
/* Including "msepoly.h" is not required to use mse::make_pointer_to_member_v2(). It just happens to include all the
other include files used by this example. */
#include "msepoly.h"
class H {
public:
/* A member function that provides a safe pointer/reference to a class/struct member is going to need to
take a safe version of the "this" pointer as a parameter. */
template<class this_type>
static auto safe_pointer_to_member_string1(this_type safe_this) {
return mse::make_pointer_to_member_v2(safe_this, &H::m_string1);
}
mse::nii_string m_string1 = "initial text";
};
int main() {
/* To demonstrate, first we'll declare some objects such that we can obtain safe pointers to those
objects. For better or worse, this library provides a bunch of different safe pointers types. */
mse::TXScopeObj<H> h_scpobj;
auto h_refcptr = mse::make_refcounting<H>();
mse::TRegisteredObj<H> h_regobj;
mse::TCRegisteredObj<H> h_rlxregobj;
/* Safe iterators are a type of safe pointer too. */
mse::mstd::vector<H> h_mstdvec;
h_mstdvec.resize(1);
auto h_mstdvec_iter = h_mstdvec.begin();
mse::us::msevector<H> h_msevec;
h_msevec.resize(1);
auto h_msevec_ipointer = h_msevec.ibegin();
auto h_msevec_ssiter = h_msevec.ss_begin();
/* And don't forget the safe async sharing pointers. */
auto h_access_requester = mse::make_asyncsharedv2readwrite<ShareableH>();
auto h_writelock_ptr = h_access_requester.writelock_ptr();
auto h_stdshared_const_ptr = mse::make_stdsharedimmutable<H>();
{
/* So here's how you get a safe pointer to a member of the object using mse::make_pointer_to_member_v2(). */
auto h_string1_scpptr = mse::make_xscope_pointer_to_member_v2(&h_scpobj, &H::m_string1);
(*h_string1_scpptr) = "some new text";
auto h_string1_scp_const_ptr = mse::make_xscope_const_pointer_to_member_v2(&h_scpobj, &H::m_string1);
auto h_string1_refcptr = mse::make_pointer_to_member_v2(h_refcptr, &H::m_string1);
(*h_string1_refcptr) = "some new text";
auto h_string1_regptr = mse::make_pointer_to_member_v2(&h_regobj, &H::m_string1);
(*h_string1_regptr) = "some new text";
auto h_string1_rlxregptr = mse::make_pointer_to_member_v2(&h_rlxregobj, &H::m_string1);
(*h_string1_rlxregptr) = "some new text";
auto h_string1_mstdvec_iter = mse::make_pointer_to_member_v2(h_mstdvec_iter, &H::m_string1);
(*h_string1_mstdvec_iter) = "some new text";
auto h_string1_msevec_ipointer = mse::make_pointer_to_member_v2(h_msevec_ipointer, &H::m_string1);
(*h_string1_msevec_ipointer) = "some new text";
auto h_string1_msevec_ssiter = mse::make_pointer_to_member_v2(h_msevec_ssiter, &H::m_string1);
(*h_string1_msevec_ssiter) = "some new text";
auto h_string1_writelock_ptr = mse::make_pointer_to_member_v2(h_writelock_ptr, &H::m_string1);
(*h_string1_writelock_ptr) = "some new text";
auto h_string1_stdshared_const_ptr = mse::make_pointer_to_member_v2(h_stdshared_const_ptr, &H::m_string1);
//(*h_string1_stdshared_const_ptr) = "some new text";
}
{
/* Though the type of the safe pointer to the object member varies depending on how the object was
declared, you can make a (templated) accessor function that will return a safe pointer of the
appropriate type. */
auto h_string1_scpptr = H::safe_pointer_to_member_string1(&h_scpobj);
(*h_string1_scpptr) = "some new text";
auto h_string1_refcptr = H::safe_pointer_to_member_string1(h_refcptr);
(*h_string1_refcptr) = "some new text";
auto h_string1_regptr = H::safe_pointer_to_member_string1(&h_regobj);
(*h_string1_regptr) = "some new text";
auto h_string1_rlxregptr = H::safe_pointer_to_member_string1(&h_rlxregobj);
(*h_string1_rlxregptr) = "some new text";
auto h_string1_mstdvec_iter = H::safe_pointer_to_member_string1(h_mstdvec_iter);
(*h_string1_mstdvec_iter) = "some new text";
auto h_string1_msevec_ipointer = H::safe_pointer_to_member_string1(h_msevec_ipointer);
(*h_string1_msevec_ipointer) = "some new text";
auto h_string1_msevec_ssiter = H::safe_pointer_to_member_string1(h_msevec_ssiter);
(*h_string1_msevec_ssiter) = "some new text";
auto h_string1_writelock_ptr = H::safe_pointer_to_member_string1(h_writelock_ptr);
(*h_string1_writelock_ptr) = "some new text";
auto h_string1_stdshared_const_ptr = H::safe_pointer_to_member_string1(h_stdshared_const_ptr);
//(*h_string1_stdshared_const_ptr) = "some new text";
}
}
Poly pointers are "chameleon" (type-erased) pointers that can be constructed from, and retain the safety features of many of the pointer types in this library. If you're writing a function and you'd like it to be able to accept different types of safe pointer parameters, you can "templatize" your function. Alternatively, you can declare your pointer parameters as poly pointers.
Note that poly pointers support only basic facilities common to all the covered pointer and iterator types, providing essentially the functionality of a C++ reference. For example, this means no assignment operator, and no operator bool()
. Where null pointer values are desired you might consider using mse::mstd::optional<>
or std::optional<>
instead.
Scope poly pointers are primarily intended to be used in function parameter declarations. In particular, as they can be constructed from a scope pointer (TXScopeFixedPointer<>
or TXScopeFixedConstPointer<>
), they must observe the same usage restrictions.
usage example:
#include "msepoly.h"
int main() {
class A {
public:
A() {}
A(std::string x) : b(x) {}
virtual ~A() {}
std::string b = "some text ";
};
class D : public A {
public:
D(std::string x) : A(x) {}
};
class B {
public:
static std::string foo1(mse::TXScopePolyPointer<A> ptr) {
std::string retval = ptr->b;
return retval;
}
static std::string foo2(mse::TXScopePolyConstPointer<A> ptr) {
std::string retval = ptr->b;
return retval;
}
static std::string foo3(mse::TXScopePolyPointer<std::string> ptr) {
std::string retval = (*ptr) + (*ptr);
return retval;
}
static std::string foo4(mse::TXScopePolyConstPointer<std::string> ptr) {
std::string retval = (*ptr) + (*ptr);
return retval;
}
protected:
~B() {}
};
/* To demonstrate, first we'll declare some objects such that we can obtain safe pointers to those
objects. For better or worse, this library provides a bunch of different safe pointers types. */
mse::TXScopeObj<A> a_scpobj;
auto a_refcptr = mse::make_refcounting<A>();
mse::TRegisteredObj<A> a_regobj;
mse::TCRegisteredObj<A> a_rlxregobj;
/* Safe iterators are a type of safe pointer too. */
mse::mstd::vector<A> a_mstdvec;
a_mstdvec.resize(1);
auto a_mstdvec_iter = a_mstdvec.begin();
mse::us::msevector<A> a_msevec;
a_msevec.resize(1);
auto a_msevec_ipointer = a_msevec.ibegin();
auto a_msevec_ssiter = a_msevec.ss_begin();
/* And don't forget the safe async sharing pointers. */
auto a_access_requester = mse::make_asyncsharedreadwrite<A>();
auto a_writelock_ptr = a_access_requester.writelock_ptr();
auto a_stdshared_const_ptr = mse::make_stdsharedimmutable<A>();
/* And note that safe pointers to member elements need to be wrapped in an mse::TXScopeAnyPointer<> for
mse::TXScopePolyPointer<> to accept them. */
auto b_member_a_refc_anyptr = mse::TXScopeAnyPointer<std::string>(mse::make_pointer_to_member_v2(a_refcptr, &A::b));
auto b_member_a_reg_anyptr = mse::TXScopeAnyPointer<std::string>(mse::make_pointer_to_member_v2(&a_regobj, &A::b));
auto b_member_a_mstdvec_iter_anyptr = mse::TXScopeAnyPointer<std::string>(mse::make_pointer_to_member_v2(a_mstdvec_iter, &A::b));
{
/* All of these safe pointer types happily convert to an mse::TXScopePolyPointer<>. */
auto res_using_scpptr = B::foo1(&a_scpobj);
auto res_using_refcptr = B::foo1(a_refcptr);
auto res_using_regptr = B::foo1(&a_regobj);
auto res_using_rlxregptr = B::foo1(&a_rlxregobj);
auto res_using_mstdvec_iter = B::foo1(a_mstdvec_iter);
auto res_using_msevec_ipointer = B::foo1(a_msevec_ipointer);
auto res_using_msevec_ssiter = B::foo1(a_msevec_ssiter);
auto res_using_writelock_ptr = B::foo1(a_writelock_ptr);
auto res_using_member_refc_anyptr = B::foo3(b_member_a_refc_anyptr);
auto res_using_member_reg_anyptr = B::foo3(b_member_a_reg_anyptr);
auto res_using_member_mstdvec_iter_anyptr = B::foo3(b_member_a_mstdvec_iter_anyptr);
/* Or an mse::TXScopePolyConstPointer<>. */
auto res_using_scpptr_via_const_poly = B::foo2(&a_scpobj);
auto res_using_refcptr_via_const_poly = B::foo2(a_refcptr);
auto res_using_regptr_via_const_poly = B::foo2(&a_regobj);
auto res_using_rlxregptr_via_const_poly = B::foo2(&a_rlxregobj);
auto res_using_mstdvec_iter_via_const_poly = B::foo2(a_mstdvec_iter);
auto res_using_msevec_ipointer_via_const_poly = B::foo2(a_msevec_ipointer);
auto res_using_msevec_ssiter_via_const_poly = B::foo2(a_msevec_ssiter);
auto res_using_writelock_ptr_via_const_poly = B::foo2(a_writelock_ptr);
auto res_using_stdshared_const_ptr_via_const_poly = B::foo2(a_stdshared_const_ptr);
auto res_using_member_refc_anyptr_via_const_poly = B::foo4(b_member_a_refc_anyptr);
auto res_using_member_reg_anyptr_via_const_poly = B::foo4(b_member_a_reg_anyptr);
auto res_using_member_mstdvec_iter_anyptr_via_const_poly = B::foo4(b_member_a_mstdvec_iter_anyptr);
}
}
These poly pointers do not support construction from scope pointers, and thus are not bound by the same usage restrictions. For example, these poly pointers may be used as a member of a class or struct.
"Any" pointers are also “chameleon” (type-erased) pointers that behave similarly to poly pointers. One difference is that unlike poly pointers which can only be directly constructed from a finite set of pointer types, "any" pointers can be constructed from almost any kind of pointer. But poly pointers can be constructed from "any" pointers, so indirectly, via "any" pointers, pretty much any type of pointer converts to a poly pointer too. In particular, if you wanted to pass a pointer generated by make_pointer_to_member_v2()
to a function that takes a poly pointer, you would first need to wrap it an "any" pointer. This is demonstrated in the scope poly pointer usage example.
"Any" pointers can also be used as function arguments. The contained pointer's original value can be recovered using the maybe_any_cast<>()
template function. The choice between using poly pointers versus "any" pointers is similar to the choice between std::variant
and std::any
.
pointer_to(X)
simply returns &X
, unless the type of &X
is a native pointer (and the library's safe pointers have not been disabled). In that case a compiler error will be induced. It can be used in place of the &
operator to help avoid inadvertent use of native pointers.
A conformance-enforcment helper tool like scpptool restricts native reference parameters to (the limited) uses that it can verify are safe. You can ensure that an argument to a native reference parameter will be accepted as a safe by first obtaining a scope pointer to the argument and expressing the argument as a (direct) dereference of the scope pointer. Note that scope pointers can be obtained from reference counting pointers, norad pointers, and pointers to shared objects using make_xscope_strong_pointer_store()
, and registered pointers when they are pointing to scope objects (or scope pointers). And, if necessary, the mse::us::unsafe_make_xscope_pointer_to()
function can be used to obtain a (potentially unsafe) scope pointer to any object.
Alternatively, instead of native references you can use scope pointers directly as reference parameters.
In cases where the function takes (some degree of) "ownership" of the referenced parameter, and you want some flexibility in the (owning) pointer/reference parameter type, you probably want to make your function a function template. Another option would be to make your pointer parameters poly pointers.
While non-scope reference types can be safely returned just like value types, functions that return (or potentially return) scope pointer/reference types are a special case that must be handled differently. The challenge is to ensure that any returned scope pointer/reference doesn't outlive its original source, and therefore its target. (Btw, a "scope section" would be an example of a scope reference type.) The general way to implement this case is demonstrated in the as_a_returnable_fparam()
section. It involves a significant amount of "ceremony", but perhaps that's not necessarily inappropriate, as it's kind of an insidously dangerous case in traditional C++. (Conformance-enforcment helper tools like scpptool will catch attempts to return scope references unsafely.)
The library requires and enforces that objects shared or passed between threads may only be of types identified as safe for such operations.
When passing an argument to a function that will be executed in another thread using the library, the argument must be of a type identified as being safe to do so. If not, a compiler error will be induced. The library knows which of its own types and the standard types are and aren't safely passable to another thread, but can't automatically deduce whether or not a user-defined type is safe to pass. So in order to pass a user-defined type, you need to "declare" that it is safely passable by wrapping it with the transparent rsv::TAsyncPassableObj<>
template. Otherwise you'll get a compile error. A type that is safe to pass should contain no indirect objects (i.e. pointers, references, etc.) that are not known to be safe to pass between threads. Using rsv::TAsyncPassableObj<>
to indicate that a user-defined type is safely passable when that type does not meet these criteria could result in unsafe code, and wouldn't be prevented by the type system. Safety enforcement is reliant on a companion tool like scpptool.
mstd::thread
is just an implementation of std::thread
that verifies that the arguments passed are of a type that is designated (i.e. recognized or declared) as safe to pass between threads. Note that this includes the passed function object. "Regular, concrete" functions and non-capture lambdas are automatically recognized as safely passable, but functors, capture-lambdas and generic lambdas would need to be explicitly declared as "passable".
usage example: (used in the example for TAsyncRASectionSplitter)
mstd::async()
is just an implementation of std::async()
that verifies that the arguments and return value passed are of a type that is designated as safe to pass between threads.
usage example: (see below)
One situation where safety mechanisms are particularly important is when sharing objects between asynchronous threads. In particular, while one thread is modifying an object, you want to ensure that no other thread accesses it. But you also want to do it in a way that allows for maximum utilization of the shared object. To this end the library provides "access requesters". Access requesters provide "lock pointers" on demand that are used to safely access the shared object.
In cases where the object you want to share is "immutable" (i.e. not modifiable), no access control is necessary. For these cases the library provides "immutable fixed pointers", which can be thought of as sort of a safer version of std::shared_ptr<>
.
In order to ensure safety, shared objects can only be accessed through lock pointers or immutable fixed pointers. If you have an existing object that you only want to share part of the time, you can swap (using std::swap()
for example) the object with a shared object when it's time to share it, and swap it back when you're done sharing.
Note that not all types are safe to share between threads. For example, because of its iterators, mstd::vector<int>
is not safe to share between threads. (And neither is std::vector<int>
.) nii_vector<int>
on the other hand is. Trying to share the former using access requesters or immutable fixed pointers would result in a compile error.
As with passing objects between threads, when using the library to share an object among threads, the object must be of a type identified as being safe to do so. If not, a compiler error will be induced. The library knows which of its own types and the standard types are and aren't safely shareable, but can't automatically deduce whether or not a user-defined type is safe to share. So in order to share a user-defined type, you need to "declare" that it is safely shareable by wrapping it with the transparent rsv::TAsyncShareableObj<>
template.
A type that is safe to share should contain no indirect objects (i.e. pointers, references, etc.) that are not known to be safe to share among threads, and should not provide any facilities for obtaining such indirect objects. Note that this would disqualify, for example, container types that have a (non-static) begin()
member function that returns an iterator (which is an indirect/reference object). (The library provides containers such as nii_array<>
that are more appropriate for sharing among threads.)
Technically it would also disqualify pretty much any object with a functioning (non-static) &
operator, but the (ubiquitous) default &
operator returns a raw pointer, and for pragmatic reasons, we more precisely identify the declaration of a raw pointer object (that receives the returned raw pointer value) to be unsafe, rather than the mere presence of the (hopefully unused) &
operator that potentially generates it. Objects with custom (non-static) &
operators that return something other than a raw pointer (such as registered objects whose &
operator yields a registered pointer), however, would be disqualified.
Types declared as shareable should also not have any mutable
qualified members that are not protected by a thread-safety mechanism.
Using rsv::TAsyncShareableObj<>
to indicate that a user-defined type is safely shareable when that type does not meet these criteria could result in unsafe code, and wouldn't be prevented by the type system. Safety enforcement is reliant on a companion tool like scpptool.
Many objects that qualify as safely shareable or passable between threads qualify as both.
usage example: (see below)
Use the mse::make_asyncsharedv2readwrite<>(...)
function (in a manner analogous to std::make_shared<>(...)
) to create an object to be shared and obtain an associated "access requester", which is just an object that holds shared ownership of the shared object. Unlike std::shared_ptr<>
, you cannot dereference an access requester directly. Instead you use the writelock_ptr()
and readlock_ptr()
member functions to obtain pointers to the shared object. Those functions will block until they can obtain the needed lock on the shared object. The obtained pointers will hold on to their lock for as long as they exist. Their locks are released when the pointers are destroyed. (Generally when they go out of scope).
Acccess requesters can be copied and passed-by-value as a parameter (to another thread, generally).
Non-blocking try_writelock_ptr()
and try_readlock_ptr()
member functions are also available. As are the limited-blocking try_writelock_ptr_for()
, try_readlock_ptr_for()
, try_writelock_ptr_until()
and try_readlock_ptr_until()
.
Note that while a "write-lock" pointer will not simultaneously co-exist with any lock pointer to the same shared object in any other thread, it can co-exist with (read- and/or write-) lock pointers in the same thread. This means that lock pointers have "upgrade lock" functionality. That is, for example, a thread that holds a read lock on a shared object (via read-lock pointer) can, at some later point, additionally obtain a write lock (via write-lock pointer) without surrendering the original read lock. It can then release the write lock (by allowing the write-lock pointer to go out of scope), again without surrendering the original read lock. Systems based on traditional "readers-writer" locks would require you to surrender the read lock before attempting to obtain a write lock, allowing another thread to potentially (and undesirably) obtain a write lock in between.
One caveat is that this introduces a new possible deadlock scenario where two threads hold read locks and both are blocked indefinitely waiting for write locks. The access requesters detect these situations, and will throw an exception (or whatever user-specified behavior) when they occur.
Just as std::weak_ptr<>
is the "weak" counterpart of std::shared_ptr<>
, TAsyncSharedV2WeakReadWriteAccessRequester<>
is the weak counter part of TAsyncSharedV2ReadWriteAccessRequester<>
. Its constructor takes a regular (strong) access requester (TAsyncSharedV2ReadWriteAccessRequester<>
), and its try_strong_access_requester()
member function returns an optional
value containing the associated strong access requester, if available.
usage example: (see below)
You can use the mse::make_asyncsharedv2readonly<>()
function to create a shared object and obtain a "read only" access requester that has a readlock_ptr()
member function, but not a writelock_ptr()
member function. "Read only" access requesters can also be copy constructed from a regular (read-write) access requester (TAsyncSharedV2ReadWriteAccessRequester<>
).
usage example: (see below)
In cases where the object you want to share is "immutable" (i.e. not modifiable), no access control is necessary. For these cases you can use TAsyncSharedV2ImmutableFixedPointer<>
, which can be thought of as sort of a safer version of std::shared_ptr<>
. Use the mse::make_asyncsharedv2immutable<>()
function to obtain a TAsyncSharedV2ImmutableFixedPointer<>
.
usage example: (see below)
Atomic objects also don't require access control. Use the make_asyncsharedv2atomic<>()
function to obtain a TAsyncSharedV2AtomicFixedPointer<>
.
usage example: (see below)
async aggregate usage example: (link to interactive version)
#include "mseasyncshared.h"
#include "msemstdvector.h"
#include "msemsestring.h"
#include <ctime>
#include <ratio>
#include <chrono>
#include <future>
#include <list>
class J {
public:
template<class _TAsyncSharedReadWriteAccessRequester>
static double foo7(_TAsyncSharedReadWriteAccessRequester A_ashar) {
auto t1 = std::chrono::high_resolution_clock::now();
/* A_ashar.readlock_ptr() will block until it can obtain a read lock. */
auto ptr1 = A_ashar.readlock_ptr(); // while ptr1 exists it holds a (read) lock on the shared object
auto t2 = std::chrono::high_resolution_clock::now();
std::this_thread::sleep_for(std::chrono::seconds(1));
auto time_span = std::chrono::duration_cast<std::chrono::duration<double>>(t2 - t1);
auto timespan_in_seconds = time_span.count();
auto thread_id = std::this_thread::get_id();
return timespan_in_seconds;
}
protected:
~J() {}
};
int main(int argc, char* argv[]) {
/* The TAsyncShared data types are used to safely share objects between asynchronous threads. */
class A {
public:
A(int x) : b(x) {}
virtual ~A() {}
int b = 3;
mse::nii_string s = "some text ";
};
/* User-defined classes need to be declared as (safely) shareable in order to be accepted by the access requesters. */
typedef mse::rsv::TAsyncShareableAndPassableObj<A> ShareableA;
class B {
public:
static double foo1(mse::TAsyncSharedV2ReadWriteAccessRequester<ShareableA> A_ashar) {
auto t1 = std::chrono::high_resolution_clock::now();
/* mse::TAsyncSharedV2ReadWriteAccessRequester<ShareableA>::writelock_ptr() will block until it can obtain a write lock. */
auto ptr1 = A_ashar.writelock_ptr(); // while ptr1 exists it holds a (write) lock on the shared object
auto t2 = std::chrono::high_resolution_clock::now();
std::this_thread::sleep_for(std::chrono::seconds(1));
auto time_span = std::chrono::duration_cast<std::chrono::duration<double>>(t2 - t1);
auto timespan_in_seconds = time_span.count();
auto thread_id = std::this_thread::get_id();
//std::cout << "thread_id: " << thread_id << ", time to acquire write pointer: " << timespan_in_seconds << " seconds.";
//std::cout << std::endl;
ptr1->s = std::to_string(timespan_in_seconds);
return timespan_in_seconds;
}
static int foo2(mse::TAsyncSharedV2ImmutableFixedPointer<ShareableA> A_immptr) {
return A_immptr->b;
}
static int foo3(mse::TAsyncSharedV2AtomicFixedPointer<int> int_atomic_ptr) {
(*int_atomic_ptr) += 1;
std::this_thread::sleep_for(std::chrono::milliseconds(10));
return *int_atomic_ptr;
}
protected:
~B() {}
};
std::cout << std::endl;
std::cout << "AsyncSharedV2 test output:";
std::cout << std::endl;
{
/* This block contains a simple example demonstrating the use of mse::TAsyncSharedV2ReadWriteAccessRequester
to safely share an object between threads. */
std::cout << "TAsyncSharedV2ReadWriteAccessRequester:";
std::cout << std::endl;
auto ash_access_requester = mse::make_asyncsharedv2readwrite<ShareableA>(7);
ash_access_requester.writelock_ptr()->b = 11;
int res1 = ash_access_requester.readlock_ptr()->b;
{
auto ptr3 = ash_access_requester.readlock_ptr();
auto ptr1 = ash_access_requester.writelock_ptr();
auto ptr2 = ash_access_requester.writelock_ptr();
}
std::list<mse::mstd::future<double>> futures;
for (size_t i = 0; i < 3; i += 1) {
futures.emplace_back(mse::mstd::async(B::foo1, ash_access_requester));
}
int count = 1;
for (auto it = futures.begin(); futures.end() != it; it++, count++) {
std::cout << "thread: " << count << ", time to acquire read pointer: " << (*it).get() << " seconds.";
std::cout << std::endl;
}
std::cout << std::endl;
}
{
std::cout << "TAsyncSharedV2ReadOnlyAccessRequester:";
std::cout << std::endl;
auto ash_access_requester = mse::make_asyncsharedv2readonly<ShareableA>(7);
int res1 = ash_access_requester.readlock_ptr()->b;
std::list<mse::mstd::future<double>> futures;
for (size_t i = 0; i < 3; i += 1) {
futures.emplace_back(mse::mstd::async(J::foo7<mse::TAsyncSharedV2ReadOnlyAccessRequester<ShareableA>>, ash_access_requester));
}
int count = 1;
for (auto it = futures.begin(); futures.end() != it; it++, count++) {
std::cout << "thread: " << count << ", time to acquire read pointer: " << (*it).get() << " seconds.";
std::cout << std::endl;
}
std::cout << std::endl;
}
{
/* Just demonstrating the existence of the "try" versions. */
auto access_requester = mse::make_asyncsharedv2readwrite<mse::nii_string>("some text");
auto writelock_ptr1 = access_requester.try_writelock_ptr();
if (writelock_ptr1) {
// lock request succeeded
int q = 5;
}
auto readlock_ptr2 = access_requester.try_readlock_ptr_for(std::chrono::seconds(1));
auto writelock_ptr3 = access_requester.try_writelock_ptr_until(std::chrono::steady_clock::now() + std::chrono::seconds(1));
}
{
/* TAsyncSharedV2WeakReadWriteAccessRequester<> is the weak counterpart to TAsyncSharedV2ReadWriteAccessRequester<>
analogous to how std::weak_ptr<> is the weak counterpart to std::shared_ptr<>. */
typedef decltype(mse::make_asyncsharedv2readwrite<mse::nii_string>("abc")) access_requester_t;
auto vec1 = mse::mstd::vector<access_requester_t>();
vec1.push_back(mse::make_asyncsharedv2readwrite<mse::nii_string>("abc"));
mse::TAsyncSharedV2WeakReadWriteAccessRequester<mse::nii_string> weak_ar1(vec1.at(0));
/* Here we're obtaining a (strong) access requester from the weak access requester, then appending it the
vector of access requesters. */
vec1.push_back(weak_ar1.try_strong_access_requester().value());
assert((*(vec1.at(1).readlock_ptr())) == "abc");
vec1.clear();
/* All the (strong) access requesters have just been destroyed so attempting to obtain a (strong) access requester
from our weak one will result in an empty optional being returned. */
assert(!(weak_ar1.try_strong_access_requester().has_value()));
}
{
/* For scenarios where the shared object is immutable (i.e. is never modified), you can get away without using locks
or access requesters. */
auto A_immptr = mse::make_asyncsharedv2immutable<ShareableA>(5);
int res1 = A_immptr->b;
std::shared_ptr<const ShareableA> A_shptr(A_immptr);
std::list<mse::mstd::future<int>> futures;
for (size_t i = 0; i < 3; i += 1) {
futures.emplace_back(mse::mstd::async(B::foo2, A_immptr));
}
int count = 1;
for (auto it = futures.begin(); futures.end() != it; it++, count++) {
int res2 = (*it).get();
}
auto A_b_safe_cptr = mse::make_const_pointer_to_member_v2(A_immptr, &ShareableA::b);
}
{
/* For scenarios where the shared object is atomic, you can get away without using locks
or access requesters. */
auto int_atomic_ptr = mse::make_asyncsharedv2atomic<int>(5);
int res1 = (*int_atomic_ptr);
std::list<mse::mstd::future<int>> futures;
for (size_t i = 0; i < 3; i += 1) {
futures.emplace_back(mse::mstd::async(B::foo3, int_atomic_ptr));
}
int count = 1;
for (auto it = futures.begin(); futures.end() != it; it++, count++) {
int res2 = (*it).get();
}
}
{
/* mse::TAsyncSharedV2ReadWriteAccessRequester's flexibilty in allowing coexisting read and write lock
pointers in the same thread introduces new ways to produce logical deadlocks. This block (likely)
demonstrates the access requester's ability to detect these potential deadlocks (and throw an exception
when they would occur). */
std::cout << "TAsyncSharedV2ReadWriteAccessRequester deadlock detection:";
std::cout << std::endl;
class CC {
public:
static void foo1(mse::TAsyncSharedV2ReadWriteAccessRequester<ShareableA> A_ashar, int id) {
auto readlock_ptr = A_ashar.readlock_ptr();
std::this_thread::sleep_for(std::chrono::seconds(1));
try {
auto writelock_ptr = A_ashar.writelock_ptr();
std::this_thread::sleep_for(std::chrono::seconds(1));
}
catch (...) {
// likely exception due to potential deadlock
std::cout << "deadlock detected ";
std::cout << std::endl;
}
}
};
auto ash_access_requester = mse::make_asyncsharedv2readwrite<ShareableA>(7);
{
auto thread1 = mse::mstd::thread(CC::foo1, ash_access_requester, 1);
auto thread2 = mse::mstd::thread(CC::foo1, ash_access_requester, 2);
thread1.join();
thread2.join();
}
std::cout << std::endl;
}
}
TAsyncRASectionSplitter<>
is used for situations where you want to allow multiple threads to concurrently access and/or modify different parts of an array or vector. You specify how you want the array/vector partitioned, and the TAsyncRASectionSplitter<>
will provide a set of access requesters used to obtain access to each partition. Instead of the usual "lock pointers", these access requesters return "lock random access sections".
usage example: (link to interactive version)
#include "msemstdvector.h"
#include "msemsestring.h"
#include "msestaticimmutable.h"
#include "mseasyncshared.h"
#include <list>
class J {
public:
/* This function takes a "random access section" (which is like an "array_view" or gsl::span) as its parameter. */
template<class _TStringRASection>
static void foo8(_TStringRASection ra_section) {
size_t delay_in_milliseconds = 3000/*arbitrary*/;
if (1 <= ra_section.size()) {
delay_in_milliseconds /= ra_section.size();
}
for (size_t i = 0; i < ra_section.size(); i += 1) {
auto now1 = std::chrono::system_clock::now();
auto tt = std::chrono::system_clock::to_time_t(now1);
/* Just trying to obtain a string with the current time and date. The standard library doesn't yet
seem to provide a safe, portable way to do this. */
#ifdef _MSC_VER
static const size_t buffer_size = 64;
char buffer[buffer_size];
buffer[0] = '\0';
ctime_s(buffer, buffer_size, &tt);
#else /*_MSC_VER*/
auto buffer = ctime(&tt);
#endif /*_MSC_VER*/
std::string now_str(buffer);
ra_section[i] = now_str;
std::this_thread::sleep_for(std::chrono::milliseconds(delay_in_milliseconds));
}
}
/* This function just obtains a writelock_ra_section from the given "splitter access requester" and calls the given
function with the writelock_ra_section as the first argument. */
template<class _TAsyncSplitterRASectionReadWriteAccessRequester, class _TFunction, class... Args>
static void invoke_with_writelock_ra_section1(_TAsyncSplitterRASectionReadWriteAccessRequester ar, _TFunction function1, Args&&... args) {
function1(ar.writelock_ra_section(), args...);
}
};
int main(int argc, char* argv[]) {
/* This block demonstrates safely allowing different threads to (simultaneously) modify different
sections of a vector. (We use vectors in this example, but it works just as well with arrays.) */
static const size_t num_sections = 10;
static const size_t section_size = 5;
const size_t num_elements = num_sections * section_size;
typedef mse::nii_vector<mse::nii_string> async_shareable_vector1_t;
typedef mse::mstd::vector<mse::nii_string> nonshareable_vector1_t;
/* Let's say we have a vector. */
nonshareable_vector1_t vector1;
vector1.resize(num_elements);
{
size_t count = 0;
for (auto& item_ref : vector1) {
count += 1;
item_ref = "text" + std::to_string(count);
}
}
/* Only access controlled objects can be shared with other threads, so we'll make an access controlled vector and
(temporarily) swap it with our original one. */
auto ash_access_requester = mse::make_asyncsharedv2readwrite<async_shareable_vector1_t>();
std::swap(vector1, (*(ash_access_requester.writelock_ptr())));
{
/* Now, we're going to use the access requester to obtain two new access requesters that provide access to
(newly created) "random access section" objects which are used to access (disjoint) sections of the vector.
We need to specify the position where we want to split the vector. Here we specify that it be split at index
"num_elements / 2", right down the middle. */
mse::TAsyncRASectionSplitter<decltype(ash_access_requester)> ra_section_split1(ash_access_requester, num_elements / 2);
auto ar1 = ra_section_split1.first_ra_section_access_requester();
auto ar2 = ra_section_split1.second_ra_section_access_requester();
/* The J::foo8 template function is just an example function that operates on containers of strings. In our case the
containers will be the random access sections we just created. We'll create an instance of the function here. */
auto my_foo8_function = J::foo8<decltype(ar1.writelock_ra_section())>;
/* We want to execute the my_foo8 function in a separate thread. The function takes a "random access section"
as an argument. But as we're not allowed to pass random access sections between threads, we must pass an
access requester instead. The "J::invoke_with_writelock_ra_section1" template function is just a helper
function that will obtain a (writelock) random access section from the access requester, then call the given
function, in this case my_foo8, with that random access section. So here we'll use it to create a proxy
function that we can execute directly in a separate thread and will accept an access requester as a
parameter. */
auto my_foo8_proxy_function = J::invoke_with_writelock_ra_section1<decltype(ar1), decltype(my_foo8_function)>;
std::list<mse::mstd::thread> threads;
/* So this thread will modify the first section of the vector. */
threads.emplace_back(mse::mstd::thread(my_foo8_proxy_function, ar1, my_foo8_function));
/* While this thread modifies the other section. */
threads.emplace_back(mse::mstd::thread(my_foo8_proxy_function, ar2, my_foo8_function));
{
int count = 1;
for (auto it = threads.begin(); threads.end() != it; it++, count++) {
(*it).join();
}
}
}
{
/* Ok, now let's do it again, but instead of splitting the vector into two sections, let's split it into more sections: */
/* First we create a list of a the sizes of each section. We'll use a vector here, but any iteratable container will work. */
mse::mstd::vector<size_t> section_sizes;
for (size_t i = 0; i < num_sections; i += 1) {
section_sizes.push_back(section_size);
}
/* Just as before, TAsyncRASectionSplitter<> will generate a new access requester for each section. */
mse::TAsyncRASectionSplitter<decltype(ash_access_requester)> ra_section_split1(ash_access_requester, section_sizes);
auto ar0 = ra_section_split1.ra_section_access_requester(0);
auto my_foo8_function = J::foo8<decltype(ar0.writelock_ra_section())>;
auto my_foo8_proxy_function = J::invoke_with_writelock_ra_section1<decltype(ar0), decltype(my_foo8_function)>;
std::list<mse::mstd::thread> threads;
for (size_t i = 0; i < num_sections; i += 1) {
auto ar = ra_section_split1.ra_section_access_requester(i);
threads.emplace_back(mse::mstd::thread(my_foo8_proxy_function, ar, my_foo8_function));
}
{
int count = 1;
for (auto it = threads.begin(); threads.end() != it; it++, count++) {
(*it).join();
}
}
}
/* Now that we're done sharing the (controlled access) vector, we can swap it back to our original vector. */
std::swap(vector1, (*(ash_access_requester.writelock_ptr())));
auto first_element_value = vector1[0];
auto last_element_value = vector1.back();
}
xscope_thread
is the scope counterpart to mstd::thread
. xscope_thread
ensures that the actual associated thread doesn't outlive it (and therefore doesn't outlive the scope), blocking in its destructor if necessary. Note that objects shared with an mstd::thread
generally have dynamic allocation (i.e. are allocated on the heap), whereas objects shared with a scope thread can themselves be scope objects (i.e. allocated on the stack). Which would generally be the primary reason for using scope threads over non-scope threads.
Any data type that qualifies as "shareable" (or "passable") with non-scope threads also qualifies as shareable (or passable) with scope threads. (But not necessarily the other way around.)
But in order to share an existing scope object, that object also has to be an "access controlled" object. You make a type "access controlled" by wrapping it with the mse::TXScopeAccessControlledObj<>
template wrapper.
mse::TXScopeAccessControlledObj<>
provides xscope_pointer()
, xscope_const_pointer()
and xscope_exclusive_pointer()
member functions which you use to obtain (scope) pointers to the contained object. Note that a pointer obtained via xscope_exclusive_pointer()
may not coexist with any other pointer to the same object. Attempting to violate this rule will result in an exception.
Ok, so getting back to scope threads, it would generally not be very common that you would use xscope_thread
s directly. More often you would use them indirectly via an xscope_thread_carrier
, which is just a simple container for creating and managing a set of xscope_thread
s.
Like xscope_thread
, xscope_future
and xscope_async()
are the scope versions of their non-scope counterparts. And similarly, rather than using them directly you would more often use them via an xscope_future_carrier
, which is just a simple container for creating and managing a set of xscope_future
s and their associated xscope_async()
functions.
And finally, the function used to obtain a (scope) access requester to an access controlled scope object is make_xscope_asyncsharedv2acoreadwrite()
. Note that it takes as its argument a scope pointer to the access controlled object, not a scope pointer to the contained object. Btw, scope access requesters are an example of an object type that can be passed to other scope threads, but does not qualify (i.e. would induce a compile error) to be passed to non-scope threads.
usage example: (link to interactive version)
#include "mseasyncshared.h"
#include "msescope.h"
#include "msemsestring.h"
#include <iostream>
#include <ctime>
#include <ratio>
#include <chrono>
#include <list>
class J {
public:
template<class _TAsyncSharedReadWriteAccessRequester>
static double foo7(_TAsyncSharedReadWriteAccessRequester A_ashar) {
auto t1 = std::chrono::high_resolution_clock::now();
/* A_ashar.readlock_ptr() will block until it can obtain a read lock. */
auto ptr1 = A_ashar.readlock_ptr(); // while ptr1 exists it holds a (read) lock on the shared object
auto t2 = std::chrono::high_resolution_clock::now();
std::this_thread::sleep_for(std::chrono::seconds(1));
auto time_span = std::chrono::duration_cast<std::chrono::duration<double>>(t2 - t1);
auto timespan_in_seconds = time_span.count();
auto thread_id = std::this_thread::get_id();
//std::cout << "thread_id: " << thread_id << ", time to acquire read pointer: " << timespan_in_seconds << " seconds.";
//std::cout << std::endl;
return timespan_in_seconds;
}
};
int main(int argc, char* argv[]) {
/* Here we demonstrate safely sharing an existing stack allocated object among threads. */
class A {
public:
A(int x) : b(x) {}
virtual ~A() {}
int b = 3;
mse::nii_string s = "some text ";
};
/* User-defined classes need to be declared as (safely) shareable in order to be accepted by the access requesters. */
typedef mse::rsv::TAsyncShareableAndPassableObj<A> ShareableA;
std::cout << ": xscope_future_carrier<>";
std::cout << std::endl;
/* (Mutable) objects can be shared between threads only if they are "access controlled". You can make an
object "access controlled" by wrapping its type with the mse::TXScopeAccessControlledObj<> template wrapper. */
mse::TXScopeObj<mse::TXScopeAccessControlledObj<ShareableA> > a_xscpacobj(7);
/* Here we obtain a scope access requester for the access controlled object. */
auto xscope_access_requester = mse::make_xscope_asyncsharedv2acoreadwrite(&a_xscpacobj);
/* xscope_future_carrier<> is just a container that holds and manages scope futures. */
mse::xscope_future_carrier<double> xscope_futures;
std::list<mse::xscope_future_carrier<double>::handle_t> future_handles;
for (size_t i = 0; i < 3; i += 1) {
/* You add a future by specifying the async() function and parameters that will return the future value. */
auto handle = xscope_futures.new_future(J::foo7<decltype(xscope_access_requester)>, xscope_access_requester);
/* You need to store the handle of the added future in order to later retrieve its value. */
future_handles.emplace_back(handle);
}
int count = 1;
for (auto it = future_handles.begin(); future_handles.end() != it; it++, count++) {
std::cout << "thread: " << count << ", time to acquire read pointer: " << xscope_futures.xscope_ptr_at(*it)->get() << " seconds.";
std::cout << std::endl;
}
std::cout << std::endl;
}
The mse::make_xscope_aco_locker_for_sharing()
function takes a scope pointer to an "access controlled object" and returns a "locker" object which holds an exclusive reference to the given access controlled object. From this locker object, you can obtain either one "scope passable" (non-const) pointer, or any number of "scope passable" const pointers. These scope passable pointers can then be safely passed directly as arguments to scope threads. This is a (little) more cumbersome, more restrictive way of sharing an object than, say, using the library's "access requesters". But you might choose to do it this way in certain cases where performance is critical. When using access requesters, each thread obtains the desired lock on a thread-safe mutex. When using mse::make_xscope_aco_locker_for_sharing()
, the lock is obtained before launching the thread(s), so the mutex does not need to be thread-safe, thus saving a little overhead.
The mse::make_xscope_exclusive_strong_pointer_store_for_sharing()
function returns the same kind of "locker" object that mse::make_xscope_aco_locker_for_sharing()
does, but instead of taking a scope pointer to an "access controlled object", it accepts any recognized "exclusive" pointer. That is, a pointer that, while it exists, holds exclusive access to its target object.
usage example: (link to interactive version)
#include "mseasyncshared.h"
#include "msescope.h"
#include "msemsestring.h"
#include <iostream>
#include <ratio>
#include <chrono>
class J {
public:
template<class _TAPointer>
static void foo17b(_TAPointer a_ptr) {
static int s_count = 0;
s_count += 1;
a_ptr->s = std::to_string(s_count);
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
template<class _TConstPointer, class _TPointer>
static void foo18(_TConstPointer src_ptr, _TPointer dst_ptr) {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
dst_ptr->s = src_ptr->s;
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
};
int main(int argc, char* argv[]) {
class A {
public:
A(int x) : b(x) {}
virtual ~A() {}
int b = 3;
mse::nii_string s = "some text ";
};
typedef mse::rsv::TAsyncShareableAndPassableObj<A> ShareableA;
mse::TXScopeObj<mse::TXScopeAccessControlledObj<ShareableA> > a_xscpacobj1(3);
mse::TXScopeObj<mse::TXScopeAccessControlledObj<ShareableA> > a_xscpacobj2(5);
mse::TXScopeObj<mse::TXScopeAccessControlledObj<ShareableA> > a_xscpacobj3(7);
{
auto xscope_aco_locker1 = mse::make_xscope_aco_locker_for_sharing(&a_xscpacobj1);
typedef decltype(xscope_aco_locker1.xscope_passable_pointer()) passable_exclusive_pointer_t;
mse::xscope_thread xscp_thread1(J::foo17b<passable_exclusive_pointer_t>, xscope_aco_locker1.xscope_passable_pointer());
}
{
auto xscope_aco_locker1 = mse::make_xscope_aco_locker_for_sharing(&a_xscpacobj1);
auto xscope_aco_locker2 = mse::make_xscope_aco_locker_for_sharing(&a_xscpacobj2);
auto xscope_aco_locker3 = mse::make_xscope_aco_locker_for_sharing(&a_xscpacobj3);
typedef decltype(xscope_aco_locker1.xscope_passable_const_pointer()) passable_const_pointer_t;
typedef decltype(xscope_aco_locker2.xscope_passable_pointer()) passable_exclusive_pointer_t;
mse::xscope_thread xscp_thread1(J::foo18<passable_const_pointer_t, passable_exclusive_pointer_t>
, xscope_aco_locker1.xscope_passable_const_pointer()
, xscope_aco_locker2.xscope_passable_pointer());
mse::xscope_thread xscp_thread2(J::foo18<passable_const_pointer_t, passable_exclusive_pointer_t>
, xscope_aco_locker1.xscope_passable_const_pointer()
, xscope_aco_locker3.xscope_passable_pointer());
}
{
auto xscope_aco_locker1 = mse::make_xscope_aco_locker_for_sharing(&a_xscpacobj1);
/* Here we're using a (non-const) "xscope_passable_pointer" as the argument. The "const" version
wouldn't be accepted because an "xscope_passable_const_pointer" is not an exclusive pointer. That is,
it doesn't hold exclusive access to its target object. We could, for example, have instead used an
exclusive pointer obtained directly from the "access controlled" object, a_xscpacobj1. */
auto xscope_xstrong_ptr_store1 = mse::make_xscope_exclusive_strong_pointer_store_for_sharing(xscope_aco_locker1.xscope_passable_pointer());
auto xscope_aco_locker2 = mse::make_xscope_aco_locker_for_sharing(&a_xscpacobj2);
auto xscope_aco_locker3 = mse::make_xscope_aco_locker_for_sharing(&a_xscpacobj3);
typedef decltype(xscope_aco_locker1.xscope_passable_const_pointer()) passable_const_pointer_t;
typedef decltype(xscope_aco_locker2.xscope_passable_pointer()) passable_exclusive_pointer_t;
mse::xscope_thread xscp_thread1(J::foo18<passable_const_pointer_t, passable_exclusive_pointer_t>
, xscope_xstrong_ptr_store1.xscope_passable_const_pointer()
, xscope_aco_locker2.xscope_passable_pointer());
mse::xscope_thread xscp_thread2(J::foo18<passable_const_pointer_t, passable_exclusive_pointer_t>
, xscope_xstrong_ptr_store1.xscope_passable_const_pointer()
, xscope_aco_locker3.xscope_passable_pointer());
}
}
You can use make_xscope_exclusive_strong_pointer_store_for_sharing()
to obtain, from an exclusive pointer of, for example, an access controlled object, pointers that can be passed to other threads. Occassionally, you may want to do the reverse. That is, obtain access controlled object pointers from an exclusive pointer that was passed to a thread. You can do this by declaring the parameter that receives the passed pointer as a TXScopeExclusiveStrongPointerStoreForAccessControl<passable_exclusive_pointer_t>
, replacing passable_exclusive_pointer_t
with the type of the passed pointer. From this parameter object you can obtain pointers in the same manner as with regular access controlled objects.
usage example: (link to interactive version)
#include "mseasyncshared.h"
#include "msescope.h"
#include "msemsestring.h"
#include <iostream>
int main(int argc, char* argv[]) {
class A {
public:
A(int x) : b(x) {}
virtual ~A() {}
int b = 3;
mse::nii_string s = "some text ";
};
typedef mse::rsv::TAsyncShareableAndPassableObj<A> ShareableA;
mse::TXScopeObj<mse::TXScopeAccessControlledObj<ShareableA> > a_xscpacobj1(3);
{
/* In this block we demonstrate obtaining various types of (const and non-const) pointers you might need from
an exclusive pointer that might be passed to a thread. */
a_xscpacobj1.pointer()->s = "";
auto xscope_aco_locker1 = mse::make_xscope_aco_locker_for_sharing(&a_xscpacobj1);
typedef decltype(xscope_aco_locker1.xscope_passable_pointer()) passable_exclusive_pointer_t;
typedef decltype(xscope_aco_locker1.xscope_passable_const_pointer()) passable_const_pointer_t;
class CD {
public:
static void foo1(mse::TXScopeExclusiveStrongPointerStoreForAccessControl<passable_exclusive_pointer_t> xscope_store, int count) {
{
auto xsptr = xscope_store.xscope_pointer();
xsptr->s.append(std::to_string(count));
}
{
/* Here, from the exclusive (non-const) pointer passed to this function, we're going to obtain a couple
of const pointers that we can pass to different (scope) threads. */
auto xscope_xstrong_ptr_store1 = mse::make_xscope_exclusive_strong_pointer_store_for_sharing(xscope_store.xscope_exclusive_pointer());
mse::xscope_thread xscp_thread1(CD::foo2, xscope_xstrong_ptr_store1.xscope_passable_const_pointer());
mse::xscope_thread xscp_thread2(CD::foo2, xscope_xstrong_ptr_store1.xscope_passable_const_pointer());
}
if (1 <= count) {
/* And here we're going to (re)obtain an exclusive strong pointer like the one that was passed to this
function, then we're going to use it to recursively call this function again in another (scope) thread. */
auto xscope_xstrong_ptr_store1 = mse::make_xscope_exclusive_strong_pointer_store_for_sharing(xscope_store.xscope_exclusive_pointer());
mse::xscope_thread xscp_thread1(CD::foo1, xscope_xstrong_ptr_store1.xscope_passable_pointer(), count - 1);
}
}
static void foo2(passable_const_pointer_t xscope_A_cptr) {
std::cout << xscope_A_cptr->s << std::endl;
}
};
mse::xscope_thread xscp_thread1(CD::foo1, xscope_aco_locker1.xscope_passable_pointer(), 3);
}
}
"Exclusive writer objects" are a specialization of access controlled objects for which all non-const pointers are exclusive. That is, when a non-const pointer of an exclusive writer object exists, no other pointer of that object may exist.
A bit of extra functionality that exclusive writer objects have over access controlled objects is that, from a const pointer of an exclusive writer object, you can obtain a const pointer that can be passed to other threads (using the make_xscope_exclusive_write_obj_const_pointer_store_for_sharing()
function).
TXScopeExclusiveStrongPointerStoreForExclusiveWriterAccess<>
is the corresponding specialization of TXScopeExclusiveStrongPointerStoreForAccessControl<>
.
usage example: (link to interactive version)
#include "mseasyncshared.h"
#include "msescope.h"
#include "msemsestring.h"
#include <iostream>
#include <ratio>
#include <chrono>
class J {
public:
template<class _TAPointer>
static void foo17b(_TAPointer a_ptr) {
static int s_count = 0;
s_count += 1;
a_ptr->s = std::to_string(s_count);
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
template<class _TConstPointer, class _TPointer>
static void foo18(_TConstPointer src_ptr, _TPointer dst_ptr) {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
dst_ptr->s = src_ptr->s;
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
};
int main(int argc, char* argv[]) {
class A {
public:
A(int x) : b(x) {}
virtual ~A() {}
int b = 3;
mse::nii_string s = "some text ";
};
typedef mse::rsv::TAsyncShareableAndPassableObj<A> ShareableA;
mse::TXScopeObj<mse::TExclusiveWriterObj<ShareableA> > a_xscpxwobj1(3);
mse::TXScopeObj<mse::TExclusiveWriterObj<ShareableA> > a_xscpxwobj2(5);
mse::TXScopeObj<mse::TExclusiveWriterObj<ShareableA> > a_xscpxwobj3(7);
{
/* A (non-const) pointer of an "exclusive writer object" qualifies as an "exclusive strong" pointer, and
thus you can obtain an xscope shareable pointer from it in the standard way. */
auto xscope_xwo_pointer_store1 = mse::make_xscope_exclusive_strong_pointer_store_for_sharing(a_xscpxwobj1.pointer());
typedef decltype(xscope_xwo_pointer_store1.xscope_passable_pointer()) passable_exclusive_pointer_t;
mse::xscope_thread xscp_thread1(J::foo17b<passable_exclusive_pointer_t>, xscope_xwo_pointer_store1.xscope_passable_pointer());
}
{
/* But uniquely, you can obtain an xscope shareable const pointer from a (non-exclusive) const pointer of an
"exclusive writer object". There is a special function for this purpose: */
auto xscope_xwo_const_pointer_store1 = mse::make_xscope_exclusive_write_obj_const_pointer_store_for_sharing(a_xscpxwobj1.const_pointer());
auto xscope_xwo_pointer_store2 = mse::make_xscope_exclusive_strong_pointer_store_for_sharing(a_xscpxwobj2.pointer());
auto xscope_xwo_pointer_store3 = mse::make_xscope_exclusive_strong_pointer_store_for_sharing(a_xscpxwobj3.pointer());
typedef decltype(xscope_xwo_const_pointer_store1.xscope_passable_const_pointer()) passable_const_pointer_t;
typedef decltype(xscope_xwo_pointer_store2.xscope_passable_pointer()) passable_exclusive_pointer_t;
mse::xscope_thread xscp_thread1(J::foo18<passable_const_pointer_t, passable_exclusive_pointer_t>
, xscope_xwo_const_pointer_store1.xscope_passable_const_pointer()
, xscope_xwo_pointer_store2.xscope_passable_pointer());
mse::xscope_thread xscp_thread2(J::foo18<passable_const_pointer_t, passable_exclusive_pointer_t>
, xscope_xwo_const_pointer_store1.xscope_passable_const_pointer()
, xscope_xwo_pointer_store3.xscope_passable_pointer());
}
}
Atomic objects don't require access control.
usage example: (link to interactive version)
#include "msescopeatomic.h"
#include "mseasyncshared.h"
#include <ratio>
#include <chrono>
int main(int argc, char* argv[]) {
/* trivially copyable class */
class D {
public:
D(int x) : b(x) {}
int b = 3;
};
/* User-defined classes need to be declared as (safely) shareable in order to be used with the atomic templates. */
typedef mse::rsv::TAsyncShareableAndPassableObj<D> ShareableD;
class B {
public:
static int foo4(mse::TXScopeAtomicFixedPointer<ShareableD> xs_D_atomic_ptr) {
auto d = (*xs_D_atomic_ptr).load();
d.b += 1;
(*xs_D_atomic_ptr).store(d);
std::this_thread::sleep_for(std::chrono::milliseconds(10));
return (*xs_D_atomic_ptr).load().b;
}
};
{
/* For scenarios where the shared object is atomic, you can get away without using locks
or access requesters. */
mse::TXScopeAtomicObj<ShareableD> xscope_D_atomic_obj(7);
int res1 = xscope_D_atomic_obj.load().b;
auto xscope_D_atomic_ptr = &xscope_D_atomic_obj;
mse::xscope_thread xscp_thread1(B::foo4, xscope_D_atomic_ptr);
mse::xscope_thread xscp_thread2(B::foo4, xscope_D_atomic_ptr);
int res2 = (*xscope_D_atomic_ptr).load().b;
}
}
TXScopeAsyncACORASectionSplitter<>
is the scope version of TAsyncRASectionSplitter<>
, which enables multiple threads to safely access disjoint sections of an array or vector simultaneously. Instead of passing an access requester to its constructor, it takes a scope pointer to an existing access controlled array or vector.
Where TXScopeAsyncACORASectionSplitter<>
provides an access requester for each section, TXScopeACORASectionSplitter<>
provides each section as an access controlled object. From each of these access controlled sections, as with all access controlled objects, you can obtain pointers to them (using make_xscope_aco_locker_for_sharing()
, for example) that can be safely passed to other threads.
One reason you might choose to use TXScopeACORASectionSplitter<>
over TXScopeAsyncACORASectionSplitter<>
is that it does not involve any (costly) thread safe locks. The trade-off being that it doesn't support dynamically locking and unlocking sections (to maximize the availability of each section).
usage example: (link to interactive version)
#include "msemstdvector.h"
#include "mseasyncshared.h"
#include "msepoly.h"
#include "msescope.h"
#include "msemsearray.h"
class J {
public:
/* This function takes a "random access section" (which is like an "array_view" or gsl::span) as its parameter. */
template<class _TStringRASection>
static void foo8(_TStringRASection ra_section) {
size_t delay_in_milliseconds = 3000/*arbitrary*/;
if (1 <= ra_section.size()) {
delay_in_milliseconds /= mse::as_a_size_t(ra_section.size());
}
for (size_t i = 0; i < ra_section.size(); i += 1) {
auto now1 = std::chrono::system_clock::now();
auto tt = std::chrono::system_clock::to_time_t(now1);
/* Just trying to obtain a string with the current time and date. The standard library doesn't yet
seem to provide a safe, portable way to do this. */
#ifdef _MSC_VER
static const size_t buffer_size = 64;
char buffer[buffer_size];
buffer[0] = '\0';
ctime_s(buffer, buffer_size, &tt);
#else /*_MSC_VER*/
auto buffer = ctime(&tt);
#endif /*_MSC_VER*/
std::string now_str(buffer);
ra_section[i] = now_str;
std::this_thread::sleep_for(std::chrono::milliseconds(delay_in_milliseconds));
}
}
/* This function just obtains a writelock_ra_section from the given "splitter access requester" and calls the given
function with the writelock_ra_section as the first argument. */
template<class TAsyncSplitterRASectionReadWriteAccessRequester, class TFunction, class... Args>
static void invoke_with_writelock_ra_section1(TAsyncSplitterRASectionReadWriteAccessRequester ar, TFunction function1, Args&&... args) {
function1(ar.writelock_ra_section(), args...);
}
/* This function just obtains an xscope_random_access_section from the given access controlled pointer and calls the given
function with the xscope_random_access_section as the first argument. */
template<class Ty, class TFunction, class... Args>
static void invoke_with_ra_section(mse::TXScopeExclusiveStrongPointerStoreForAccessControl<Ty> xs_ac_store, TFunction function1, Args&&... args) {
auto xscope_ra_section = mse::make_xscope_random_access_section(xs_ac_store.xscope_pointer());
function1(xscope_ra_section, args...);
}
};
int main(int argc, char* argv[]) {
/* This block demonstrates safely allowing different threads to (simultaneously)
modify different sections of an existing vector declared as a local variable. */
static const size_t num_sections = 10;
static const size_t section_size = 5;
const size_t num_elements = num_sections * section_size;
typedef mse::nii_vector<mse::nii_string> async_shareable_vector1_t;
typedef mse::mstd::vector<mse::nii_string> nonshareable_vector1_t;
/* Let's say we have a vector. */
nonshareable_vector1_t vector1;
vector1.resize(num_elements);
{
size_t count = 0;
for (auto& item_ref : vector1) {
count += 1;
item_ref = "text" + std::to_string(count);
}
}
/* Only access controlled objects can be shared with other threads, so we'll make an access controlled vector and
(temporarily) swap it with our original one. */
mse::TXScopeObj<mse::TXScopeAccessControlledObj<async_shareable_vector1_t> > xscope_acobj;
std::swap(vector1, *(xscope_acobj.xscope_pointer()));
std::cout << "mse::TXScopeACORASectionSplitter<>: " << std::endl;
{
/* From the access controlled vector we're going to obtain scope pointers to two access controlled
"random access section" objects which are used to access (disjoint) sections of the vector. Then we're
going to use "locker" objects to obtain pointers that are designated as safe to pass to other threads.
We need to specify the position where we want to split the vector. Here we specify that it be split
at index "num_elements / 2", right down the middle. */
mse::TXScopeACORASectionSplitter<async_shareable_vector1_t> xscope_ra_section_split2(&xscope_acobj, num_elements / 2);
/* Here we obtain scope pointers to the access controlled sections. */
auto first_ra_section_aco_xsptr = xscope_ra_section_split2.xscope_ptr_to_first_ra_section_aco();
auto second_ra_section_aco_xsptr = xscope_ra_section_split2.xscope_ptr_to_second_ra_section_aco();
/* Access controlled object pointers aren't themselves passable to other threads, but we can obtain
corresponding pointers that are passable via a "locker" object that takes exclusive control over
the access controlled object. */
#ifndef _MSC_VER
auto xscope_section_access_owner1 = mse::make_xscope_aco_locker_for_sharing(first_ra_section_aco_xsptr);
auto xscope_section_access_owner2 = mse::make_xscope_aco_locker_for_sharing(second_ra_section_aco_xsptr);
#else // !_MSC_VER
/* An apparent bug in msvc2017 and msvc2019preview (March 2019) prevents the other branch from compiling.
This presents an opportunity to demonstrate an alternative solution. Instead of a "locker" object that
takes a scope pointer to the access controlled object, we can use an "exclusive strong pointer store"
which takes any recognized exclusive pointer. */
auto xscope_section_access_owner1 = mse::make_xscope_exclusive_strong_pointer_store_for_sharing(
(*first_ra_section_aco_xsptr).xscope_exclusive_pointer());
auto xscope_section_access_owner2 = mse::make_xscope_exclusive_strong_pointer_store_for_sharing(
(*second_ra_section_aco_xsptr).xscope_exclusive_pointer());
#endif // !_MSC_VER
/* The J::foo8 template function is just an example function that operates on containers of strings. In this case the
containers will be the random access sections we just created. We'll create an instance of the function here. */
auto my_foo8_function = J::foo8<mse::TXScopeAnyRandomAccessSection<mse::nii_string> >;
typedef decltype(my_foo8_function) my_foo8_function_type;
mse::xscope_thread_carrier threads;
/* So this thread will modify the first section of the vector. */
threads.new_thread(J::invoke_with_ra_section<decltype(xscope_section_access_owner1.xscope_passable_pointer()), my_foo8_function_type>
, xscope_section_access_owner1.xscope_passable_pointer(), my_foo8_function);
/* While this thread modifies the other section. */
threads.new_thread(J::invoke_with_ra_section<decltype(xscope_section_access_owner2.xscope_passable_pointer()), my_foo8_function_type>
, xscope_section_access_owner2.xscope_passable_pointer(), my_foo8_function);
/* Note that in this particular scenario we didn't need to use any access requesters (or (thread safe) locks). */
}
std::cout << "mse::TXScopeAsyncACORASectionSplitter<>: " << std::endl;
{
/* Ok, now let's do it again, but instead of splitting the vector into two sections, let's split it into more sections: */
/* First we create a list of a the sizes of each section. We'll use a vector here, but any iteratable container will work. */
mse::mstd::vector<size_t> section_sizes;
for (size_t i = 0; i < num_sections; i += 1) {
section_sizes.push_back(section_size);
}
/* This time (for demonstration purposes) we'll use TXScopeAsyncACORASectionSplitter<> to generate a new
access requester for each section. */
mse::TXScopeAsyncACORASectionSplitter<async_shareable_vector1_t> xscope_ra_section_split1(&xscope_acobj, section_sizes);
auto ar0 = xscope_ra_section_split1.ra_section_access_requester(0);
auto my_foo8_function = J::foo8<decltype(ar0.writelock_ra_section())>;
typedef decltype(my_foo8_function) my_foo8_function_type;
auto my_foo8_proxy_function = J::invoke_with_writelock_ra_section1<decltype(ar0), my_foo8_function_type>;
{
mse::xscope_thread_carrier xscope_threads;
for (size_t i = 0; i < num_sections; i += 1) {
auto ar = xscope_ra_section_split1.ra_section_access_requester(i);
xscope_threads.new_thread(my_foo8_proxy_function, ar, my_foo8_function);
}
/* The scope will not end until all the scope threads have finished executing. */
}
}
/* Now that we're done sharing the (controlled access) vector, we can swap it back to our original vector. */
std::swap(vector1, *(xscope_acobj.xscope_pointer()));
auto first_element_value = vector1[0];
auto last_element_value = vector1.back();
}
[provisional]
While not encouraging the use of static
or (non-thread_local
) global variables, the library does provide some facilities for their use. Note that because static
and non-thread_local
global variables can be accessible from multiple threads, their type must be one that is recognized or declared as safely shareable.
usage example: (link to interactive version)
#include "msescope.h"
#include "msemsestring.h"
#include "msestaticimmutable.h"
#include <iostream>
MSE_RSV_DECLARE_GLOBAL_IMMUTABLE(mse::nii_string) gimm_string1 = "some text";
int main(int argc, char* argv[]) {
{
auto gimm_ptr1 = &gimm_string1;
auto xs_gimm_store1 = mse::make_xscope_strong_pointer_store(gimm_ptr1);
auto xs_ptr1 = xs_gimm_store1.xscope_ptr();
std::cout << *xs_ptr1 << std::endl;
MSE_DECLARE_STATIC_IMMUTABLE(mse::nii_string) simm_string2 = "abc";
auto simm_ptr2 = &simm_string2;
auto xs_simm_store2 = mse::make_xscope_strong_pointer_store(simm_ptr2);
auto xs_ptr2 = xs_simm_store2.xscope_ptr();
std::cout << *xs_ptr2 << std::endl;
class CA {
public:
auto foo1() const {
MSE_DECLARE_STATIC_IMMUTABLE(mse::nii_string) simm_string = "abc";
/* mse::return_value() just returns its argument and ensures that it's of a (pointer) type that's safe to return. */
return mse::return_value(&simm_string);
}
};
auto simm_ptr3 = CA().foo1();
auto xs_simm_store3 = mse::make_xscope_strong_pointer_store(simm_ptr3);
auto xs_cptr3 = xs_simm_store3.xscope_ptr();
std::cout << *xs_cptr3 << std::endl;
}
}
Note that proper use of the MSE_RSV_DECLARE_GLOBAL_IMMUTABLE()
macro is not fully enforced in the type system. Full enforcement requires the use of a tool like scpptool. In debug builds any unsafe use will be caught at run-time. (This is (now) kind of redundant if you're using an aforementioned enforcement tool.) You can enable the run-time checking in non-debug builds by defining the MSE_STATICIMMUTABLEPOINTER_RUNTIME_CHECKS_ENABLED
preprocessor symbol.
Not yet available. Atomics declared static
(or global) in traditional C++ should be fairly safe. Just make sure your object doesn't contain any indirect elements (i.e. pointers/references).
Not yet available. Access requesters declared static
(or global) would be safe.
These classes are meant to behave like, and be compatible with their native counterparts. In debug mode, they check for "use before initialization", and in release mode, they use default initialization to help ensure deterministic behavior. Upon value assignment involving a narrowing conversion, CInt
and CSize_t
will check to ensure that the value fits within the type's range. They check for division by zero and CSize_t
's -=
operator checks that the operation evaluates to a non-negative value. And unlike its native counterpart, arithmetic operations involving CSize_t
that could evaluate to a negative number are returned as a (signed) CInt
.
usage example: (link to interactive version)
#include "mseprimitives.h"
int main(int argc, char* argv[]) {
mse::CInt i = 5;
i -= 17;
mse::CSize_t szt = 5;
szt += 3;
auto i2 = szt + i;
mse::CBool b = false;
if (-4 == i2) {
b = true;
}
if (b) {
try {
szt -= 20; // out of range result - this is going to throw an exception
}
catch (...) {
// expected exception
}
}
}
Note that while CInt
and CSize_t
have no problem interacting with native signed integers, they do not implicitly play well with size_t
or native unsigned integers. We'd be generally wary of using native unsigned integer types due to the (unintuitive) implicit conversion/promotion rules between signed and unsigned native integers. But if you need to obtain a size_t
from a CSize_t
, you can do so explicitly using the mse::as_a_size_t()
function.
Btw, CInt
is actually just an alias for a specific instantiation of the TInt<>
template, which can be used to make a safe version of any given integer type. (Eg. typedef mse::TInt<signed char> my_safe_small_int;
)
[provisional]
Note that by default TInt<>
does not address overflow from arithmetic operations. However, when the MSE_RETURN_RANGE_EXTENDED_TYPE_FOR_INTEGER_ARITHMETIC
preprocessor symbol is defined, results of arithmetic operations involving TInt<>
are returned as a (larger) type that can accomodate the range of possible results (without risk of overflow), if such a type is available. Assigning such a result, for example, back to one of the original TInt<>
operands may result in a "narrowing" conversion that would be checked (at runtime). While not currently the case, one could imagine that, in the future, instantiations of TInt<>
that don't have a (larger) available type that can accomodate the results of some of their arithmetic operations simply may not support those operations.
CInt
, CSize_t
and CBool
are intended to be compatible replacements for their native counterparts, and in "disabled" mode they are just aliased to their corresponding native counterparts. There are however, some functional differences between these elements and their native counterparts. For example, they can be used as base classes where their (scalar) native counterparts cannot. So any code that relies on such additional properties might not work properly when the elements are substituted with their native counterparts.
So for those cases CNDInt
, CNDSize_t
and CNDBool
are just versions that are not aliased to their native counterparts in "disabled" mode. In fact, when not in "disabled" mode, CInt
, CSize_t
and CBool
are just aliases for CNDInt
, CNDSize_t
and CNDBool
.
Quarantined types are meant to hold values that are obtained from user input or some other untrusted source (like a media file for example). These are not yet available in the library, but are an important concept with respect to safe programming. Values obtained from untrusted sources are the main attack vector of malicious actors and should be handled with special care. For example, the so-called "stagefright" vulnerability in the Android OS is the result of a specially crafted media file causing the sum of integers to overflow.
It is intended that these types will appropriately handle "extreme" values (at some run-time cost if necessary), and ensure that their values are in an appropriate range when converted to their (high-performance) native counterparts.
Not yet available.
Integer types with more comprehensive range checking can be found here: https://github.com/robertramey/safe_numerics.
The library provides a few array types - mstd::array<>
, nii_array<>
and xscope_nii_array<>
. mstd::array<>
is simply a memory-safe drop-in replacement for std::array<>
. Due to their iterators, arrays are not, in general, safe to share among threads. nii_array<>
is designed to be safely shared between asynchronous threads. Note that these two arrays do not support using scope types as the element type, while xscope_nii_array<>
does.
And remember that you can use "random access sections" to provide access to a subsection of any vector or array.
(Note that because these safe arrays are not actually aggregate types, aggregate intialization is emulated (at compile-time). Some compilers may not be able to handle very large initializer lists well. A work-around for such cases is to initialize the safe array with an std::array<>
initialized with the large initializer list.)
mstd::array<>
is a memory-safe drop-in replacement for std::array<>
. Note that the current implementation requires "mseregistered.h
".
usage example:
#include "msemstdarray.h"
#include "msemsearray.h"
#include <array>
int main(int argc, char* argv[]) {
mse::mstd::array<int, 3> ma;
std::array<int, 3> sa;
/* These two arrays should be completely interchangeable. The difference being that ma should throw
an exception on any attempt to access invalid memory. */
/* mse::us::msearray is not quite as safe as mse::mstd::array in the following way: */
std::array<int, 3>::iterator sa1_it;
mse::us::msearray<int, 3>::ss_iterator_type msea1_it; // bounds checked iterator just like mse::mstd::array::iterator
mse::mstd::array<int, 3>::iterator ma1_it;
{
std::array<int, 3> sa1 = { 1, 2, 3 };
sa1_it = sa1.begin();
mse::us::msearray<int, 3> msea1 = { 1, 2, 3 };
msea1_it = msea1.ss_begin();
mse::mstd::array<int, 3> ma1 = { 1, 2, 3 };
ma1_it = ma1.begin();
}
// (*sa1_it) = 4; // not good - undefined behavior
// (*msea1_it) = 4; // not good - undefined behavior
try {
(*ma1_it) = 4; // not undefined behavior - will throw an exception
} catch(...) {
// expected exception
}
}
Due to their iterators, std::array<>
s (and mstd::array<>
s) are not, in general, safe to share among threads. nii_array<>
is a "stripped down" array that does not support "implicit reference" iterators, allowing it to be safely shareable between asynchronous threads. "Explicit reference" iterators are supported. That is, in order to obtain an iterator, you must explicitly provide a (safe) pointer to the nii_array<>
. So for example, instead of a begin()
member function (that takes no parameters), you can obtain an iterator using the (generic) make_begin_iterator(...)
free function that takes as an argument a (safe) pointer to the array.
nii_array<>
s are swappable with mstd::array<>
s (and std::array<>
s).
Also note that an nii_array<>
will be (automatically) marked as safely shareable only if its element type is known or declared to be safely shareable.
You might choose to use nii_array<>
s over mstd::array<>
s even in cases where the array is not being shared among threads, as it actually has less overhead.
usage example: (see the similar fixed_nii_vector<>
)
xscope_nii_array<>
is just the scope version of nii_array<>
. So unlike nii_array<>
, xscope_nii_array<>
supports elements that are scope types. But as a scope type itself, xscope_nii_array<>
is subject to the restrictions of scope objects. Additionally, in cases where the element type is or contains a scope pointer or reference object, operations that modify array elements (such as swap()
, or the assignment operator or the fill()
member function) are disabled. (Though the subscript operator and at()
member function can still yield a non-const
reference to any specified element.)
The implementation of, for example, mstd::array<>
iterators uses registered pointers to ensure that iterators are not used to access array elements after the array has been deallocated. This incurs a slight run-time cost. So just as the library provides scope pointers without run-time cost, scope iterators for arrays are also provided. Scope iterators have usage restrictions similar to scope pointers. For example, they can only target arrays declared as scope objects, and may not be used as a member of any class or struct that is not itself a scope object, and may not be used as a function return value. mstd::array<>
, nii_array<>
and xscope_nii_array<>
all support scope iterators.
And from a scope iterator, you can obtain a scope pointer to its target element.
usage example:
#include "msemstdarray.h"
int main(int argc, char* argv[]) {
/* If the array is declared as a "scope" object (which basically indicates that it is declared
on the stack), then you can use "scope" iterators. While there are limitations on when they can
be used, scope iterators would be the preferred iterator type where performance is a priority
as they don't require extra run time overhead to ensure that the array has not been deallocated. */
/* Here we're declaring an array as a scope object. */
mse::TXScopeObj<mse::mstd::array<int, 3>> array1_scpobj = mse::mstd::array<int, 3>{ 1, 2, 3 };
/* Here we're obtaining a scope iterator to the array. */
auto scp_array_iter1 = mse::mstd::make_xscope_begin_iterator(&array1_scpobj);
auto scp_array_iter2 = mse::mstd::make_xscope_end_iterator(&array1_scpobj);
std::sort(scp_array_iter1, scp_array_iter2);
auto scp_array_citer3 = mse::mstd::make_xscope_begin_const_iterator(&array1_scpobj);
scp_array_citer3 = scp_array_iter1;
scp_array_citer3 = mse::mstd::make_xscope_begin_const_iterator(&array1_scpobj);
scp_array_citer3 += 2;
auto res1 = *scp_array_citer3;
auto res2 = scp_array_citer3[0];
/* Here we demonstrate the case where the array is a member of a class/struct declared as a
scope object. */
class CContainer1 {
public:
CContainer1() : m_vector({ 1, 2, 3 }) {}
mse::mstd::array<int, 3> m_array;
};
mse::TXScopeObj<CContainer1> container1_scpobj;
auto container1_m_array_scpptr = mse::make_xscope_pointer_to_member_v2(&container1_scpobj, &CContainer1::m_array);
auto scp_iter4 = mse::mstd::make_xscope_begin_iterator(container1_m_array_scpptr);
scp_iter4++;
auto res3 = *scp_iter4;
/* You can also obtain a corresponding scope pointer from a scope iterator. */
auto scp_ptr1 = mse::xscope_pointer(scp_iter4);
auto res4 = *scp_ptr1;
auto scp_cptr1 = mse::xscope_const_pointer(scp_iter4);
auto res5 = *scp_cptr1;
}
The library provides a number of vector types. Like their array counterparts, mstd::vector<>
is a memory-safe drop-in replacement for std::vector<>
and nii_vector<>
is a vector that doesn't support "implicit reference" iterators and (so) is eligible to be safely shared among asynchronous threads.
But unlike (fixed-size) arrays, dynamic containers like (resizable) vectors present an extra memory safety challenge in that the container may be modified/resized so that an element in the container might be eliminated, or moved to a different location, while there are still references (iterators) targeting the element. This results in dynamic containers having additional overhead (for safety mechanisms) and/or usage restrictions. For this reason, directly referencing elements in these (resizable) vectors is discouraged. You may instead move the vector contents to a (more efficient) "non-resizable" vector and access the elements from that container. A fixed_nii_vector<>
is provided, but generally you'll want to use xscope_borrowing_fixed_nii_vector<>
. Upon construction it "borrows" the contents of a specified vector and returns the (possibly modified) contents upon destruction. The premise here is that the moving of a vector's contents is an inexpensive operation that might be expected to be completely optimized away in cases where the contents are returned to their original location. Like arrays, fixed-size vectors support (efficient) scope iterators.
So to restate, the idea is to avoid referencing elements in dynamic containers like resizable vectors. To access (or hold references to) the elements, move the contents (if only temporarily) to a non-dynamic container, like a fixed-size vector.
But if there are occasions when you want to perform a dynamic/resizing operation (like inserting or removing elements) while holding references to individual elements, traditional vectors aren't ideal for this as such operations can invalidate any outstanding iterators. Traditionally a list container might be considered more appropriate for these situations as its iterators tend to remain intact after insertion/removal operations. But lists are generally not as "cache friendly" as vectors, which can be an issue when performance is a priority. So the library also includes ivector<>
, a vector whose iterators behave like list iterators. That is, they don't get invalidated by insert/delete/resize vector operations unless the element they were pointing to is deleted, and after any such operation, they will continue to point to the same item, which may then be in a different position in the vector.
And remember that you can use "random access sections" to provide access to a subsection of any vector or array.
mstd::vector<>
is a memory-safe drop-in replacement for std::vector<>
.
usage example:
#include "msemstdvector.h"
#include <vector>
int main(int argc, char* argv[]) {
mse::mstd::vector<int> mv;
std::vector<int> sv;
/* These two vectors should be completely interchangeable. The difference being that mv should throw
an exception on any attempt to access invalid memory. */
std::vector<int>::iterator sv1_it;
mse::mstd::vector<int>::iterator mv1_it;
{
std::vector<int> sv1 = { 1, 2, 3 };
sv1_it = sv1.begin();
mse::mstd::vector<int> mv1 = { 1, 2, 3 };
mv1_it = mv1.begin();
}
// (*sv1_it) = 4; // not good
try {
/* At this point, mv1's data has not actually been deallocated/destructed yet because it "knows" that there
is an iterator, namely mv1_it, that is still referencing it. It will be deallocated when there are no more
iterators referencing it. */
(*mv1_it) = 4; // In debug mode this will fail an assert. In non-debug mode it'll just work (safely).
} catch(...) {
/* At present, no exception will be thrown. With future library implementations, maybe. */
}
}
Like its array counterpart, nii_array<>
, nii_vector<>
is a vector that does not support "implicit reference" iterators, allowing it to be safely shareable between asynchronous threads. You can obtain "explicit reference" iterators using the (generic) make_begin_iterator(...)
free function that takes as an argument a (safe) pointer to the vector, but directly accessing the elements of an nii_vector<>
is discouraged. Prefer instead to access (or hold references to) vector elements by using an xscope_borrowing_fixed_nii_vector<>
. (Indeed, if the elements are going to be accessed or referenced directly, consider using stnii_vector<>
instead.)
Note that the standard vector insert()
, erase()
and emplace()
member functions return "implicit reference" iterators which, again, nii_vector<>
does not support. Instead there are free function counterparts that take a (safe) pointer to the container as their first argument which is used to generate and return an "explicit reference" iterator (as demonstrated in the usage example).
nii_vector<>
s are swappable with mstd::vector<>
s (and std::vector<>
s).
Also note that an nii_vector<>
will be (automatically) marked as safely shareable only if its element type is known or declared to be safely shareable.
You might choose to use nii_vector<>
s over mstd::vector<>
s even in cases where the vector is not being shared among threads, as it actually has less overhead.
usage example: (see the example for xscope_borrowing_fixed_nii_vector<>
)
A fixed_nii_vector<>
is basically like an nii_array<>
(i.e. not resizable) whose size is determined at construction rather than at compile-time. (Except that unlike an array, a fixed_nii_vector<>
can (cheaply) "take" the contents of a dynamic/resizable vector during move construction, leaving the source vector empty.)
usage example:
#include "msemsevector.h"
#include "mseasyncshared.h"
#include "mseregistered.h"
int main(int argc, char* argv[]) {
typedef mse::fixed_nii_vector<mse::nii_string> fixed_nii_vector1_t;
mse::TRegisteredObj<fixed_nii_vector1_t> rg_vo1{ mse::nii_string{"abc"}, mse::nii_string{"def"} };
mse::TRegisteredPointer<fixed_nii_vector1_t> vo1_regptr1 = &rg_vo1;
/* fixed_nii_vector<> does not have a begin() member function that returns an "implicit reference" iterator. You can obtain an
iterator using the make_begin_iterator() et al. functions, which take a (safe) pointer to the container. */
auto iter1 = mse::make_begin_iterator(vo1_regptr1);
auto citer1 = mse::make_end_const_iterator(vo1_regptr1);
citer1 = iter1;
/* fixed_nii_vector<>s can be constructed from other (resizeable) vector types */
auto nvec1 = mse::nii_vector<int>{ 1, 2, 3 };
mse::fixed_nii_vector<int> fixed_nvec1(nvec1);
assert(fixed_nvec1.size() == nvec1.size());
mse::fixed_nii_vector<int> fixed_nvec2(mse::mstd::vector<int>{ 1, 2 });
class A {
public:
A() {}
int m_i = 0;
};
/* Here we're declaring that A can be safely shared between asynchronous threads. */
typedef mse::rsv::TAsyncShareableAndPassableObj<A> shareable_A_t;
/* When the element type of an fixed_nii_vector<> is marked as "async shareable", the fixed_nii_vector<> itself is
(automatically) marked as async shareable as well and can be safely shared between asynchronous threads
using "access requesters". */
auto access_requester1 = mse::make_asyncsharedv2readwrite<mse::fixed_nii_vector<shareable_A_t>>();
auto access_requester2 = mse::make_asyncsharedv2readwrite<fixed_nii_vector1_t>();
/* If the element type of an fixed_nii_vector<> is not marked as "async shareable", then neither is the
fixed_nii_vector<> itself. So attempting to create an "access requester" using it would result in a compile
error. */
//auto access_requester3 = mse::make_asyncsharedv2readwrite<mse::fixed_nii_vector<A>>();
//auto access_requester4 = mse::make_asyncsharedv2readwrite<mse::fixed_nii_vector<mse::mstd::string>>();
{
/* If the vector is declared as a "scope" object (which basically indicates that it is declared
on the stack), then you can use "scope" iterators. While there are limitations on when they can
be used, scope iterators would be the preferred iterator type where performance is a priority
as they don't require extra run-time overhead to ensure that the vector has not been prematurely
deallocated. */
/* Here we're declaring a fixed vector as a scope object. */
mse::TXScopeObj<mse::fixed_nii_vector<int> > vector1_xscpobj{ 1, 2, 3 };
{
/* Here we're obtaining a scope iterator to the vector. */
auto xscp_iter1 = mse::make_xscope_begin_iterator(&vector1_xscpobj);
auto xscp_iter2 = mse::make_xscope_end_iterator(&vector1_xscpobj);
std::sort(xscp_iter1, xscp_iter2);
/* Here we're obtaining a scope pointer from a scope iterator. */
auto xscp_ptr1 = mse::xscope_pointer(xscp_iter1);
auto res3 = *xscp_ptr1;
auto xscp_citer3 = mse::make_xscope_begin_const_iterator(&vector1_xscpobj);
xscp_citer3 = xscp_iter1;
xscp_citer3 = mse::make_xscope_begin_const_iterator(&vector1_xscpobj);
xscp_citer3 += 2;
auto res1 = *xscp_citer3;
auto res2 = xscp_citer3[0];
}
}
}
xscope_fixed_nii_vector<>
is just the scope version of fixed_nii_vector<>
. So unlike fixed_nii_vector<>
, xscope_fixed_nii_vector<>
supports elements of scope type. But as a scope type itself, xscope_fixed_nii_vector<>
is subject to the restrictions of scope objects.
xscope_borrowing_fixed_nii_vector<>
is a kind of xscope_fixed_nii_vector<>
that, at construction, "borrows" (or "takes" by moving) the contents of a specified (via scope pointer) existing vector, then, upon destruction "returns" the (possibly modified) contents back to the original owner. Using a xscope_borrowing_fixed_nii_vector<>
is generally the preferred way to access the elements in a (resizable) vector.
usage example:
#include "msemsevector.h"
#include "mseasyncshared.h"
int main(int argc, char* argv[]) {
auto xs_nii_vector1_xscpobj = mse::make_xscope(mse::nii_vector<int>{ 1, 2, 3 });
/* first we demonstrate some resizing operations on the nii_vector<> */
/* Note that the standard emplace(), insert() and erase() member functions return an iterator. Since nii_vector<> doesn't
support "implicit reference" iterators (i.e. iterators generated from the native "this" pointer) those operations are provided by
free functions that take an explicit (safe) "this" pointer used to generate and return an "explicit reference" iterator. */
mse::push_back(&xs_nii_vector1_xscpobj, 4/*value*/);
mse::erase(&xs_nii_vector1_xscpobj, 2/*position index*/);
mse::insert(&xs_nii_vector1_xscpobj, 1/*position index*/, 5/*value*/);
mse::insert(&xs_nii_vector1_xscpobj, 0/*position index*/, { 6, 7, 8}/*value*/);
mse::emplace(&xs_nii_vector1_xscpobj, 2/*position index*/, 9/*value*/);
const auto nii_vector1_expected = mse::nii_vector<int>{ 6, 7, 9, 8, 1, 5, 2, 4 };
assert(nii_vector1_expected == xs_nii_vector1_xscpobj);
/* Constructing a xscope_borrowing_fixed_nii_vector<> requires a (non-const) scope pointer to an eligible vector. */
auto xs_bf_nii_vector1_xscpobj = mse::make_xscope(mse::make_xscope_borrowing_fixed_nii_vector(&xs_nii_vector1_xscpobj));
{
/* Here we're obtaining a scope iterator to the fixed vector. */
auto xscp_iter1 = mse::make_xscope_begin_iterator(&xs_bf_nii_vector1_xscpobj);
auto xscp_iter2 = mse::make_xscope_end_iterator(&xs_bf_nii_vector1_xscpobj);
std::sort(xscp_iter1, xscp_iter2);
/* Here we're obtaining a scope pointer from a scope iterator. */
auto xscp_ptr1 = mse::xscope_pointer(xscp_iter1);
auto res3 = *xscp_ptr1;
auto xscp_citer3 = mse::make_xscope_begin_const_iterator(&xs_bf_nii_vector1_xscpobj);
xscp_citer3 = xscp_iter1;
xscp_citer3 = mse::make_xscope_begin_const_iterator(&xs_bf_nii_vector1_xscpobj);
xscp_citer3 += 2;
auto res1 = *xscp_citer3;
auto res2 = xscp_citer3[0];
}
class A {
public:
A() {}
int m_i = 0;
};
/* Here we're declaring that A can be safely shared between asynchronous threads. */
typedef mse::rsv::TAsyncShareableAndPassableObj<A> shareable_A_t;
/* When the element type of an xscope_borrowing_fixed_nii_vector<> is marked as "async shareable", the
xscope_borrowing_fixed_nii_vector<> itself is (automatically) marked as async shareable as well and can be safely
shared between asynchronous threads using "access requesters". */
auto xs_shareable_nii_vector1 = mse::make_xscope(mse::nii_vector<shareable_A_t>{});
auto xs_aco_bf_nii_vector1 = mse::make_xscope(mse::make_xscope_access_controlled(mse::make_xscope_borrowing_fixed_nii_vector(&xs_shareable_nii_vector1)));
auto xs_access_requester1 = mse::make_xscope_asyncsharedv2acoreadwrite(&xs_aco_bf_nii_vector1);
auto xs_shareable_nii_vector2 = mse::make_xscope(mse::nii_vector<mse::nii_string>{ "abc", "def" });
auto xs_aco_bf_nii_vector2 = mse::make_xscope(mse::make_xscope_access_controlled(mse::make_xscope_borrowing_fixed_nii_vector(&xs_shareable_nii_vector2)));
auto xs_access_requester2 = mse::make_xscope_asyncsharedv2acoreadwrite(&xs_aco_bf_nii_vector2);
auto xs_shareable_nii_vector3 = mse::make_xscope(mse::nii_vector<A>{});
/* If the element type of an xscope_borrowing_fixed_nii_vector<> is not marked as "async shareable", then neither is the
xscope_borrowing_fixed_nii_vector<> itself. So attempting to create an "access controlled object" using it would result in
a compile error. */
//auto xs_aco_bf_nii_vector3 = mse::make_xscope(mse::make_xscope_access_controlled(mse::make_xscope_borrowing_fixed_nii_vector(&xs_shareable_nii_vector3)));
//auto xs_access_requester3 = mse::make_xscope_asyncsharedv2acoreadwrite(&xs_aco_bf_nii_vector3);
}
Note that the xscope_borrowing_fixed_nii_vector<>
described above can only be obtained from a non-const
(scope) pointer to the lending vector. In situations where only a const
(scope) pointer to the vector is available, xscope_borrowing_fixed_nii_vector<>
has a counterpart, xscope_accessing_fixed_nii_vector<>
, which can be obtained from a const
(scope) pointer to supported vectors (including mstd::vector<>
, stnii_vector<>
and mtnii_vector<>
, but notably not nii_vector<>
(unless it's an xscope exclusive writer object)).
xscope_accessing_fixed_nii_vector<>
, like xscope_borrowing_fixed_nii_vector<>
, ensures, while it exists, that the vector contents are not deallocated/relocated/resized. But unlike xscope_borrowing_fixed_nii_vector<>
, xscope_accessing_fixed_nii_vector<>
's access to the vector contents is not exclusive. So, for example, multiple xscope_accessing_fixed_nii_vector<>
s corresponding to the same vector can exist and be used at the same time. This lack of exclusivity results in xscope_accessing_fixed_nii_vector<>
being branded as ineligible to be passed to, or shared with, asynchronous threads.
usage example: (link to interactive version)
#include "mseslta.h"
#include "msemsevector.h"
int main(int argc, char* argv[]) {
auto vec2 = mse::make_xscope(mse::stnii_vector<int>{ 5, 7 });
{
auto const& vec2_cref = vec2;
/* Obtaining an xscope_borrowing_fixed_nii_vector<> requires a non-const pointer to the lending vector.
When only a const pointer is available we can instead use xscope_accessing_fixed_nii_vector<> for supported vector types. */
auto af_vec2a = mse::make_xscope_accessing_fixed_nii_vector(&vec2_cref);
// or
//auto af_vec2a = mse::xscope_accessing_fixed_nii_vector(&vec2_cref);
auto& elem_ref1 = af_vec2a[0];
int i4 = elem_ref1;
}
{
/* While const (scope) pointers to some vector types aren't supported, "access controlled" (scope)
const pointers would be supported when those vectors are declared as (xscope) "exclusive writer" objects. */
auto const vec3 = mse::make_xscope_exclusive_writer(mse::nii_vector<int>{ 11, 13 });
auto af_vec3a = mse::make_xscope_accessing_fixed_nii_vector(vec3.const_pointer());
auto& elem_ref2 = af_vec3a[0];
int i5 = elem_ref2;
}
}
mse::ivector<>
is a safe vector like mse::mstd::vector<>
, but its (implicit reference) iterators behave more like list iterators than standard vector iterators. That is, upon insert or delete, the iterators continue to point to the same item, not (necessarily) the same position. And they don't become "invalid" upon insert or delete, unless the item they point to is deleted.
usage example: (link to interactive version)
#include "mseivector.h"
int main(int argc, char* argv[]) {
mse::ivector<int> v = { 1, 2, 3, 4 };
auto ip1 = v.begin();
ip1 += 2;
assert(3 == (*ip1));
auto ip2 = v.begin();
v.erase(ip2); /* remove the first item */
assert(3 == (*ip1)); /* ip1 continues to point to the same item, not the same position */
ip1--;
assert(2 == (*ip1));
for (auto cip = v.cbegin(); v.cend() != cip; cip++) {
/* You might imagine what would happen if cip were a regular vector iterator. */
v.insert(v.begin(), (*cip));
}
/* Btw, the iterators are compatible with stl algorithms, like any other stl iterators. */
std::sort(v.begin(), v.end());
}
While the preferred method for accessing elements of a resizable vector is via xscope_borrowing_fixed_nii_vector
, note that constructing an xscope_borrowing_fixed_nii_vector
requires a non-const scope pointer to the resizable vector. For situations where such a non-const scope pointer is not available, the elements can be accessed directly from the resizable vector, potentially with a little more overhead, and some restrictions.
Like the arrays and fixed-size vectors, the provided resizable vectors also support scope iterators. But note that the scope iterators of resizable vectors have some somewhat subtle usage constraints that scope iterators of fixed-size vectors don't have. For safety/performance reasons, scope iterators of "dynamic" containers, like resizable vectors, have the side effect, while they exist, of "structure locking" the associated container. That is, they put the container in a state such that it cannot be resized or relocated, while still allowing the existing contents of the container to be modified or replaced. The "structure lock" is released upon destruction of the scope iterator.
So when you obtain a scope pointer to a vector element from a scope iterator, the assurance that the scope pointer's target element remains valid relies on the fact that the resizable vector is structure locked for the duration of the scope pointer's existence. Since the structure lock is maintained by the scope iterator, the scope iterator must be assured to outlive the obtained scope pointer. This means that obtaining a scope pointer from a temporary/rvalue vector scope iterator is not supported. (Attempting to do so results in a compile error.) Again, this is not the case for array or fixed-size vector scope iterators.
nii_vector<>
has a subtle restriction. Unlike the other vectors, its scope const iterator doesn't "structure lock" the vector and therefore, you cannot obtain a scope pointer from it. The reason it doesn't do structure locking is because const iterators to the same vector can be created in multiple different threads simultaneously and nii_vector<>
does not have a thread-safe structure locking mechanism.
So the library provides mtnii_vector<>
, essentially a version of nii_vector<>
that does have a thread-safe (atomic) structure locking mechanism, and so doesn't suffer the same restriction. The thread-safe locking mechanism does incur some extra cost, so you'd generally only want to use it when you need to share a resizable vector among asynchronous threads. (So presumably rarely.)
usage example:
#include "msemsevector.h"
#include "mseasyncshared.h"
#include "mseregistered.h"
int main(int argc, char* argv[]) {
/* mtnii_vector<> is a safe vector that is eligible to be (safely) shared between asynchronous threads. */
typedef mse::mtnii_vector<mse::mtnii_string> mtnii_vector1_t;
mse::TRegisteredObj<mtnii_vector1_t> rg_vo1;
for (size_t i = 0; i < 5; i += 1) {
rg_vo1.push_back("some text");
}
mse::TRegisteredPointer<mtnii_vector1_t> vo1_regptr1 = &rg_vo1;
/* mtnii_vector<> does not have a begin() member function that returns an "implicit reference" iterator. You can obtain an
iterator using the make_begin_iterator() et al. functions, which take a (safe) pointer to the container. */
auto iter1 = mse::make_begin_iterator(vo1_regptr1);
auto citer1 = mse::make_end_const_iterator(vo1_regptr1);
citer1 = iter1;
rg_vo1.emplace(vo1_regptr1, citer1, "some other text");
rg_vo1.insert(vo1_regptr1, citer1, "some other text");
mse::mtnii_string str1 = "some other text";
rg_vo1.insert(vo1_regptr1, citer1, str1);
class A {
public:
A() {}
int m_i = 0;
};
/* Here we're declaring that A can be safely shared between asynchronous threads. */
typedef mse::rsv::TAsyncShareableAndPassableObj<A> shareable_A_t;
/* When the element type of an mtnii_vector<> is marked as "async shareable", the mtnii_vector<> itself is
(automatically) marked as async shareable as well and can be safely shared between asynchronous threads
using "access requesters". */
auto access_requester1 = mse::make_asyncsharedv2readwrite<mse::mtnii_vector<shareable_A_t>>();
auto access_requester2 = mse::make_asyncsharedv2readwrite<mtnii_vector1_t>();
/* If the element type of an mtnii_vector<> is not marked as "async shareable", then neither is the
mtnii_vector<> itself. So attempting to create an "access requester" using it would result in a compile
error. */
//auto access_requester3 = mse::make_asyncsharedv2readwrite<mse::mtnii_vector<A>>();
//auto access_requester4 = mse::make_asyncsharedv2readwrite<mse::mtnii_vector<mse::mstd::string>>();
typedef mse::mstd::vector<mse::mtnii_string> vector1_t;
vector1_t vo2 = { "a", "b", "c" };
/* mstd::vector<>s, for example, are not safely shareable between threads. But if its element type is
safely shareable, then the contents of the mse::mstd::vector<>, can be swapped with a corresponding
shareable mtnii_vector<>. Note that vector swaps are intrinsically fast operations. */
std::swap(vo2, *(access_requester2.writelock_ptr()));
{
/* If the vector is declared as a "scope" object (which basically indicates that it is declared
on the stack), then you can use "scope" iterators. While there are limitations on when they can
be used, scope iterators would be the preferred iterator type where performance is a priority
as they don't require extra run-time overhead to ensure that the vector has not been prematurely
deallocated. */
/* Here we're declaring a vector as a scope object. */
mse::TXScopeObj<mse::mtnii_vector<int> > vector1_xscpobj = mse::mtnii_vector<int>{ 1, 2, 3 };
{
/* Here we're obtaining a scope iterator to the vector. */
auto xscp_iter1 = mse::make_xscope_begin_iterator(&vector1_xscpobj);
auto xscp_iter2 = mse::make_xscope_end_iterator(&vector1_xscpobj);
std::sort(xscp_iter1, xscp_iter2);
/* Note that scope iterators to vectors (and other dynamic containers), "lock the structure" of the container
so that, for example, it cannot be resized. This allows us to obtain a scope pointer to the iterator's
target element. */
auto xscp_ptr1 = mse::xscope_pointer(xscp_iter1);
auto res3 = *xscp_ptr1;
auto xscp_citer3 = mse::make_xscope_begin_const_iterator(&vector1_xscpobj);
xscp_citer3 = xscp_iter1;
xscp_citer3 = mse::make_xscope_begin_const_iterator(&vector1_xscpobj);
xscp_citer3 += 2;
auto res1 = *xscp_citer3;
auto res2 = xscp_citer3[0];
}
/* After all the scope pointers have gone out of scope, you may again perform operations that affect the container's
"structure" (i.e. size or capacity). */
vector1_xscpobj.push_back(4);
}
}
stnii_vector<>
is a "performance" version of mtnii_vector<>
that is not eligible to be shared among threads. Because scope iterators "structure lock" the container when they are created, and because it's possible to simultaneously/concurrently create multiple (const) scope iterators in different threads, mtnii_vector<>
employs a thread-safe (atomic) locking mechanism. This means that every operation that affects the size of an mtnii_vector<>
makes a thread-safe (atomic) access operation. Since stnii_vector<>
is not eligible to be shared among threads, it does not need to perform any costly thread-safe access operations.
While you might prefer stnii_vector<>
to nii_vector<>
in cases where the vector won't be shared between asynchronous threads, using xscope_borrowing_fixed_nii_vector<>
would still be the preferred method of accessing (or holding references to) the elements.
usage example:
#include "msescope.h"
#include "msemsevector.h"
#include "msemstdvector.h"
int main(int argc, char* argv[]) {
/* stnii_vector<> is just a version of mtnii_vector<> that is not eligible to be shared between threads (and has
a little less overhead as a result). */
mse::TXScopeObj<mse::stnii_vector<int> > vector1_xscpobj = mse::stnii_vector<int>{ 1, 2, 3 };
{
mse::TXScopeFixedConstPointer<mse::stnii_vector<int> > xscptr = &vector1_xscpobj;
auto xscp_citer1 = mse::make_xscope_begin_const_iterator(xscptr);
xscp_citer1 += 2;
auto xscp_cptr1 = mse::xscope_const_pointer(xscp_citer1);
auto res1 = *xscp_cptr1;
}
vector1_xscpobj.push_back(4);
/* stnii_vector<>s can be (efficiently) swapped with mtnii_vector<>s. */
auto mtniiv1 = mse::mtnii_vector<int>();
std::swap(vector1_xscpobj, mtniiv1);
/* Or mstd::vector<>s. */
auto mstdv1 = mse::mstd::vector<int>();
std::swap(vector1_xscpobj, mstdv1);
}
As noted previously, unlike the other vectors, (for thread safety reasons) nii_vector<>
's scope const iterator doesn't "structure lock" the vector and therefore, you cannot obtain a scope pointer from it. However, an object protected by an "excluse writer" access control wrapper is ensured to remain unmodified while a corresponding "access controlled const pointer" exists. So it is safe to obtain a (const) scope pointer from a scope const iterator constructed from an "access controlled const pointer" of an nii_vector<>
protected by an "exclusive writer" access control wrapper.
usage example:
#include "msemsevector.h"
int main(int argc, char* argv[]) {
/* Unfortunately, you cannot obtain a direct scope const pointer to an nii_vector<> element from a scope const
pointer to the nii_vector<>. (nii_vector<> is the only one of the library's vectors that has this shortcoming.)
However, for vectors that are access controlled with an "exclusive writer" access policy, you can use an
"exclusive writer" const pointer to obtain a direct scope const pointer to a vector element. */
auto xs_ew_niivec1 = mse::make_xscope(mse::make_exclusive_writer(mse::nii_vector<int>{ 1, 2, 3 }));
// which can also be written as:
// mse::TXScopeObj<mse::TExclusiveWriterObj<mse::nii_vector<int> > > xs_ew_niivec1 = mse::nii_vector<int>{ 1, 2, 3 };
{
auto xs_ew_cptr1 = mse::make_xscope_access_controlled_const_pointer(xs_ew_niivec1);
/* The following scope iterator is of a type that reflects that it was constructed from an "exclusive writer"
const pointer. */
auto xs_citer1 = mse::make_xscope_begin_const_iterator(xs_ew_cptr1);
xs_citer1 += 2;
/* We can obtain a (zero-overhead) scope pointer from the (lvalue) iterator. */
auto xs_cptr1 = mse::xscope_const_pointer(xs_citer1);
auto val1 = *xs_cptr1;
}
/* Once the "access controlled const pointer" no longer exists, we can obtain an "access controlled
(non-const) pointer" and modify the vector again. */
mse::push_back(mse::make_xscope_access_controlled_pointer(xs_ew_niivec1), 4);
}
TXScopeRandomAccessSection, TXScopeRandomAccessConstSection, TRandomAccessSection, TRandomAccessConstSection
A "random access section" is basically a convenient interface to access a (contiguous) subsection of an existing array or vector. You construct them, using the make_random_access_section()
functions, by specifying an iterator to the start of the section, and the length of the section. Random access sections support most of the member functions and operators that std::basic_string_view does, except that the "substr()" member function is named "subsection()".
Note that for convenience, random access sections can be constructed from just a (safe) pointer to a supported container object, but in some cases the exact type of the resulting random access section may not be obvious. Constructing instead from a specified iterator and length should avoid any ambiguity.
usage example:
#include "msemsearray.h" //random access sections are defined in this file
#include "msemstdarray.h"
#include "msemstdvector.h"
class J {
public:
template<class _TRASection>
static void foo13(_TRASection ra_section) {
for (_TRASection::size_type i = 0; i < ra_section.size(); i += 1) {
ra_section[i] = 0;
}
}
template<class _TRAConstSection>
static int foo14(_TRAConstSection const_ra_section) {
int retval = 0;
for (_TRAConstSection::size_type i = 0; i < const_ra_section.size(); i += 1) {
retval += const_ra_section[i];
}
return retval;
}
template<class _TRAConstSection>
static int foo15(_TRAConstSection const_ra_section) {
int retval = 0;
for (const auto& const_item : const_ra_section) {
retval += const_item;
}
return retval;
}
};
int main(int argc, char* argv[]) {
mse::mstd::array<int, 4> mstd_array1{ 1, 2, 3, 4 };
mse::mstd::vector<int> mstd_vec1{ 10, 11, 12, 13, 14 };
auto xscp_ra_section1 = mse::make_xscope_random_access_section(mstd_array1.begin(), 2);
J::foo13(xscp_ra_section1);
auto ra_const_section2 = mse::make_random_access_const_section(++mstd_vec1.begin(), 3);
auto res6 = J::foo15(ra_const_section2);
auto res7 = J::foo14(ra_const_section2);
auto xscp_ra_section1_xscp_iter1 = xscp_ra_section1.xscope_begin();
auto xscp_ra_section1_xscp_iter2 = xscp_ra_section1.xscope_end();
auto res8 = xscp_ra_section1_xscp_iter2 - xscp_ra_section1_xscp_iter1;
bool res9 = (xscp_ra_section1_xscp_iter1 < xscp_ra_section1_xscp_iter2);
/* Like non-owning scope pointers, scope sections may not be used as a function return value. (The return_value()
function wrapper enforces this.) Pretty much the only time you'd legitimately want to do this is when the
returned section is constructed from one of the input parameters. Let's consider a simple example of a
"first_half()" function that takes a scope section and returns a scope section spanning the first half of the
section. The library provides the random_access_subsection() function which takes a random access section and a
tuple containing a start index and a length and returns a random access section spanning the indicated
subsection. You could use this function to implement the equivalent of a "first_half()" function like so: */
auto xscp_ra_section3 = mse::random_access_subsection(xscp_ra_section1, std::make_tuple(0, xscp_ra_section1.length()/2));
assert(xscp_ra_section3.length() == 1);
}
Note that scope sections, like (non-owning) scope pointers, are not retargetable (i.e. don't support the assignment operator). If you find yourself needing a retargetable scope section, you can use the technique of adding a level of indirection and using a (retargetable) registered (or norad) pointer to the scope sections, is demonstrated with scope pointers.
See also TXScopeCSSSXSTERandomAccessSection.
From an interface perspective, you might think of strings roughly as glorified vectors of characters, and thus they are given similar treatment in the library. A few string types are provided that correspond to their vector counterparts. mstd::string
is a memory-safe drop-in replacement for std::string
and nii_string
is a string that doesn't support "implicit reference" iterators and (so) is eligible to be safely shared among asynchronous threads. xscope_borrowing_fixed_nii_basic_string<>
is a non-resizable string that facilitates more efficient and less restricted direct access to characters in the string.
mstd::string
is a memory-safe drop-in replacement for std::string
. As with the standard library, mstd::string
is defined as an alias for mstd::basic_string<char>
. The mstd::wstring
, mstd::u16string
and mstd::u32string
aliases are also present.
Like its vector counterpart, nii_vector<>
, nii_string
is a string that does not support "implicit reference" iterators, allowing it to be safely shareable between asynchronous threads. But directly accessing the characters of an nii_string
is discouraged. Prefer instead to access (or hold references to) characters of the string by using an xscope_borrowing_fixed_nii_basic_string<>
. The nii_wstring
, nii_u16string
and nii_u32string
aliases are also present.
See the fixed_nii_vector<>
counterpart.
See the xscope_fixed_nii_vector<>
counterpart.
See the xscope_borrowing_fixed_nii_vector<>
counterpart.
"String sections" are string specialized versions of "random access sections".
usage example:
#include "msemsestring.h" // make_string_section() is defined in this header
#include "msemstdstring.h"
int main(int argc, char* argv[]) {
/* "String sections" are the string specialized versions of "random access sections", basically providing the
functionality of std::string_view but supporting construction from any (safe) iterator type, not just raw
pointer iterators. */
mse::mstd::string mstring1("some text");
auto string_section1 = mse::make_string_section(mstring1.begin() + 1, 7);
auto string_section2 = string_section1.substr(4, 3);
assert(string_section2.front() == 't');
assert(string_section2.back() == 'x');
/* Unlike std::string_view, string sections are available in "non-const" versions. */
string_section2[0] = 'T';
std::cout << string_section2;
assert(mstring1 == "some Text");
}
TNRPStringSection<>
is just a version of TStringSection<>
that, for enhanced safety, does not support construction from unsafe raw pointer iterators or (unsafe) std::string
iterators. Use the make_nrp_string_section()
functions to create them.
std::string_view
is, in a way, a problematic addition to the standard library in the sense that it has an intrinsically unsafe interface. That is, its constructors support only (unsafe) raw pointer iterator parameters. In contrast, the standard library generally uses iterator types which allow for the option of a memory safe implementation. So to enable memory safe use, this library's version, mstd::string_view
, generalizes the interface to support construction from safe iterator types. So while technically mstd::string_view
can act as a drop-in replacement for std::string_view
, it is designed to be used with safe iterator types, not unsafe raw pointer iterators.
Like std::string_view
, mstd::string_view
is defined as an alias for mstd::basic_string_view<char>
. The mstd::wstring_view
, mstd::u16string_view
and mstd::u32string_view
aliases are also present. Note that mstd::basic_string_view<>
is in fact just a slightly augmented version of TAnyStringConstSection<>
.
usage example:
#include "msepoly.h" // mstd::string_view is defined in this header
#include "msemstdstring.h"
int main(int argc, char* argv[]) {
/* std::string_view stores an (unsafe) pointer iterator into its target string. mse::mstd::string_view can
instead store any type of string iterator, including memory safe iterators. So for example, when assigned
from an mse::mstd::string, mse::mstd::string_view will hold one of mse::mstd::string's safe (strong) iterators
(obtained with a call to the string's cbegin() member function). Consequently, the mse::mstd::string_view will
be safe against "use-after-free" bugs to which std::string_view is so prone. */
mse::mstd::string_view msv1;
{
mse::mstd::string mstring1("some text");
msv1 = mstring1;
}
try {
/* This is not undefined (or unsafe) behavior. */
auto ch1 = msv1[3]; /* In debug mode this will fail an assert. In non-debug mode it'll just work (safely). */
assert('e' == ch1);
}
catch (...) {
/* At present, no exception will be thrown. Instead, the lifespan of the string data is extended to match
that of the mstd::string_view. It's possible that in future library implementations, an exception may be thrown. */
}
mse::mstd::string mstring2("some other text");
/* With std::string_view, you specify a string subrange with a raw pointer iterator and a length. With
mse::mstd::string_view you are not restricted to (unsafe) raw pointer iterators. You can use memory safe
iterators like those provided by mse::mstd::string. */
auto msv2 = mse::mstd::string_view(mstring2.cbegin()+5, 7);
assert(msv2 == "other t");
}
See also TXScopeCSSSXSTEStringSection.
mse::nrp_string_view
is just a version of mse::mstd::string_view
that, for enhanced safety, does not support construction from unsafe raw pointer iterators or (unsafe) std::string
iterators.
TXScopeAnyRandomAccessIterator, TXScopeAnyRandomAccessConstIterator, TAnyRandomAccessIterator, TAnyRandomAccessConstIterator
In modern C++ (and SaferCPlusPlus), arrays of different sizes are actually different types, with incompatible iterators. So, for example, if you wanted to make a function that accepts the iterators of arrays of varying size, you would generally do that by "templatizing" the function. Alternatively, you could use an "any random access iterator" which is a "chameleon" (type-erased) iterator that can be constructed from basically any iterator that supports operator[]
(the "square bracket" operator).
TXScopeAnyRandomAccessSection, TXScopeAnyRandomAccessConstSection, TAnyRandomAccessSection, TAnyRandomAccessConstSection
TAnyRandomAccessSection<_Ty>
is essentially just an alias for TRandomAccessSection<TAnyRandomAccessIterator<_Ty> >
. Analogous to TAnyRandomAccessIterator<>
, it can be used to enable a function to accept, as a parameter, any type of "random access section".
usage example:
#include "msepoly.h"
int main(int argc, char* argv[]) {
mse::mstd::array<int, 4> array1 { 1, 2, 3, 4 };
mse::mstd::array<int, 5> array2 { 5, 6, 7, 8, 9 };
mse::mstd::vector<int> vec1 { 10, 11, 12, 13, 14 };
class B {
public:
static void foo1(mse::TXScopeAnyRandomAccessIterator<int> ra_iter1) {
ra_iter1[1] = 15;
}
static int foo2(mse::TXScopeAnyRandomAccessConstIterator<int> const_ra_iter1) {
const_ra_iter1 += 2;
--const_ra_iter1;
const_ra_iter1--;
return const_ra_iter1[2];
}
static void foo3(mse::TXScopeAnyRandomAccessSection<int> ra_section) {
for (mse::TXScopeAnyRandomAccessSection<int>::size_type i = 0; i < ra_section.size(); i += 1) {
ra_section[i] = 0;
}
}
static int foo4(mse::TXScopeAnyRandomAccessConstSection<int> const_ra_section) {
int retval = 0;
for (mse::TXScopeAnyRandomAccessSection<int>::size_type i = 0; i < const_ra_section.size(); i += 1) {
retval += const_ra_section[i];
}
return retval;
}
static int foo5(mse::TXScopeAnyRandomAccessConstSection<int> const_ra_section) {
int retval = 0;
for (const auto& const_item : const_ra_section) {
retval += const_item;
}
return retval;
}
};
auto array_iter1 = array1.begin();
array_iter1++;
auto res1 = B::foo2(array_iter1);
B::foo1(array_iter1);
auto array_const_iter2 = array2.cbegin();
array_const_iter2 += 2;
auto res2 = B::foo2(array_const_iter2);
auto res3 = B::foo2(vec1.cbegin());
B::foo1(++vec1.begin());
auto res4 = B::foo2(vec1.begin());
mse::TXScopeAnyRandomAccessSection<int> ra_section1(array_iter1, 2);
B::foo3(ra_section1);
mse::TXScopeAnyRandomAccessSection<int> ra_section2(++vec1.begin(), 3);
auto res5 = B::foo5(ra_section2);
B::foo3(ra_section2);
auto res6 = B::foo4(ra_section2);
}
TAnyStringSection<_Ty>
is essentially just an alias for TStringSection<TAnyRandomAccessIterator<_Ty> >
. Like TAnyRandomAccessSection<_Ty>
, it can be used to enable a function to accept, as a parameter, any type of "string section".
usage example:
#include "msepoly.h" // TAnyStringSection<> is defined in this header
#include "msemstdstring.h"
#include "msemsestring.h"
#include "mseregistered.h"
int main(int argc, char* argv[]) {
/* "Any" string sections are basically polymorphic string sections that can hold the value of any string
section type. They can be used as function parameter types to enable functions to accept any type of string
section. */
mse::mstd::string mstring1("some text");
mse::TAnyStringSection<char> any_string_section1(mstring1.begin()+5, 3);
auto string_literal = "some text";
mse::TAnyStringConstSection<char> any_string_const_section2(string_literal+5, 3);
typedef mse::TRegisteredObj<mse::nii_string> reg_nii_string_t;
reg_nii_string_t reg_nii_string3("some other text");
/* This is a different type of (safe) iterator to a different type of string. */
auto iter = reg_nii_string_t::ss_begin(®_nii_string3);
/* Resulting in a different type of string section. */
auto string_section3 = mse::make_string_section(iter+11, 3);
mse::TAnyStringSection<char> any_string_section3 = string_section3;
assert(any_string_section1 == any_string_section3);
assert(any_string_section1.front() == 't');
assert(any_string_section1.back() == 'x');
any_string_section1 = string_section3;
any_string_section1[1] = 'E';
}
TXScopeAnyNRPStringSection, TXScopeAnyNRPStringConstSection, TAnyNRPStringSection, TAnyNRPStringConstSection
TAnyNRPStringSection<>
is just a version of TAnyStringSection<>
that, for enhanced safety, doesn't support construction from unsafe raw pointer iterators or (unsafe) std::string
iterators.
TXScopeCSSSXSTERandomAccessIterator<>
and TXScopeCSSSXSTERandomAccessSection<>
are "type-erased" template classes that function much like TXScopeAnyRandomAccessIterator<>
and TXScopeAnyRandomAccessSection<>
in that they can be used to enable functions to take as arguments iterators or sections of various container types (like arrays or vectors) without making the functions into template functions. But in this case there are limitations on what types can be converted. In exchange for these limitations, these types require less overhead. The "CSSSXSTE" part of the typenames stands for "Contiguous Sequence, Static Structure, XScope, Type-Erased". So the first restriction is that the target container must be recognized as a "contiguous sequence" (basically an array or vector). It also must be recognized as having a "static structure". This essentially means that the container cannot be resized. Note that while vectors support resizing, the ones in the library can be "structure locked" at run-time to ensure that they are not resized (i.e. have a "static structure") during certain periods. And only the "scope" versions of the iterators and sections are supported.
TXScopeCSSSXSTERandomAccessSection<>
might be considered, in essence, the primary safe counterpart of std::span<>
. While TXScopeAnyRandomAccessSection<>
more closely matches the flexibilty of std::span<>
, TXScopeCSSSXSTERandomAccessSection<>
would more closely match its (low) overhead.
usage example:
#include "msemsearray.h" //TXScopeCSSSXSTERandomAccessIterator/Section are defined in this header
#include "msemstdarray.h"
#include "msemstdvector.h"
int main(int argc, char* argv[]) {
auto xs_mstd_array1 = mse::make_xscope(mse::mstd::array<int, 4>{ 1, 2, 3, 4 });
auto xs_msearray2 = mse::make_xscope(mse::us::msearray<int, 5>{ 5, 6, 7, 8, 9 });
auto xs_mstd_vec1 = mse::make_xscope(mse::mstd::vector<int>{ 10, 11, 12, 13, 14 });
class B {
public:
static void foo1(mse::TXScopeCSSSXSTERandomAccessIterator<int> ra_iter1) {
ra_iter1[1] = 15;
}
static int foo2(mse::TXScopeCSSSXSTERandomAccessConstIterator<int> const_ra_iter1) {
const_ra_iter1 += 2;
--const_ra_iter1;
const_ra_iter1--;
return const_ra_iter1[2];
}
static void foo3(mse::TXScopeCSSSXSTERandomAccessSection<int> ra_section) {
for (mse::TXScopeCSSSXSTERandomAccessSection<int>::size_type i = 0; i < ra_section.size(); i += 1) {
ra_section[i] = 0;
}
}
static int foo4(mse::TXScopeCSSSXSTERandomAccessConstSection<int> const_ra_section) {
int retval = 0;
for (mse::TXScopeCSSSXSTERandomAccessSection<int>::size_type i = 0; i < const_ra_section.size(); i += 1) {
retval += const_ra_section[i];
}
return retval;
}
static int foo5(mse::TXScopeCSSSXSTERandomAccessConstSection<int> const_ra_section) {
int retval = 0;
for (const auto& const_item : const_ra_section) {
retval += const_item;
}
return retval;
}
};
auto xs_mstd_array_iter1 = mse::make_xscope_begin_iterator(&xs_mstd_array1);
xs_mstd_array_iter1++;
auto res1 = B::foo2(xs_mstd_array_iter1);
B::foo1(xs_mstd_array_iter1);
auto xs_msearray_const_iter2 = mse::make_xscope_begin_const_iterator(&xs_msearray2);
xs_msearray_const_iter2 += 2;
auto res2 = B::foo2(xs_msearray_const_iter2);
auto xs_mstd_vec1_citer1 = mse::make_xscope_begin_const_iterator(&xs_mstd_vec1);
auto res3 = B::foo2(xs_mstd_vec1_citer1);
/*
Note that attempting to combine the previous two lines into a single expression like so:
auto res3b = B::foo2(mse::make_xscope_begin_const_iterator(&xs_mstd_vec1));
is not supported and would result in a compile error.
In the case of dynamic containers, like vectors and strings, the (scope) iterators can only be converted to
TXScopeCSSSXSTERandomAccessIterator<>s if they are lvalues, not temporaries/rvalues. This is because scope iterators
hold a "structure lock" that prevents the container from being resized. If the scope iterator you're attempting to
convert from is a temporary (rvalue) one, then the structure lock would be just as temporary and it would not be safe
to create a TXScopeCSSSXSTERandomAccessIterator<> that outlives the period when the container is "structure locked".
"Non-resizable" containers, like arrays, do not have the same issue, so this restriction does not apply to their
(scope) iterators.
*/
auto xs_mstd_vec1_citer2 = ++mse::make_xscope_begin_iterator(&xs_mstd_vec1);
B::foo1(xs_mstd_vec1_citer2);
auto xs_mstd_vec1_iter1 = mse::make_xscope_begin_iterator(&xs_mstd_vec1);
auto res4 = B::foo2(xs_mstd_vec1_iter1);
auto xs_mstd_vec1_begin_iter1 = mse::make_xscope_begin_iterator(&xs_mstd_vec1);
mse::TXScopeCSSSXSTERandomAccessIterator<int> xs_te_iter1 = xs_mstd_vec1_begin_iter1;
auto xs_mstd_vec1_end_iter1 = mse::make_xscope_end_iterator(&xs_mstd_vec1);
mse::TXScopeCSSSXSTERandomAccessIterator<int> xs_te_iter2 = xs_mstd_vec1_end_iter1;
auto res5 = xs_te_iter2 - xs_te_iter1;
xs_te_iter2 = xs_te_iter1;
{
auto std_array1 = mse::make_xscope(std::array<int, 4>{ 1, 2, 3, 4 });
mse::TXScopeCSSSXSTERandomAccessIterator<int> xs_te_iter1(mse::make_xscope_begin_iterator(&std_array1));
mse::TXScopeCSSSXSTERandomAccessIterator<int> xs_te_iter2 = mse::make_xscope_end_iterator(&std_array1);
auto res5 = xs_te_iter2 - xs_te_iter1;
xs_te_iter1 = xs_te_iter2;
}
mse::TXScopeObj<mse::mstd::array<int, 4> > mstd_array3_scbobj = mse::mstd::array<int, 4>({ 1, 2, 3, 4 });
auto mstd_array_scpiter3 = mse::mstd::make_xscope_begin_iterator(&mstd_array3_scbobj);
++mstd_array_scpiter3;
B::foo1(mstd_array_scpiter3);
mse::TXScopeCSSSXSTERandomAccessSection<int> xscp_ra_section1(xs_mstd_array_iter1++, 2);
B::foo3(xscp_ra_section1);
auto xs_mstd_vec_iter1 = mse::make_xscope_begin_iterator(&xs_mstd_vec1);
mse::TXScopeCSSSXSTERandomAccessSection<int> xscp_ra_section2(++xs_mstd_vec_iter1, 3);
/*
Note that had we instead used the postfix increment operator on the xs_mstd_vec_iter1 argument in the previous line
like so:
mse::TXScopeCSSSXSTERandomAccessSection<int> xscp_ra_section2b(xs_mstd_vec_iter1++, 3);
we would have gotten a compile error. That's because unlike the prefix increment operator, the postfix increment
operator returns a temporary (rvalue) copy of the iterator, and as noted earlier, in the case of dynamic containers,
like vectors and strings, conversion/construction from temporary (rvalue) versions of their iterators could be unsafe
and is not supported.
*/
auto res6 = B::foo5(xscp_ra_section2);
B::foo3(xscp_ra_section2);
auto res7 = B::foo4(xscp_ra_section2);
auto xscp_ra_section1_xscp_iter1 = xscp_ra_section1.xscope_begin();
auto xscp_ra_section1_xscp_iter2 = xscp_ra_section1.xscope_end();
auto res8 = xscp_ra_section1_xscp_iter2 - xscp_ra_section1_xscp_iter1;
bool res9 = (xscp_ra_section1_xscp_iter1 < xscp_ra_section1_xscp_iter2);
auto xs_ra_section1 = mse::make_xscope_random_access_section(xs_mstd_array_iter1, 2);
B::foo3(xs_ra_section1);
auto xs_ra_const_section2 = mse::make_xscope_random_access_const_section(mse::make_xscope_begin_const_iterator(&xs_mstd_vec1), 2);
B::foo4(xs_ra_const_section2);
}
TXScopeCSSSXSTEStringSection<>
is the string specialized version of TXScopeCSSSXSTERandomAccessSection<>
. Like TXScopeAnyStringSection<>
, it can, with lower overhead but more restrictions, be used to enable functions to take as arguments sections of strings. While mstd::string_view
(/TAnyStringConstSection<char>
) closely matches the interface and flexibilty of std::string_view
, TXScopeCSSSXSTEStringConstSection<char>
would more closely match its (low) overhead.
usage example:
#include "msemsestring.h" // TXScopeCSSSXSTEStringSection is defined in this header
#include "msemstdstring.h"
int main(int argc, char* argv[]) {
auto xs_mstring1 = mse::make_xscope(mse::mstd::string("some text"));
auto xs_mstring1_iter1 = mse::make_xscope_begin_iterator(&xs_mstring1) + 5;
mse::TXScopeCSSSXSTEStringSection<char> xs_csssxste_string_section1(xs_mstring1_iter1, 3);
/* Note when passing a (scope) string iterator to TXScopeCSSSXSTEStringSection<>'s constructor, as with
TXScopeCSSSXSTERandomAccessSection<>, that iterator must be an lvalue, not a temporary (rvalue), otherwise
you'll get a compile error.*/
mse::TXScopeCSSSXSTEStringConstSection<char> xs_csssxste_string_const_section2("some text");
auto xs_nii_string3 = mse::make_xscope(mse::nii_string("some other text"));
auto xs_string_section3 = mse::make_xscope_string_section(mse::make_xscope_begin_iterator(&xs_nii_string3) + 11, 3);
/* Note that xs_string_section3 is just a regular string section, not a TXScopeCSSSXSTEStringSection<>, so we
can construct it from an rvalue iterator. */
mse::TXScopeCSSSXSTEStringSection<char> xs_csssxste_string_section3 = xs_string_section3;
/* As with iterators, only construction from lvalue string sections is supported, not temporary (rvalue) string sections. */
assert(xs_csssxste_string_section1 == xs_csssxste_string_section3);
assert(xs_csssxste_string_section1.front() == 't');
assert(xs_csssxste_string_section1.back() == 'x');
xs_csssxste_string_section1[1] = 'E';
}
Conceptually, you might think of an optional<>
as a dynamic container, like a vector<>
, that supports a maximum of one element. So the library provides a few versions of optional<>
that roughly correspond to their vector<>
counterparts. The library introduces "optional element" pointers, which would loosely correspond to iterators.
mstd::optional<>
is essentially just a safe implementation of std::optional<>.
usage example:
#include "mseoptional.h"
int main(int argc, char* argv[]) {
auto opt1 = mse::mstd::optional<int>{ 7 };
assert(opt1.has_value());
auto val1 = opt1.value();
}
Analogous to its vector counterpart, nii_vector<>
, optional<>
is eligible to be shared among asynchronous threads. And like nii_vector<>
, the preferred way of accessing or referencing the contained element is via xscope_borrowing_fixed_optional<>
.
usage example: (see the example for xscope_borrowing_fixed_optional<>
)
Analogous to its vector counterpart, fixed_nii_vector<>
, fixed_optional<>
is just a version of optional<>
whose number of contained elements (zero or one) is fixed at construction.
Analogous to its vector counterpart, xscope_fixed_nii_vector<>
, xscope_fixed_optional<>
is just a version of fixed_optional<>
that supports scope types as its element type, and as a scope type itself, is subject to the restrictions of scope objects.
Analogous to its vector counterpart, xscope_borrowing_fixed_nii_vector<>
, xscope_borrowing_fixed_optional<>
is like xscope_fixed_optional<>
, except that, at construction, "borrows" the contents of a specified (via scope pointer) existing optional<>
, then, upon destruction "releases" the (possibly modified) contents back to the original owner. Unlike xscope_borrowing_fixed_nii_vector<>
though, the contained element, if any, is not (necessarily) moved when "borrowed", but equivalently, the "borrowing" object is granted "exclusive access" to the "lending" object for the duration of the borrow.
usage example:
#include "mseoptional.h"
#include "msemstdstring.h"
int main(int argc, char* argv[]) {
auto xs_opt1 = mse::make_xscope(mse::make_optional(mse::mstd::string("abc")));
// which can also be written as
// auto xs_opt1 = mse::TXScopeObj<mse::optional<mse::mstd::string> >("abc");
xs_opt1 = {};
xs_opt1 = mse::mstd::string("def");
{
/* Here we obtain an xscope_borrowing_fixed_optional<> that "borrows" the contents of xs_opt1. */
auto xs_bfopt1 = mse::make_xscope_borrowing_fixed_optional(&xs_opt1);
/* Note that accessing xs_opt1 is prohibited while xs_bfopt1 exists. This restriction is enforced.*/
/* Here we obtain a (safe) pointer to the contained element. */
auto xs_elem_ptr1 = mse::make_xscope_fixed_optional_element_pointer(&xs_bfopt1);
/* We can also then obtain a scope pointer to the contained element. */
auto xs_ptr1 = mse::xscope_pointer(xs_elem_ptr1);
auto val1 = *xs_ptr1;
*xs_ptr1 = mse::mstd::string("ghi");
}
/* After the xscope_borrowing_fixed_optional<> is gone, we can again access our optional<>. */
xs_opt1.reset();
}
Analogous to mtnii_vector<>
, mt_optional<>
supports direct access and references to its contained element and is eligible to be shared among threads (when its contained element is eligible to be shared), at a cost of slightly higher run-time overhead. Like their iterator counterparts, scope optional element pointers, while they exist, hold a "structure lock" on their target optional<>
object which prevents the contained element from being destroyed. As with the vectors, xscope_borrowing_fixed_optional<>
(like all the "fixed-size" optionals) can also be shared among threads and are generally preferred when suitable.
usage example:
#include "mseoptional.h"
#include "msemsestring.h"
#include "mseasyncshared.h"
int main(int argc, char* argv[]) {
auto opt1_access_requester = mse::make_asyncsharedv2readwrite<mse::mt_optional<mse::nii_string> >("abc");
auto elem_ptr1 = mse::make_optional_element_pointer(opt1_access_requester.writelock_ptr());
auto val1 = *elem_ptr1;
}
mstd::optional<>
and mt_optional<>
can, like any other type, be declared as a scope type (using mse::make_xscope()
/ mse::TXScopeObj<>
). But they do not support using scope types as their contained element type. It is (intended to be) uncommon to need such capability. But the library does provide a couple of versions that support it. xscope_mt_optional<>
is eligible to be shared among (scope) threads, while xscope_st_optional<>
is not. xscope_mt_optional<>
and xscope_st_optional<>
are of course themselves scope types and subject to the corresponding usage restrictions.
usage example:
#include "mseoptional.h"
#include "msemstdstring.h"
int main(int argc, char* argv[]) {
/* Here we're creating a (string) object of scope type. */
auto xs_str1 = mse::make_xscope(mse::mstd::string("abc"));
/* Here we're creating an xscope_st_optional<> object that contains a scope pointer to the (scope) string object.
mstd::optional<>, for example, would not support this. */
auto xsopt1 = mse::make_xscope_st_optional(&xs_str1);
// which can also be written as
// auto xsopt1 = mse::xscope_st_optional<mse::TXScopeFixedPointer<mse::mstd::string> >(&xs_str1);
auto val1 = *(xsopt1.value());
}
Conceptually, you might think of an any
as a dynamic container, like an optional<>
, that supports a maximum of one element, but without restriction on the element type. So the library provides a few versions of any
, and "any element" pointers that roughly correspond to their optional<>
counterparts.
In contrast to std::optional<>::value()
which returns a reference to the contained element, std::any_cast<>()
does not return a reference. If the argument is an std::any
, it returns a value corresponding to the contained element. If the argument is instead a (raw) pointer to an std::any
, it returns a (possibly null) raw pointer to the contained element.
Because of safety requirements, this library's implementation of any_cast<>()
accepts any type of (safe smart) pointer targeting any of the library's any
types, and returns another (possibly null) safe (smart) pointer to the contained element. Additionally, the library provides a maybe_any_cast_ptr<>()
template function which (only) accepts any type of (safe smart) pointer targeting any of the library's any
types and returns an optional<>
pointer to the contained element (in lieu of returning a null pointer when the cast fails), as well as maybe_any_cast<>()
which accepts any of the library's any
types (rather than a pointer to one of them) and returns an optional<>
value (in lieu of throwing an exception when the cast fails).
mstd::any
is essentially just a safe implementation of std::any.
usage example:
#include "mseany.h"
int main(int argc, char* argv[]) {
auto any1 = mse::mstd::any<int>{ 7 };
assert(any1.has_value());
auto val1 = mse::mstd::any_cast<int>(any1);
}
Unlike its optional counterpart, optional<>
, any
is not eligible to be shared among asynchronous threads, as at any given time its contained element may itself not be safely shareable. But like optional<>
, the preferred way of accessing or referencing the contained element is via xscope_borrowing_fixed_any<>
.
usage example: (see the example for xscope_borrowing_fixed_any<>
)
Analogous to its optional counterpart, fixed_optional<>
, fixed_any
is just a version of any
whose contained element type and quantity (zero or one) is fixed at construction.
Analogous to its optional counterpart, xscope_fixed_optional<>
, xscope_fixed_any
is just a version of fixed_any
that supports elements of scope type, and as a scope type itself, is subject to the restrictions of scope objects.
Analogous to its optional counterpart, xscope_borrowing_fixed_optional<>
, xscope_borrowing_fixed_any<>
is like xscope_fixed_any
, except that, at construction, "borrows" the contents of a specified (via scope pointer) existing any
, then, upon destruction "releases" the (possibly modified) contents back to the original owner. Like xscope_borrowing_fixed_optional<>
, the contained element, if any, is not (necessarily) moved when "borrowed", but equivalently, the "borrowing" object is granted "exclusive access" to the "lending" object for the duration of the borrow.
usage example:
#include "mseany.h"
#include "msemstdstring.h"
int main(int argc, char* argv[]) {
auto xs_any1 = mse::make_xscope(mse::make_any<mse::mstd::string>("abc"));
// which can also be written as
// auto xs_any1 = mse::TXScopeObj<mse::any<mse::mstd::string> >("abc");
xs_any1 = {};
xs_any1 = mse::mstd::string("def");
{
/* Here we obtain an xscope_borrowing_fixed_any<> that "borrows" the contents of xs_any1. */
auto xs_bfany1 = mse::make_xscope_borrowing_fixed_any(&xs_any1);
/* Note that accessing xs_any1 is prohibited while xs_bfany1 exists. This restriction is enforced.*/
/* Here we obtain a (safe) pointer to the contained element. */
auto xs_elem_ptr1 = mse::make_xscope_fixed_any_element_pointer<mse::mstd::string>(&xs_bfany1);
/* We can also then obtain a scope pointer to the contained element. */
auto xs_ptr1 = mse::xscope_pointer(xs_elem_ptr1);
auto val1 = *xs_ptr1;
*xs_ptr1 = mse::mstd::string("ghi");
}
/* After the xscope_borrowing_fixed_any<> is gone, we can again access our `any`. */
xs_any1.reset();
}
Analogous to mtoptional<>
, mt_any
supports direct access and references to its contained element and is eligible to be shared among threads (and thus can only accept values of shareable type). Like their iterator counterparts, scope any element pointers, while they exist, hold a "structure lock" on their target any
object which prevents the contained element from being destroyed. As with the optionals, xscope_borrowing_fixed_any<>
(like all the "fixed-size" anys) can also be shared among threads and are generally preferred when suitable.
usage example:
#include "mseany.h"
#include "msemsestring.h"
#include "mseasyncshared.h"
int main(int argc, char* argv[]) {
auto any1_access_requester = mse::make_asyncsharedv2readwrite<mse::mt_any>(mse::nii_string("abc"));
auto elem_ptr1 = mse::make_any_element_pointer<mse::nii_string>(any1_access_requester.writelock_ptr());
auto val1 = *elem_ptr1;
}
mstd::any
and mt_any
can, like any other type, be declared as a scope type (using mse::make_xscope()
/ mse::TXScopeObj<>
). But they do not support using scope types as their contained element type. It is (intended to be) uncommon to need such capability. But the library does provide a couple of versions that support it. xscope_mt_any
is eligible to be shared among (scope) threads, while xscope_st_any
is not. xscope_mt_any
and xscope_st_any
are of course themselves scope types and subject to the corresponding usage restrictions.
usage example:
#include "mseany.h"
#include "msemstdstring.h"
int main(int argc, char* argv[]) {
/* Here we're creating a (string) object of scope type. */
auto xs_str1 = mse::make_xscope(mse::mstd::string("abc"));
/* Here we're creating an xscope_st_any object that contains a scope pointer to the (scope) string object.
mstd::any, for example, would not support this. */
auto xsany1 = mse::make_xscope_st_any<decltype(&xs_str1)>(&xs_str1);
auto val1 = *(mse::any_cast<decltype(&xs_str1)>(xsany1));
}
Tuples present fewer safety issues than containers with iterators or dynamic structure. But the provided tuple types, aside from supporting the library's scope and data race safety mechanisms, also come with safe pointer types that directly target individual tuple elements.
mstd::tuple<>
is essentially a drop-in substitute for std::tuple<>
.
usage example:
#include "msetuple.h"
#include "msemstdstring.h"
#include "mserefcounting.h"
int main(int argc, char* argv[]) {
{
/* You may, on occasion, need a (safe) pointer that directly targets a tuple element. You
could make the element type a "registered" or "norad" object. Alternatively, you can obtain a safe
pointer to a contained element from a pointer to the tuple<> object like so: */
auto tup1_refcptr = mse::make_refcounting<mse::mstd::tuple<double, char, mse::mstd::string> >(mse::mstd::make_tuple(3.8, 'A', mse::mstd::string("Lisa Simpson")));
auto elem_ptr1 = mse::make_tuple_element_pointer<double>(tup1_refcptr);
auto val1 = *elem_ptr1;
}
{
/* More commonly, the tuple<> object might be declared as a scope object. */
auto xs_tup1 = mse::make_xscope(mse::mstd::make_tuple(3.8, 'A', mse::mstd::string("Lisa Simpson")));
// which can also be written as
// auto xs_tup1 = mse::TXScopeObj<mse::mstd::tuple<double, char, mse::mstd::string> >(3.8, 'A', mse::mstd::string("Lisa Simpson"));
auto xs_elem_ptr1 = mse::make_xscope_tuple_element_pointer<2>(&xs_tup1);
/* Here we obtain a "regular" scope pointer to the element from the scope "tuple element pointer". */
auto xs_ptr1 = mse::xscope_pointer(xs_elem_ptr1);
auto val1 = *xs_ptr1;
}
}
mstd::tuple<>
can, like any other type, be declared as a scope type (using mse::make_xscope()
/ mse::TXScopeObj<>
). But it does not support using scope types as a contained element type. It is (intended to be) uncommon to need such capability. But the library does provide a version, xscope_tuple<>
, that supports it.
usage example:
#include "msetuple.h"
#include "msemstdstring.h"
int main(int argc, char* argv[]) {
/* Here we're creating a (string) object of scope type. */
auto xs_str1 = mse::make_xscope(mse::mstd::string("abc"));
/* Here we're creating an xscope_tuple<> object that contains a scope pointer to the (scope) string object.
mstd::tuple<>, for example, would not support this. */
auto xstup1 = mse::make_xscope_tuple(&xs_str1);
// which can also be written as
// auto xstup1 = mse::xscope_tuple<mse::TXScopeFixedPointer<mse::mstd::string> >(&xs_str1);
auto val1 = *(std::get<0>(xstup1));
}
The library's safe iterators work just fine with the standard library algorithms. But some of the algorithms, like std::for_each()
, take a function object parameter and pass to the function object a (native) reference to an element. If you want to avoid using native references, the library provides versions of some of these algorithms that pass to the function object a (safe) pointer to the element instead of a native reference to the element.
usage example: (link to interactive version)
#include "msescope.h"
#include "msealgorithm.h"
#include "msemstdarray.h"
#include "msemstdvector.h"
int main(int argc, char* argv[]) {
mse::TXScopeObj<mse::nii_array<int, 3> > xscope_na1 = mse::nii_array<int, 3>{ 1, 2, 3 };
auto xscope_na1_begin_citer = mse::make_xscope_begin_const_iterator(&xscope_na1);
auto xscope_na1_end_citer = mse::make_xscope_end_const_iterator(&xscope_na1);
mse::mstd::array<int, 3> ma1{ 1, 2, 3 };
auto xscope_nv1 = mse::make_xscope(mse::nii_vector<int>{ 1, 2, 3 });
/* Here we declare a vector protected by an "excluse writer" access control wrapper. */
auto xscope_ewnv1 = mse::make_xscope(mse::make_xscope_exclusive_writer(mse::nii_vector<int>{ 1, 2, 3 }));
{
/* mse::for_each_ptr() is like std::for_each() but instead of passing, to the given function, a reference
to each item it passes a (safe) pointer to each item. The actual type of the pointer varies depending on the
type of the given iterators. */
typedef mse::for_each_ptr_type<decltype(ma1.begin())> item_ptr_t;
mse::for_each_ptr(ma1.begin(), ma1.end(), [](item_ptr_t x_ptr) { std::cout << *x_ptr << std::endl; });
mse::for_each_ptr(xscope_na1_begin_citer, xscope_na1_end_citer, [](const auto x_ptr) { std::cout << *x_ptr << std::endl; });
/* A "scope range" version is also available that bypasses the use of iterators. As well as often being more
convenient, it can theoretically be little more performance optimal. */
typedef mse::xscope_range_for_each_ptr_type<decltype(&xscope_na1)> range_item_ptr_t;
mse::xscope_range_for_each_ptr(&xscope_na1, [](range_item_ptr_t x_ptr) { std::cout << *x_ptr << std::endl; });
{
auto xscope_bfnv1 = mse::make_xscope_borrowing_fixed_nii_vector(&xscope_nv1);
mse::xscope_range_for_each_ptr(&xscope_bfnv1, [](const auto x_ptr) { std::cout << *x_ptr << std::endl; });
}
{
/* Note that for performance (and safety) reasons, vectors may be "structure locked" for the duration of the loop.
That is, any attempt to modify the size of the vector during the loop may result in an exception. */
auto xscope_nv1_begin_iter = mse::make_xscope_begin_iterator(&xscope_nv1);
auto xscope_nv1_end_iter = mse::make_xscope_end_iterator(&xscope_nv1);
mse::for_each_ptr(xscope_nv1_begin_iter, xscope_nv1_end_iter, [](const auto x_ptr) { std::cout << *x_ptr << std::endl; });
mse::xscope_range_for_each_ptr(&xscope_nv1, [](const auto x_ptr) { std::cout << *x_ptr << std::endl; });
}
{
/* A vector protected by an "excluse writer" access control wrapper is ensured to remain unmodified while a
corresponding "access controlled const pointer" exists. */
auto xscope_ewnv1_cptr = mse::make_xscope_access_controlled_const_pointer(&xscope_ewnv1);
/* So looping over its elements is safe and efficient. */
mse::xscope_range_for_each_ptr(xscope_ewnv1_cptr, [](const auto x_ptr) { std::cout << *x_ptr << std::endl; });
}
}
}
usage example: (link to interactive version)
#include "msescope.h"
#include "msealgorithm.h"
#include "msemstdarray.h"
int main(int argc, char* argv[]) {
mse::TXScopeObj<mse::nii_array<int, 3> > xscope_na1 = mse::nii_array<int, 3>{ 1, 2, 3 };
auto xscope_na1_begin_citer = mse::make_xscope_begin_const_iterator(&xscope_na1);
auto xscope_na1_end_citer = mse::make_xscope_end_const_iterator(&xscope_na1);
mse::mstd::array<int, 3> ma1{ 1, 2, 3 };
{
typedef mse::find_if_ptr_type<decltype(xscope_na1_begin_citer)> item_ptr_t;
auto found_citer1 = mse::find_if_ptr(xscope_na1_begin_citer, xscope_na1_end_citer, [](item_ptr_t x_ptr) { return 2 == *x_ptr; });
auto res1 = *found_citer1;
auto found_citer3 = mse::find_if_ptr(ma1.cbegin(), ma1.cend(), [](auto x_ptr) { return 2 == *x_ptr; });
/* This version returns an optional scope pointer to the found item rather than an iterator. */
typedef mse::xscope_range_get_ref_if_ptr_type<decltype(&xscope_na1)> range_item_ptr_t;
auto xscope_optional_xscpptr4 = mse::xscope_range_get_ref_if_ptr(&xscope_na1, [](range_item_ptr_t x_ptr) { return 2 == *x_ptr; });
auto res4 = xscope_optional_xscpptr4.value();
/* This version returns a scope pointer to the found item or throws an exception if an appropriate item isn't
found. */
auto xscope_pointer5 = mse::xscope_range_get_ref_to_element_known_to_be_present_ptr(&xscope_na1, [](auto x_ptr) { return 2 == *x_ptr; });
auto res5 = *xscope_pointer5;
}
}
[provisional]
While not encouraging the use of thread_local
variables, the library does provide facilities for their use.
usage example: (link to interactive version)
#include "msescope.h"
#include "msemstdstring.h"
#include "msethreadlocal.h"
#include <iostream>
MSE_DECLARE_THREAD_LOCAL_GLOBAL(mse::mstd::string) tlg_string1 = "some text";
int main(int argc, char* argv[]) {
{
auto tlg_ptr1 = &tlg_string1;
auto xs_tlg_store1 = mse::make_xscope_strong_pointer_store(tlg_ptr1);
auto xs_ptr1 = xs_tlg_store1.xscope_ptr();
*xs_ptr1 += "...";
std::cout << *xs_ptr1 << std::endl;
MSE_DECLARE_THREAD_LOCAL_CONST(mse::mstd::string) tlc_string2 = "abc";
auto tlc_ptr2 = &tlc_string2;
auto xs_tlc_store2 = mse::make_xscope_strong_pointer_store(tlc_ptr2);
auto xs_cptr2 = xs_tlc_store2.xscope_ptr();
std::cout << *xs_cptr2 << std::endl;
class CA {
public:
auto foo1() const {
MSE_DECLARE_THREAD_LOCAL(mse::mstd::string) tl_string = "abc";
/* mse::return_value() just returns its argument and ensures that it's of a (pointer) type that's safe to return. */
return mse::return_value(&tl_string);
}
};
auto tl_ptr3 = CA().foo1();
auto xs_tl_store3 = mse::make_xscope_strong_pointer_store(tl_ptr3);
auto xs_cptr3 = xs_tl_store3.xscope_ptr();
*xs_cptr3 += "def";
std::cout << *xs_cptr3 << std::endl;
}
}
mstd::function<>
is essentially a drop-in replacement for std::function<>
.
usage example: (link to interactive version)
#include "msefunctional.h"
int main(int argc, char* argv[]) {
/* mstd::function<> is essentially just an implementation of std::function<> that supports the library's scope and
data race safety mechanisms. */
mse::mstd::function<int()> function1 = []() { return 3; };
function1 = []() { return 5; };
int res1 = function1();
}
Some function objects, like functors and capture lambdas, can be scope types and would not be supported by mstd::function<>
.
usage example: (link to interactive version)
#include "msefunctional.h"
int main(int argc, char* argv[]) {
/* xscope_function<>s support scope function objects as well. */
mse::xscope_function<int()> xs_function1 = []() { return 5; };
auto xs_int1 = mse::make_xscope(int(5));
auto int1_xsptr = &xs_int1;
struct xscope_my_function_obj_t : public mse::rsv::XScopeTagBase, public mse::rsv::ContainsNonOwningScopeReferenceTagBase {
xscope_my_function_obj_t(decltype(int1_xsptr) int_xsptr) : m_int_xsptr(int_xsptr) {}
decltype(int1_xsptr) m_int_xsptr; /* this is a scope pointer */
int operator()() const { return *m_int_xsptr; }
};
/* xscope_my_function_obj_t is a scope type with a scope (pointer) member. */
xscope_my_function_obj_t xscope_my_function_obj1(int1_xsptr);
mse::xscope_function<int()> xs_function2 = xscope_my_function_obj1;
int res1 = xs_function2();
/* Just as structs with scope pointer/reference members need to be declared as such, lambdas that
capture scope pointer/references must be declared as such. */
auto xs_lambda1 = mse::rsv::make_xscope_reference_or_pointer_capture_lambda([int1_xsptr]() { return *int1_xsptr; });
mse::xscope_function<int()> xs_function3 = xs_lambda1;
}
In situations where a lifetime checker, or equivalent static analyzer, is not available, the degree of memory safety that can be achieved is a function of the degree to which use of C++'s (memory) unsafe elements is avoided.
Note that one of C++'s more subtle unsafe elements is the implicit this
pointer when accessing member variables from member functions. Consider this example:
#include "msescope.h"
#include "msemstdvector.h"
class CI {
public:
template<class safe_vector_pointer_type>
void foo1(safe_vector_pointer_type vec_ptr) {
vec_ptr->clear();
/* These next two lines are equivalent and technically unsafe. */
m_i += 1;
this->m_i += 1;
}
int m_i = 0;
};
int main() {
mse::TXScopeObj<mse::mstd::vector<CI>> vec1;
vec1.resize(1);
auto iter = vec1.begin();
iter->foo1(&vec1);
}
The above example contains unchecked accesses to deallocated memory via an implicit and explicit this
pointer. The this
pointer (implicit or explicit) is a native pointer, and like any other native pointer, is unsafe and can/should be replaced with a safer substitute:
#include "msescope.h"
#include "msemstdvector.h"
class CI {
public:
template<class safe_this_type, class safe_vector_pointer_type>
static void foo2(safe_this_type safe_this, safe_vector_pointer_type vec_ptr) {
vec_ptr->clear();
/* The safe_this pointer will catch the attempted invalid memory access. */
safe_this->m_i += 2;
}
int m_i = 0;
};
int main() {
mse::TXScopeObj<mse::mstd::vector<CI>> vec1;
vec1.resize(1);
auto iter = vec1.begin();
iter->foo2(iter, &vec1);
}
So technically, in situations where a complete lifetime checker is not available, achieving complete memory safety requires passing a safe this
pointer parameter as an argument to every member function that accesses a member variable. (I.e. Make your member functions static
. Or "free".)
But certain member functions can't be made static. Namely constructors, destructors and member operators. If all the constructors and destructors in the program are compiler-generated defaults (or are otherwise known to be "well behaved") then they would all be perfectly safe. The (theoretical) problem is that user-defined constructors or destructors aren't guaranteed to be "well behaved". Specifically, in conventional C++ they could cause objects to be deleted in the middle of their constructor/destructor(/non-static member function) calls. And not just their own object, but, for example, a misbehaving constructor that is invoked by a parent constructor could cause that parent object to be deleted before its construction is completed.
Fortunately, with the SaferCPlusPlus subset it's not quite so bad. Let's consider the possibility of an object's this
pointer being invalidated (i.e. the object being destroyed) while in the middle of executing its constructor. In the SaferCPlusPlus subset there are a limited number of circumstances when a constructor is invoked. One is when calling make_refcounting<>()
. In this case, no direct or indirect reference to the object (other than the this
pointer itself) is available until after the constructor has finished executing, so there's no opportunity for the object to be destroyed (and the this
pointer invalidated) before then. Same goes for registered_new()
and norad_new()
.
A more complicated case is when a container, like say, mstd::vector<>
causes the invocation of child object constructors. Consider this example:
#include "msescope.h"
#include "msemstdvector.h"
#include "msemstdstring.h"
class CMisbehaver1 {
public:
template<typename TVectorPtr>
CMisbehaver1(TVectorPtr vector_ptr) {
vector_ptr->clear(); // potentially dangerous behavior in a constructor
// is the "this" pointer still valid here?
this->m_string1 = "some text";
}
mse::mstd::string m_string1;
};
int main() {
typedef mse::mstd::vector<CMisbehaver1> misb1_vec_t;
typedef mse::mstd::vector<misb1_vec_t> misb1_vec_vec_t;
mse::TXScopeObj<misb1_vec_vec_t> xs_vv1;
xs_vv1.resize(1);
xs_vv1[0].emplace_back(&xs_vv1);
}
It's a little bit tricky, but the emplace_back()
call on the last line of the main()
function is going to cause the constructor of CMisbehaver1
to be invoked. But in its first line, the constructor seemingly causes its parent container to be deleted, making its own this
pointer invalid.
But because the parent container (i.e. the thing invoking the constructor) is an element of the library, it has run-time checks to ensure that its destructor is not executed while in the middle of one of its (non-static) member functions, like emplace_back()
(or a constructor or the destructor). So in this case, rather than execute its destructor in the middle of the emplace_back()
call, the program will terminate.
Because constructors and destructors of dynamically (i.e. heap) allocated objects can only be invoked via elements of the library, those elements can (and do) ensure that the this
pointer remains pointing to validly allocated memory for the whole constructor/destructor call in all cases.
The remaining case is when a local variable is declared (on the stack). In this case the this
pointer is intrinsically guaranteed to point to a validly allocated object, not just for the duration of the constructor, but indeed for the duration of the scope. (Same goes for variables with static or thread_local storage if you're using those.)
Unfortunately, C++ does permit you to reference the object before the completion of its construction. For example, in this declaration
{
std::vector<size_t> v1 { v1.at(3) };
}
the local variable v1
is used after it has been allocated, but before its constructor has been executed. It's also an issue inside constructors as in this example:
{
struct W {
W(int i) : j(i) {}
int j;
};
struct Y {
W w1;
W w2;
Y() : w1(w2.j), w2(w1.j) {}
};
}
This kind of bug is probably quite rare and would probably need to be addressed by a static tool, like the lifetime checker, or frankly, the compiler itself.
While we can generally ensure that the this
pointer remains pointing to a validly allocated object in constructors/destructors, we cannot do the same for native reference parameters. This means that technically, without a lifetime checker, the safety of (user-defined) constructors which take native reference parameters, like copy and move constructors, cannot be ensured.
Also note that explicitly calling std::move()
(the one in the <utility>
library, not the one in the <algorithm>
library) is potentially unsafe, particularly if applied to certain scope objects. Basically, just let the compiler decide when a reference is an rvalue reference.
And also, SaferCPlusPlus does not yet provide safer substitutes for all of the standard library containers, just the ones responsible for the most problems (vector and array). So be careful with your maps, sets, etc. In many cases lists can be replaced with ivector<>
s that support list-style iterators, often with a performance benefit.
If you have questions or comments you can post in the discussions section.