In addition to checking for memory consistency, our static checker can also check for potential violations of determinism. Mode of check (memory safety, determinism, or both) is specified in the command line. TODO: non-memory UB checks, cross-platform checks.
- all files included via
#include <>
are considered library files. Out of functions declared in library files, ONLY functions which protoypes are explicitly listed in a special file safe_library.h, are allowed. safe_library.h MUST include ALL safe functions from C library. TODO: safe classes (with ALL safe functions of the class also explicitly listed) - all files included via
#include ""
are considered project files. For such "project files":- by default, all the classes/functions are considered 'safe', and are analysed for safety
- if a class/function is labeled as [[nodecpp::memory_unsafe]] , or belongs to a namespace labeled as [[nodecpp::memory_unsafe]], memory safety checks are skipped for this class/function
- if a class/function is labeled as [[nodecpp::non_deterministic]] , or belongs to a namespace labeled as [[nodecpp::non_deterministic]], determinism checks are skipped for this class/function
- NB: first, have to double-check that attributes on namespaces are really allowed by all three major compilers
Legend for TEST CASES:
-
i
- variable of integral type -
p
- variable of raw pointer type (T*
) -
r
- variable of reference type (T&
) -
np
- variable ofnullable_ptr<T>
type -
sp
- variable ofsoft_ptr<T>
type -
op
- variable ofowning_ptr<T>
type -
fp()
- function taking raw pointer type (T*
) -
fop()
- function takingowning_ptr<T>
-
NSTR
- naked_struct type -
nstr
- variable of naked_struct type -
af()
- asynchronous function returningnodecpp::awaitable<>
-
naf()
- function with[[nodecpp::no_await]]
at its declaration -
IMPORTANT: whenever we're speaking of
safe_ptr<T>
ornullable_ptr<T>
, thennot_null<safe_ptr<T>>
andnot_null<nullable_ptr<T>>
are ALWAYS implied (and SHOULD be included into relevant test cases) -
IMPORTANT: whenever we're speaking of
owning_ptr<T>
,safe_ptr<T>
ornullable_ptr<T>
, then their short aliases (optr<T>
,sptr<T>
, andnptr<T>
) are ALWAYS implied (and SHOULD be included into relevant test cases)
Consistency checks always apply (regardless of the command line, and any attributes)
- [Rule C1] ONLY those [[nodecpp::]] attributes which are specified by this document, are allowed. Using of unspecified [[nodecpp::]] attribute is an error.
- TEST CASES/PROHIBIT:
[[nodecpp::abracadabra]]
- TEST CASES/PROHIBIT:
- [Rule C2] use of [[nodecpp::]] attributes is allowed ONLY in those places specified by this document. Using of [[nodecpp::]] attributes in a wrong place is an error.
- TEST CASES/PROHIBIT:
[[nodecpp::naked_struct]] void f();
- TEST CASES/PROHIBIT:
- [Rule C3] if some namespace has [[nodecpp::memory_unsafe]] or [[nodecpp::non_deterministic]] attribute, these attributes MUST be the same for ALL the instances of the same namespace.
- TEST CASES/PROHIBIT:
namespace [[nodecpp:memory_unsafe]] abc {}; namespace abc {};
- TEST CASES/PROHIBIT:
- [Rule S1] Not allowing to create pointers except in an allowed manner. Any (sub)expression which has a type of T* (or T&) is prohibited unless it is one of the following:
- (sub)expressino is an assignment where the right side of (sub)expression is already a pointer/reference to T (or a child class of T).
- or (sub)expression is a
dynamic_cast<>
- NB: MOST of C-style casts, reinterpret_casts, and static_casts (formally - all such casts between different types) MUST be prohibited under generic [Rule S1], but SHOULD be reported separately under [Rule S1.1]
- or (sub)expression is a function call
- or (sub)expression is this
- or (sub)expression is dereferencing of a
nullable_ptr<T>
,soft_ptr<T>
, orowning_ptr<T>
- or (sub)expression is a variable or function paramenter of type
T*
- or (sub)expression is address of object
&i
- NB: Convertion of literals
nullptr
and0
toT*
are prohibited and SHOULD be diagnosed as a separate [Rule S1.2] - NB: dereferencing
*nullptr
is not allowed by languaje asnullptr
is of typenullptr_t
and doesn't have overloadoperator*
. And implicit conversion fromnullptr_t
to anyT*
is prohibited by this rule. - TEST CASES/PROHIBIT:
(int*)p
,p^p2
,p+i
,p[i]
(syntactic sugar for *(p+a) which is prohibited),p1=p2=p+i
,*nullptr = 123
- TEST CASES/ALLOW:
dynamic_cast<X*>(p)
,p=p2
,p=np
,p=sp
,p=op
,fp(p)
,fp(np)
,fp(sp)
,fp(op)
,&i
,*np
,*sp
,*op
,*p
- [Rule S1.1] C-style casts, reinterpret_casts, and static_casts are prohibited. See NB in [Rule S1]. NB: if [Rule S1] is already enforced, this rule (which effectively prohibits even casts from X* to X*) is NOT necessary to ensure safety, but significantly simplifies explaining and diagnostics.
- [Rule S1.1.1] special casts, in particular
soft_ptr_static_cast<>
. are also prohibited in safe code - TEST CASES/PROHIBIT:
(int*)p
,static_cast<int*>(p)
,reinterpret_cast<int*>(p)
,soft_ptr_static_cast<X*>(p)
- [Rule S1.1.1] special casts, in particular
- [Rule S1.2] Separate diagnostics for converting
nullptr
or0
to any raw pointer type (see above)- TEST CASES/PROHIBIT:
int* x = 0;
,void fp(nullptr);
- TEST CASES/PROHIBIT:
- [Rule S1.3] raw pointer variables (of type T*) and raw pointer function parameters MUST be initialized to non-null value. NB: [Rule S1] already enforces that null values are not valid
T*
expressions.- NB: raw references are ok (we're ensuring that they're not null in the first place)
- TEST CASES/PROHIBIT:
int* x;
- TEST CASES/ALLOW:
int* x = &i;
- [Rule S1.4]. Unions with any raw/naked/soft/owning pointers (including any classes containing any such pointers) are prohibited
- TEST CASES/PROHIBIT:
union { nullable_ptr<X> x; int y; }
- TEST CASES/ALLOW:
union { int x; long y; }
- TEST CASES/PROHIBIT:
- [Rule S2] const-ness is enforced
- [Rule S2.1] const_cast is prohibited
- [Rule S2.2] mutable members are prohibited
- TEST CASE/PROHIBIT:
const_cast<X*>
,mutable int x;
- [Rule S3] non-constant global variables, static variables, and thread_local variables are prohibited. NB: while prohibiting thread_local is not 100% required to ensure safety, it is still prohibited at least for now.
- const statics/globals are ok (because of [Rule S2])
- static/globals of empty types are also ok
- NB: this rule effectively ensures that all our functions are no-side-effect ones (at least unless explicitly marked so)
- TEST CASES/PROHIBIT:
int x;
at global scope,thread_local int x;
,static int x;
within function,static int x;
within the class - TEST CASES/ALLOW:
static void f();
within class, free-standingstatic void f();
,static constexpr int x;
,static const int x;
(both in class and globally)
- [Rule S4] new operator (including placement new) is prohibited (developers should use make_owning<> instead); delete operator is also prohibited
- TEST CASES/PROHIBIT:
new X()
,new int
- TEST CASES/ALLOW:
make_owning<X>()
,make_owning<int>()
- [Rule S4.1] result of
make_owning<>()
call MUST be assigned to anowning_ptr<T>
(or passed to a function takingowning_ptr<T>
)- TEST CASES/PROHIBIT:
make_owning<X>();
,soft_ptr<X> = make_owning<X>();
- TEST CASES/ALLOW:
auto x = make_owning<X>();
,owning_ptr<X> x = make_owning<X>();
,fop(make_owning<X>());
- TEST CASES/PROHIBIT:
- TEST CASES/PROHIBIT:
- [Rule S5] scope of raw pointer (T*) cannot expand [[**TODO/v0.5: CHANGE Rule S5 completely to rely on Herb Sutter's D1179: https://github.com/isocpp/CppCoreGuidelines/blob/master/docs/Lifetime.pdf; NB: SOME of the rules below may still be needed on top of D1179 **]]
- [Rule S5.1] each
nullable_ptr<>
, reference (T&) or naked_struct is assigned a scope. If there is an assignment of an object of 'smaller' scope to an object of 'smaller' one, it is a violation of this rule. Returning of pointer to a local variable is also a violation of this rule.- for pointers/references originating from
owning_ptr<>
orsafe_ptr<>
, scope is always "infinity" - for pointers/references originating from on-stack objects, scopes are nested according to lifetimes of respective objects
- scopes cannot overlap, so operation "scope is larger than another scope" is clearly defined
- TEST CASES/PROHIBIT:
int& ff() { int i = 0; return i; }
- for pointers/references originating from
- [Rule S5.2] If we cannot find a scope of pointer/reference returned by a function, looking only at its signature - it is an error.
- if any function takes ONLY ONE pointer (this includes
safe_ptr<>
andowning_ptr<>
, ANDthis
pointer if applicable), and returns more or one pointers/references, we SHOULD deduce that all returned pointers are of the same scope as the pointer passed to it- similar logic applies if the function takes ONLY ONE non-const pointer AND returns non-const pointer
- NB: this stands because of prohibition on non-const globals
- NB: it allows getters returning references
- in the future, we MAY add mark-up to say which of the input pointers the returned one refers to.
- in the future, we MAY add type-based analysis here
- TEST CASES/PROHIBIT:
X* x = ff(x1,x2);
where x1 and x2 have different scope - TEST CASES/ALLOW:
X* x = ff(x1);
,X* x = ff(x1,x2);
where x1 and x2 have the same scope
- if any function takes ONLY ONE pointer (this includes
- [Rule S5.3] double raw/nullable_ptrs where the outer pointer/ref is non-const, are prohibited, BOTH declared AND appearing implicitly within expressions. This also includes reference to a pointer (or to a
nullable_ptr<>
).- NB: it also applies to multi-level pointers: to be valid, ALL outer pointers/references except for last one, MUST be const
- NB: passing nullable_ptrs by value is ok. Even if several nullable_ptrs are passed to a function, there is still no way to mess their scopes up as long as there are no double pointers (there is no way to assign pointer to something with a larger scope).
- NB: const reference to a pointer (and const pointer to pointer) is ok because of [Rule S2]
- TEST CASES/PROHIBIT:
int** x;
,&p
,int *& x = p;
,void ff(nullable_ptr<int>& x)
- TEST CASES/ALLOW:
void ff(nullable_ptr<int> np);
,void ff(const nullable_ptr<int>& np);
,const int *& x = p;
- [Rule S5.4] by default, no struct/class may contain nullable_ptrs, raw pointers or references (neither struct/class can contain a naked_struct, neither even a safe/owning pointer to a naked_struct)
- if a struct/class is marked up as
[[nodespp::naked_struct]]
, it may contain nullable_ptrs (but not raw pointers), and other naked_structs by value; it still MUST NOT contain raw/naked/safe/owning pointers to a naked_struct - allocating naked_struct on heap is prohibited
- NB: having raw pointers (T*) is prohibited by [Rule S1.3]
- TEST CASES/PROHIBIT:
struct X { nullable_ptr<Y> y; };
,[[nodecpp:naked_struct]] struct X { soft_ptr<NSTR> y; };
,make_owning<NSTR>()
- TEST CASES/ALLOW:
struct X { soft_ptr<Y> y; };
,[[nodecpp:naked_struct]] struct X { nullable_ptr<Y> y; };
- if a struct/class is marked up as
- [Rule S5.5] Creating a non-const pointer/reference to a naked_struct (or passing naked_struct by reference) is prohibited (it would violate [Rule S5.1]).
- This implies prohibition on member functions of naked_struct (as
this
parameter is always a pointer), except forconst
ones - NB: passing naked_struct by value is ok - and should be treated as passing several nullable_ptrs (with their respective scopes)
- TEST CASES/PROHIBIT:
nullable_ptr<NSTR>
,void ff(NSTR&)
- TEST CASES/ALLOW:
const_nullable_ptr<NSTR>
,void ff(const NSTR&)
- This implies prohibition on member functions of naked_struct (as
- [Rule S5.6] Lambda is considered as an implicit naked_struct, captures may include local var references, nullable_ptrs and raw pointer
this
.- TEST CASES/ALLOW:
sort()
passing lamda with local vars captured by reference
- TEST CASES/ALLOW:
- [Rule S5.7] There is special parameter mark-up
[[nodecpp::may_extend_to_this]]
that may be applied to method parameter of typestd::function
or lambda on library code api (or[[nodecpp::memory_unsafe]]
marked code), AND means that the scope of marked-up parameter MAY be extended tothis
of called instance. If such a parameter is specified, then the scope of the captures MUST be equal-or-larger-than the scope of the called instance.- When applied to parameter of type
std::function
, it means it can only be initialized with a lambda that verifies the mentioned retrictions. - In the future, we MAY introduce other similar mark-up (
[[nodecpp::may_extend_to_a]]
?) - TEST CASES/PROHIBIT:
this->on()
(which is marked as[[nodecpp::may_extend_to_this]]
) passing lambda with local vars passed by reference - TEST CASES/ALLOW:
this->on()
passing lambda withthis->members
captured by reference
- When applied to parameter of type
- [Rule S5.8] nullable_ptr<>s and references MUST NOT survive over co_await
- TEST CASES/PROHIBIT:
co_await some_function(); auto x = *np;
,co_await some_function(); auto x = r;
- TEST CASES/ALLOW:
co_await some_function(); auto x = *sp;
- TEST CASES/PROHIBIT:
- [Rule S5.1] each
- [Rule S6] Prohibit inherently unsafe things
- [Rule S6.1] prohibit asm, both MSVC and GCC style.
- [Rule S7] Prohibit unsupported-yet things
- [Rule S7.1] prohibit function pointers (in the future, will be supported via something like naked_func_ptr<> checking for nullptr)
- [Rule S8] Prohibit functions except for those safe ones.
- Out of functions declared in "library files" (those included via
#include <>
), ONLY functions which protoypes are explicitly listed in a special safe-library header file (specified in a command line to the compiler such as safe-library=safe_library.h), are allowed. safe-library header MUST include ALL safe functions from C library.- safe-library header should allow specifying safe classes (with ALL safe member functions within the class also explicitly listed; whatever-functions-are-unlisted, are deemed unsafe)
- safe-library header should support
#include
within; it should support BOTH (a) including other safe-library headers, and (b) including real library files (such as our own soft_ptr.h). TBD: for (b), there should be a way to restrict#include
within safe-library header to only direct #include (without nested #includes). - template types and functions listed as safe are so independant of its parameters. Control of allowed template parameters must be done at a different level (i.e. using enable_if<> or concepts).
- functions and methods listed as safe are so independant of its arguments or overloads.
- TODO: support for
strlen()
etc. - TEST CASES/PROHIBIT:
memset(p,1,1)
- TEST CASES/ALLOW:
soft_ptr<X*> px;
- All the functions from "project files" (those included via
#include ""
) are ok (even if they're labeled with [[nodecpp::memory_unsafe]]). It is a responsibility of the developers/architects to ensure that [[nodecpp::memory_unsafe]] functions are actually safe.
- Out of functions declared in "library files" (those included via
- [Rule S9] nodecpp::awaitable<>/co_await consistency (necessary to prevent leaks). Corroutines must return
nodecpp::awaitable<>
only.- [Rule S9.1] For any function f, ALL return values of ALL functions/coroutines returning nodecpp::awaitable<> and NOT having
[[nodecpp::no_await]]
at their declaration, MUST be fed toco_await
operator within the same function f, and without any conversions. In addition, such return values MUST NOT be copied, nor passsed to other functions (except for special functionwait_for_all()
).- TEST CASES/PROHIBIT:
af();
,{ auto x = af(); }
,int x = af();
,auto x = af(); anothre_f(x); /* where another_f() takes nodecpp::awaitable<> */
,auto x = af(); auto y = x;
- TEST CASES/ALLOW:
co_await af();
,int x = co_await af();
,auto x = af(); auto y = af2(); co_await x; co_await y;
,nodecpp::awaitable<int> x = af(); co_await x;
,co_await wait_for_all(af(), af2())
- TEST CASES/PROHIBIT:
- [Rule S9.2] All calls to library functions returning nodecpp::awaitable<> that DO have attribute [[nodecpp::no_await]] at their declaration can be optionally fed to co_await operator.
- TEST CASES/ALLOW:
co_await naf();
,naf();
- TEST CASES/ALLOW:
- [Rule S9.3] Prohibit using
co_await
inside subexpression or at other places than root statement expression or root variable initializer.- TEST CASES/PROHIBIT:
int j = (co_await af()) + 1;
,if(co_await af()) { ... };
- TEST CASES/ALLOW:
int i = co_await af();
,co_await af();
- TEST CASES/PROHIBIT:
- [Rule S9.1] For any function f, ALL return values of ALL functions/coroutines returning nodecpp::awaitable<> and NOT having
- [Rule S10] Prohibit using unsafe collections and iterators
-
Collections, such as
std::vector<...>
,std::string
, etc, and iterators internally use unsafe memory management, and, therefore, must be prohibited. Safe collections (such asnodecpp::vector<...>
should be used instead.- TEST CASES/PROHIBIT:
std::vector<...> v
,std::string s
, etc; - TEST CASES/ALLOW:
nodecpp::vector<...> v
,nodecpp::string s
, etc;
- TEST CASES/PROHIBIT:
-
[Rule S10.1] support StringLiteral class - it can be created ONLY from string literal, OR from another string literal.
- TEST CASES/PROHIBIT:
const char* s = "abc"; StringLiteral x = s;
,void fsl(StringLiteral x) {} ... const char* s = "abc"; fsl(s);
- TEST CASES/ALLOW:
StringLiteral x = "abc";
,void fsl(StringLiteral x) {} ... fsl("abc");
,void fsl(StringLiteral x) {} ... StringLiteral x = "abc"; fsl(x);
- TEST CASES/PROHIBIT:
-
[Rule S10.2] deep_const types, are those that given a const intance, they become inmutable in deep (in the sence of shallow/deep copy). In C++ doing
const T t;
works as a shallow const, if T is a class having a member that is a pointer, then only the pointer itself is const but not the pointed object. The same shallow const semantics are followed byowning_ptr
,safe_ptr
, etc. deep_const types are primitives,owning_ptr<const T>
, and classes with special mark-up[[nodecpp::deep_const]]
and whose members and bases are all deep_const.- [Rule S10.2.1] Special mark-up
[[nodecpp::deep_const_when_params]]
is allowed only at library code, to mark a template as deep_const if and only if all its template parameters are deep_const. - TEST CASES/PROHIBIT:
class [[nodecpp::deep_const]] C { owning_ptr<int> oi; };
- TEST CASES/ALLOW:
class [[nodecpp::deep_const]] MyHash { int i = 0; owning_ptr<const int> oi; };
- [Rule S10.2.1] Special mark-up
-
[Rule S10.3] no_side_effect functions, are those that don't do any side effect. Some local side effects are already checked by previous rules (i.e. no global variables), but most calls to the OS are also side effects (i.e. open files, read/write the network, read/write to the console, get current date/time). no_side_effect functions and methods have special mark-up
[[nodecpp::no_side_effect]]
and their body is checked so that only calls to other no_side_effect functions are allowed.- [Rule S10.3.1] Special no_side_effect exception is granted to
TRACE
family of functions. - [Rule S10.3.2] Special mark-up
[[nodecpp::no_side_effect_when_const]]
is allowed only at library code, to indicate that allconst
methods of a class, are also no_side_effect. - TEST CASES/PROHIBIT:
[[nodecpp::no_side_effect]] void f() {fmt::print("hello!");}
- TEST CASES/ALLOW:
[[nodecpp::no_side_effect]] void f() { otherNoSideEffect(); TRACE("Done!");}
- [Rule S10.3.1] Special no_side_effect exception is granted to
-
[Rule S10.4] hashed contaniner
unordered_map<Key, Value, Hash, KeyEqual>
must have aKey
type parameter that is compatible with deep_const requirements.Hash
andKeyEqual
type parameters must be compatible also with deep_const requirements and both have a definition ofoperator()
comptatible with no_side_effect requirements.- TEST CASES/PROHIBIT:
unordered_map<soft_ptr<int>, int> m;
- TEST CASES/ALLOW:
unordered_map<string, int> m;
,class [[nodecpp::deep_const]] MyHash { [[nodecpp::no_side_effect]] size_t operator()(const Key& k) {...}}; unordered_map<Key, string, MyHash> m;
- TEST CASES/PROHIBIT:
-
- Not allowing to convert pointers into non-pointers
- [Rule D1] any (sub)expression which takes an argument of raw pointer type X* AND returns non-pointer type is prohibited, unless it is one of the following:
- function call taking X* as parameter
- convertion to
bool
- TEST CASES/PROHIBIT:
(int)p
,static_cast<int>(p)
- TEST CASES/ALLOW:
fp(p)
,if(!p)
- [Rule D1] any (sub)expression which takes an argument of raw pointer type X* AND returns non-pointer type is prohibited, unless it is one of the following:
- [Rule D2] Prohibiting uinitialized variables, including partially unitialized arrays
- TEST CASES/PROHIBIT:
int x;
(in function),int a[3] = {1,2};
(in function),X x;
(in function, if class X has non-constructed members AND has no constructor),int x1 : 8;
(in function, this is an uninitialized bit field) - TEST CASES/ALLOW:
int x = 0;
(in function),X x;
(in function, provided that class X has default constructor),int a[3] = {1,2,3};
(in function),int x1 : 8 = 42;
(in function, this is an initialized bit field) - [Rule D2.1] Prohibiting uninitialized class members; at the moment, ONLY C++11 initializers next to data member are recognized. TODO: allow scenarios when ALL the constructors initialize data member in question.
- TEST CASES/PROHIBIT:
int x;
(as data member),int a[3] = {1,2};
(as data member),X x;
(as data member, if class X has has non-constructed members AND has no constructor),int x1 : 8;
(as data member, this is an uninitialized bit field) - TEST CASES/ALLOW:
int x = 0;
(as data member),X x;
(as data member, provided that class X has default constructor),int a[3] = {1,2,3};
(as data member),int x1 : 8 = 42;
(as data member, this is an initialized bit field)
- TEST CASES/PROHIBIT:
- TEST CASES/PROHIBIT:
- [Rule M1] Only nodecpp::error can be thrown/caught (NO derivatives)
- [Rule M1.1] Only nodecpp::error can be thrown
- TEST CASES/ALLOW:
throw nodecpp::error(nodecpp::errc::bad_alloc);
,throw nodecpp::error::bad_alloc;
- TEST CASES/PROHIBIT:
throw 0;
,throw std::exception
,throw std::error;
- TEST CASES/ALLOW:
- [Rule M1.2] Only nodecpp::exception can be caught (and ONLY by reference)
- TEST CASES/ALLOW:
catch(nodecpp::error& x)
- TEST CASES/PROHIBIT:
catch(int)
,catch(std::exception)
,catch(std::error)
,catch(nodecpp::error)
- TEST CASES/ALLOW:
- [Rule M1.1] Only nodecpp::error can be thrown
- [Rule M2] Ensuring code consistency regardless of tracing/assertion levels
- [Rule M2.1] Within NODETRACE* macros, there can be ONLY const expressions
- TEST CASES/ALLOW:
NODETRACE3("{}",a==b);
- TEST CASES/PROHIBIT:
NODETRACE3("{}",a=b);
- TEST CASES/ALLOW:
- [Rule M2.2] Within NODEASSERT* macros, there can be ONLY const expressions
- TEST CASES/ALLOW:
NODEASSERT2(a==b);
- TEST CASES/PROHIBIT:
NODEASSERT2(a=b);
- TEST CASES/ALLOW:
- [Rule M2.1] Within NODETRACE* macros, there can be ONLY const expressions