Comparison of Ada
2012 with the PRQA High Integrity C++ Coding Standard
This document compares the safety critical subset of C++
defined in the High Integrity C++ Coding Standard Version 4.0 (HIC) produced by
PRQA and available at
www.codingstandard.com.
The comparison will follow the flow of the HIC.
1.
General
1.1.
Implementation
Compliance
1.1.1.
Ensure that code complies with the 2011 ISO C++
Language Standard
Compilers often provide features beyond those defined in the
Standard, and unrestricted usage of such features will likely hamper code
portability. To this end code should be routinely parsed with a separate
compiler or code analysis tool apart from the compiler used for production
purposes.
Corresponding Ada rule:
Compilers often provide features beyond those defined in the
Standard, and unrestricted usage of such features will likely hamper code
portability. To this end Ada compilers provide the Ada profile
No_Implementation_Extensions, which enforces the restriction to Standard Ada in
a standard way. The only thing not caught by this profile is the use of
implementation-defined library packages, such as the GNAT packages for the GNAT
compiler. Those packages can be caught by adding the No_Dependence profile.
1.2.
Redundancy
1.2.1.
Ensure that all statements are reachable
For the purposes of this rule, missing else and default clauses
are considered also.
If a statement cannot be reached for any combination of
function inputs (e.g. function arguments, global variables, volatile objects),
it can be eliminated.
Corresponding Ada rule:
While Ada contains the same rule, Ada compilers perform
reachability analysis and report compilation errors when code is not reachable.
Ada compilers identify missing others clauses in case statements when all possible values are not
explicitly included in a case statement.
1.2.2.
Ensure that no expression or sub-expression is
redundant
An expression statement with no side effects can be removed
or replaced with a null statement without effecting behavior of the program.
Similarly, it is sometimes possible to simplify an
expression by removing operands that do not change the resulting value of the
expression, for example by multiplying by 1 or 0.
Redundant code causes unnecessary maintenance overhead and
may be a symptom of a poor design.
Corresponding Ada rule:
The corresponding Ada rule would be identical. This rule
applies to any programming language.
1.3.
Deprecated Features
1.3.1.
Do not use the increment operator (++) on a
variable of type bool
Incrementing an object of type bool results in its value
being set to true. This feature was
deprecated in the 1998 C++ Language Standard and thus may be withdrawn in a
later version.
Prefer to use an explicit assignment to true.
Corresponding Ada rule:
The Ada Boolean
type is an enumerated type with the values of (False, True). Ada does not allow
arithmetic operations on enumerated types. Furthermore, Ada does not provide an
increment operator.
1.3.2.
Do not use the register keyword
Most compilers ignore the register keyword, and perform their own register assignments.
Moreover, this feature was deprecated in the 2011 Language Standard and thus
may be removed in a later version.
Corresponding Ada rule:
Ada does not provide any equivalent to the register keyword in C++.
1.3.3.
Do not use the C Standard Library .h headers
The C standard library headers are included in C++ for
compatibility. However, their inclusion was deprecated in the 1998 C++ Language
Standard and thus may be withdrawn in a later version.
Corresponding Ada rule:
Ada does not provide a preprocessor and does not interface
with C or C++ programs at the source code level.
1.3.4.
Do not use deprecated STL features
The following STL features were deprecated in the 2011 C++
Language Standard and thus may be withdrawn in a later version:
·
Std::auto_ptr
·
Std::bind1st
·
Std::bind2nd
·
Std::ptr_mem_fun_ref
·
Std::unary_function
·
Std::binary_function
Of particular note is std::auto_ptr as it has been suggested
that a search and replace of this type to std::unique_ptr may uncover latent
bugs to the incorrect use of std::auto_ptr.
Corresponding Ada rule:
Avoid the use of the language features listed in Appendix J
of the Ada 2012 Language Reference Manual. Use Ada 2012 aspect specifications
instead. Avoiding Annex J features can be enforced through the use of the
profile No_Obsolescent_Features.
1.3.5.
Do not use throw
exception specifications
Specifying an exception specification using throw( type-id-listopt ) has
been deprecated in the 2011 C++ Language Standard and thus may be removed in a
future version.
The new syntax is to use noexcept
or noexcept( expr).
Corresponding Ada rule:
Ada does not have an equivalent to either noexcept or
noexcept(expr). In C++ the noexcept keyword verifies that no exception is
thrown in a specified function. The noexcept(expr) declares that a function may
throw exceptions for some types but not other types. Both forms of noexcept
return a Boolean result.
The SPARK subset of Ada prohibits exceptions and is able to
prove that a subprogram will not raise an exception. Exceptions are prohibited
because of the difficulty in formally proving the correctness of a program
which raises exceptions.
2.
Lexical Conventions
2.1.
Character
sets
2.1.1.
Do not use tab characters in source files
Tab width is not consistent across all editors or tools.
Code indentation can be especially confusing when tabs and spaces are used
interchangeably. This may easily happen where code is maintained in different
editors.
In string and character literals \t should be used in
preference to a direct tab character.
Corresponding Ada rule:
Use of tabs in Ada source code is subject to the same editor
and tool variations as use of a tab in C++ source code.
Insertion of a tab character in a string or character
literal or variable should be use Ada.Characters.Latin_1.HT.
2.2.
Trigraph
sequences
2.2.1.
Do not use digraphs or trigraphs
Trigraphs are special three character sequences, beginning
with two question marks and followed by one other character. They are
translated into specific single characters, e.g. \ or ^. Digraphs are special
two character sequences that are similarly translated.
Corresponding Ada rule:
Ada does not provide digraph or trigraph sequences.
2.3.
Comments
2.3.1.
Do not use the C comment delimiters /* … */
The scope of C++ comments is clearer; until the end of a
logical source line (taking line splicing into account). Errors can result from
the use of C comments.
Corresponding Ada rule:
Ada does not allow the use of C comments.
2.3.2.
Do not comment out code
Source code should always be under version control.
Therefore keeping old code in comments is unnecessary. It can make
browsing, searching, and refactoring the
source code more difficult.
Corresponding Ada rule:
The same rule should apply to any programming language.
2.4.
Identifiers
2.4.1.
Ensure that each identifier is distinct from any
other visible identifier
Similarity of identifiers impairs readability, can cause
confusion and may lead to mistakes.
Names should not differ only in case (foo/Foo) or in the use
of underscores(foobar/foo_bar). Additionally, certain combinations of characters
look very similar.
Note: This rule
does not require that an identifier cannot be reused.
Corresponding Ada rule:
Ada identifiers are case-insensitive, thus (foo/Foo) are
treated as the same identifier.
Ada compilers identify duplicate definition of identifier
within the same scope.
2.5.
Literals
2.5.1.
Do not concatenate strings with different
encoding prefixes
The C++ Standard permits string literals with the following
encoding prefixes: u, U, u8, L. A program that concatenates a pair of string
literals with u8 and L prefixes is ill-formed.
The result of the remaining prefix combinations are all
implementation defined. For this reason encoding sequences should not be mixed.
Corresponding Ada rule:
Ada provides multiple character encoding sequences. Each
sequence is a distinct type and therefore each string type is unique. Ada does
not allow mixing data types within a single array.
2.5.2.
Do not use octal constants (other than zero)
Octal constants are specified with a leading digit 0;
therefore, literal 0 is technically an octal constant.
Do not use any other octal literals, as based on
unfamiliarity, this could be confusing and error prone.
Corresponding Ada rule:
Ada allows the use of any number base from 2 through 16. A
literal of a particular number base is expressed as base#digits# (e.g 2#111# is
the binary equivalent of 7 base 10) except for base 10, which is implied by
simply expressing the digits. Therefore, 11 and 011 are interpreted as 11 base
10.
2.5.3.
Use nullptr
for the null pointer constant
The 2011 C++ Language Standard introduced the nullptr keyword do denote a null pointer
constant.
The NULL macro and
constant expression 0 can be used in
both pointer contexts and integral contexts. nullptr, however, is only valid for use in pointer contexts and so
cannot be unexpectedly used as an integral value.
Corresponding Ada rule:
Ada access types are functionally equivalent to pointers but
are not pointers. Ada access types can be set to null, which is not a numerical value and cannot be confused as an
integral value. The address of a variable can be set using with address aspect
of a variable declaration:
Foo : Integer with Address
=> 16#3ff#;
3.
Basic Concepts
3.1.
Scope
3.1.1.
Do not hide declarations
Reusing the same identifier for different declarations is
confusing and difficult to maintain. If the hiding declaration is later
removed, or the identifier is renamed, a compilation error may not be
generated, as the declaration that was previously hidden will now be found.
While hidden namespace scope identifiers can still be
accessed with a fully qualified name, hidden block scope identifiers will not
be accessible.
In C++ it is possible for the same identifier to refer to
both a type and an object or a function. In this case the object or function
will hide the type.
Corresponding Ada rule:
Use of an identifier in a local scope that matches a visible
identifier in a dependent package will result in a compiler error. Use of an
identifier in an inner block will hide the use of the same identifier an
enclosing block.
Ada does not allow an identifier to refer to both a type and
an object or a function within the same or enclosing scopes.
3.2.
Program
and linkage
3.2.1.
Do not declare functions at block scope
A declaration for a function should not be common to its
definition, any redeclarations, and any calls to it.
To ensure that the same type is used in all declarations,
functions should always be declared at namespace scope(See Rules 7.4.3: “Ensure that an object or a function used
from multiple translation units is declared in a single header file” and
7.4.1: “Ensure that any objects,
functions, or types to be used from a single translation unit are defined in an
unnamed namespace in the main source file”).
Corresponding Ada rule:
Ada provides the package construct to enforce name space and
encapsulation. Any type, object, subprogram, or task used by more than one
compilation unit must be declared within a package. Ada provides no other means
to share declarations between compilation units.
Ada does allow a single subprogram or task to define types,
objects, subprograms or tasks to be used only within the defining subprogram or
task.
3.3.
Storage
duration
3.3.1.
Do not use variables with static storage
duration
Variables with linkage (and hence static storage duration),
commonly referred to as global variables, can be accessed and modified from
anywhere in the translation unit if they have internal linkage, and anywhere in
the program if they have external linkage. This can lead to uncontrollable
relationships between functions and modules.
Additionally, certain aspects of the order of initialization
of global variables are unspecified and implementation defined in the C++
Language Standard. This can lead to unpredictable results for global variables
that are initialized at run-time (dynamic initialization).
This rule does not prohibit the use of a const object with
linkage, so long as:
·
It is initialized through static initialization
·
The object is no ODR used
The order of initialization of block scope objects with
static storage is well defined. However, the lifetime of such an object ends at
program termination, which may be incompatible with future uses of the code,
e.g. as a shared library. It is preferable to use objects with dynamic storage
duration to represent program state, allocated from the heap or a memory pool.
Corresponding Ada rule:
The only way to share variables across compilation units in
Ada is by declaring those variables within a package. The order of
initialization of constants is controlled by package elaboration rules. While
Ada does provide default elaboration rules, it also provides elaboration
pragmas allowing the programmer to specify elaboration order for packages.
3.4.
Object
lifetime
3.4.1.
Do not return a reference to a pointer or an
automatic variable defined with the function
The lifetime of a variable with automatic storage duration
ends on exiting the enclosing block. If a reference or a pointer to such a
variable is returned from a function, the lifetime of the variable will have
ended before the caller can access it through the returned handle, resulting in
undefined behavior.
Corresponding Ada rule:
When an object is destroyed in Ada, whether the object was
created through allocation from a storage pool, or on the program stack, the
Finalize procedure for that object is called. There are no exceptions in Ada.
Ada accessibility rules, which prevent dangling references,
are written in terms of
accessibility
levels, which reflect the run-time nesting of
masters. As explained in
7.6.1, a master is the
execution of a certain construct, such as a
subprogram_body. An
accessibility level is
deeper than another if it is more deeply nested at run
time. For example, an object declared local to a called subprogram has a deeper
accessibility level than an object declared local to the calling subprogram.
The accessibility rules for access types require that the accessibility level
of an object designated by an access value be no deeper than that of the access
type. This ensures that the object will live at least as long as the access
type, which in turn ensures that the access value cannot later designate an
object that no longer exists. The Unchecked_Access attribute may be used to
circumvent the accessibility rules.
A given accessibility
level is said to be
statically deeper than another if the given level is known at
compile time to be deeper than the other for all possible executions. In most
cases, accessibility is enforced at compile time by Legality Rules. Run-time
accessibility checks are also used, since the Legality Rules do not cover
certain cases involving access parameters and generic packages.
3.4.2.
Do not assign the address of a variable to a
pointer with a greater lifetime
The C++ Standard defines 4 kinds of storage duration:
·
Static
·
Thread
·
Automatic
·
Dynamic
The lifetime of objects with the first 3 kinds of storage
duration is fixed, respectively:
·
Until program termination
·
Until thread termination
·
Upon exiting the enclosing block
Therefore, undefined behavior will likely occur if an
address of a variable with automatic storage duration is assigned to a pointer
with static storage duration, or one defined in an outer block. Similarly, for
a thread_local variable aliased to a pointer with static
storage duration.
If using high_integrity::thread,
then references or pointers with local storage duration should not be passed
into threads that have the high_integrity::DETACH
property.
Corresponding Ada rule:
Ada accessibility rules, which prevent dangling references,
are written in terms of
accessibility
levels, which reflect the run-time nesting of
masters. As explained in
7.6.1, a master is the
execution of a certain construct, such as a
subprogram_body. An
accessibility level is
deeper than another if it is more deeply nested at run
time. For example, an object declared local to a called subprogram has a deeper
accessibility level than an object declared local to the calling subprogram.
The accessibility rules for access types require that the accessibility level
of an object designated by an access value be no deeper than that of the access
type. This ensures that the object will live at least as long as the access
type, which in turn ensures that the access value cannot later designate an
object that no longer exists. The Unchecked_Access attribute may be used to
circumvent the accessibility rules.
A given accessibility level is said to be statically deeper than another if the given level is known at
compile time to be deeper than the other for all possible executions. In most
cases, accessibility is enforced at compile time by Legality Rules. Run-time
accessibility checks are also used, since the Legality Rules do not cover
certain cases involving access parameters and generic packages.
3.4.3.
Use RAII for all resources
Objects with non-trivial destructors and automatic storage
duration have their destructors called implicitly when they go out of scope.
The destructor will be called for both normal control flow and when an
exception is thrown.
The same principle does not apply for a raw handle to a
resource, e.g. a pointer to allocated memory. By using a manager class, the
lifetime of the resource can be correctly controlled, specifically by releasing
it in the destructor.
This idiom is known as Resource Allocation Is Initialization
(RAII) and the C++ Language Standard provides RAII wrappers for many resources,
such as:
·
Dynamically allocated memory, e.g.
std::unique.ptr
·
Files, e.g. std::ifstream
·
Mutexes, e.g. std::lock_guard
Corresponding Ada rule:
The Ada language provides the package Ada.Finalization,
which defines two data types:
·
Controlled
– which provides the procedures:
o
Initialize
o
Adjust
o
Finalize
·
Limited_Controlled - which provides the following procedures
o
Initialize
o
Finalize
3.5.
Types
3.5.1.
Do not make any assumptions about the internal
representation of a value or an object
Avoid C++ constructs and practices that are likely to make
your code non-portable:
·
A union provides a way to alter the type
ascribed to a value without changing its representation. This reduces type
safety and is usually unnecessary. In general it is possible to create a safe
abstraction using polymorphic types.
·
Integer types other than signed / unsigned char
have implementation defined size. Do not use integer types directly, instead
use size specified typedefs, defined in a common header file, which can be
easily adjusted to match a particular platform.
·
Do not mix bitwise arithmetic operations on the
same variable, as this it likely to be non-portable between big and little
endian architectures.
·
Do not assume the layout of objects in memory,
e.g. by comparing pointers to different objects with relational operators,
using the offsetof macro, or
performing pointer arithmetic with unspecified or implementation defined
layout.
Corresponding Ada rule:
Ada also has predefined types. In
the case of the predefined integer and floating point types it is advised that
the programmer define types or subtypes which better meet their needs. Ada
allows the programmer to specify the valid range of values for numeric types,
along with specific memory characteristics.
Ada allows the programmer to define
the size of an object or a subtype in bits. The size may of an integer, for
instance, may not exceed the size of the largest integer representation allowed
on the target hardware. The size of a component of a compound type may be
specified, and the specific memory layout of record components may be
specified. Record components can be less than a word in size, or their size may
extend across multiple words.
The size of an object can be
interrogate at run time. For example, a packed array of Boolean values will
represent each array element in one bit. Each bit can be individually indexed
through the standard Ada array indexing notation.
4.
Standard Conversions
4.1.
Array to
pointer conversion
When an array is bound to a function parameter of pointer
type the array is implicitly converted to a pointer to the first element of the
array.
In order to retain the array dimension, the parameter should
be changed to a reference type or changed to a user defined type such as
std::array.
Corresponding Ada rule:
Ada does not provide array to pointer conversion. An array
is a first class type. The dimension and index values of the array are
available wherever the array object is visible.
4.2.
Integral
conversions
4.2.1.
Ensure that the U suffix is applied to a literal
in a context requiring an unsigned integral expression
If a literal is used to initialize a variable of unsigned
type, or with an operand of unsigned type in a binary operation, the U suffix
should be appended to the literal to circumvent an implicit conversion, and
make the intent explicit.
Corresponding Ada rule:
Ada does not provide implicit conversions between data
types. An unsigned type in Ada is known
as a modular type. All integer literals in Ada are of the type Universal
Integer. A value of type Universal Integer will be implicitly converted to the
target type during assignment or in an arithmetic operation. Ada will raise a
compile-time error if the literal is not within the range of the target type
during an assignment. Ada will raise a run time exception if the result of the
arithmetic operation is not within the range of the target type.
The programmer is allowed to define unique numeric data
types. There are no implicit conversions between programmer defined data types,
even when they two types share the same memory layout. Ada is strongly typed.
4.2.2.
Ensure that data loss does not demonstrably
occur in integral expressions
Data loss can occur in a number of contexts:
·
Implicit conversions
·
Type casts
·
Shift operations
·
Overflow in signed arithmetic operations
·
Wraparound in unsigned arithmetic operations
If possible, integral type conversions should be avoided
altogether, by performing operations in a uniform type matched to the execution
environment.
Where data storage is a concern, type conversions should be
localized with appropriate guards (e.g. assertions) to detect data loss.
Similar techniques can be used to guard shift and arithmetic
operations, especially where the data is tainted in a security sense, i.e. a malicious user can trigger data loss with
appropriately crafted input data.
Data loss may also occur if high order bits are lost in a
left shift operation, or the right hand operator of a shift operator is so
large that the resulting value is always 0 or undefined regardless of the value
of the left hand operand.
Therefore, appropriate safeguards should be coded explicitly
(or instrumented by a tool) to ensure that data loss does not occur in shift
operations.
For the purposes of this rule integral to bool conversions are considered to
result in data loss as well. It is preferable to use equality or relational
operators to replace such type conversions. The C++ Language Standard states
that unless the condition of an if, for,
while, or do statement had a
Boolean type, it will implicitly converted to bool.
Note: An implicit
conversion using an operator bool
declared as explicit does not violate
this rule.
Corresponding Ada rule:
All integral data operations in Ada are protected by range
checks. Any violation of a range specification at run time will result in the
exception Constraint_Error.
Ada only allows bit shifting operations on unsigned types.
The Ada Boolean type is not numeric, and therefore does not
undergo conversion to an integral type. All conditions of an if, for, or while statement must result in a Boolean value.
4.3.
Floating
point conversions
The C++ Standard provides 3 floating point types: float, double, long double that, at
least conceptually, increase in precision.
Expressions that implicitly or explicitly cause a conversion
from long double to float or double, and from double
to float should be avoided as they
may result in data loss.
When using a literal in a context that requires a type float, use the F suffix, and for
consistency use the L suffix in a long
double context.
Corresponding Ada rule:
Ada provides a predefined floating point type named float, which should provide at least 6
decimal digits of precision. The programmer is allowed to define custom floating
point types with a user specified range and precision.
There is no implicit conversion between floating point
types.
Ada also provides the ability to define fixed point types.
Fixed point types are implemented as scaled integers. Fixed point types have a
constant number of digits after the decimal place, while floating point types
have a variable number of digits after the decimal place (or none at all)
depending upon the magnitude of the value represented.
There is no implicit conversion between numeric types in
Ada.
4.4.
Floating-integral conversions
4.4.1.
Do not convert floating values to integral
values except through use of standard functions
An implicit or explicit conversion from a floating to an
integral type can result in data loss due to the significant difference in the
respective range of values for each type.
Additionally, floating point to integral type conversions
are biased as the fractional part is simply truncated instead of being rounded
to the nearest integral value. For this reason use one of the standard library
functions: std::floor and std::ceil is recommended if a conversion
to an integral type is necessary.
Note: A return
value of std::floor and std::ceiling is of floating type, and an
implicit or explicit conversion of this value to an integral type is permitted.
Corresponding Ada rule:
There are no implicit conversions between Ada numeric types.
Every floating point type and subtype has the attributes S’Ceiling and S’Floor
defined to calculate the Ceiling and Floor of the floating point value.
If an explicit conversion between a floating point value and
an integral value is performed the conversion will include a range check. If the resulting
value is out of range of the integral type the exception Constraint_Error will
be raised.
5.
Expressions
5.1.
Primary expressions
5.1.1.
Use of symbolic names instead of literal values
in code
Use of “magic” numbers and strings in expressions should be
avoided in preference to constants with meaningful names.
The use of named constants improves both the readability and
maintainability of the code.
Corresponding Ada rule:
Use of constants is recommended over the use of literals.
5.1.2.
Do not rely on the sequence of evaluation within
an expression
To enable optimizations and parallelization, the C++
Standard uses a notation of sequenced
before, e.g.:
·
Evaluation of a full expression is sequenced
before the next full-expression to be evaluated
·
Evaluation operands of an operator are sequenced
before evaluation of the operator
·
Evaluation of arguments in a function call are
sequenced before the execution of the function
·
For built-in operators &&, ||, and operator
? evaluation of the first operand is
sequenced before evaluation of the other operand(s)
This defines a partial order on evaluations, and where two
evaluations are unsequenced with respect to one another, their execution can
overlap. Additionally, two evaluations may be indeterminately sequenced, which
is similar, except that the execution cannot overlap.
This definition leaves great latitude to a compiler to
re-order evaluation of sub-expressions, which can lead to unexpected, and even
undefined behavior. For this reason, and to improve readability an expression
should not:
·
Have more than one side effect
·
Result in the modification and access of the
same scalar object
·
Include a sub-expression that is an assignment
operation
·
Include a sub-expression that is a pre- or
post-increment/decrement operation
·
Include a built-in comma operator (for
overloaded comma operation see Rule 13.2.1: “Do not overload operators with special semantics”)
Corresponding Ada rule:
In Ada assignment is not an expression, and cannot therefore
be part of a compound expression. Furthermore, there are no pre- or
post-increment/decrement expressions in Ada. These limitations limit the
problems associated with violation of Rule 5.1.2.
Ada 2012 does allow the use of “in out” parameters in
functions, which can lead to order dependencies. Early versions of Ada
prohibited “in out” parameters in functions, allowing only “in” parameters, to
avoid this problem. It is still recommended that functions only use “in”
parameters except when interfacing with other languages, such as C and C++.
5.1.3.
Use parentheses in expressions to specify the
intent of the expression
The effects of precedence and associativity of operators in
a complex expression can be far from obvious. To enhance readability and
maintainability of code, no reliance on precedence or associativity should be
made, by using explicit parentheses, except for:
·
Operands of assignment
·
Any combination of + and – operations only
·
Sequence of &&
operations only
·
Sequence of ||operations
only
Corresponding Ada rule:
In every language it is recommended to use explicit
parentheses instead of relying upon precedence and associativity rules.
In Ada the logical and operator is and. The logical or operator is or.
The equivalent of the C++ && is “and then” and the equivalent of
the C++ || is “or else”.
5.1.4.
Do not capture variables implicitly in a lambda
Capturing variables helps document the intention of the
author. It also allows for different variables to be captured
by copy and captured by reference within the same
lambda.
Exception:
It is not necessary to capture objects with static storage
duration or constants that are not ODR used.
However, the use of objects with static storage duration
should be avoided. See Rule 3.3.1: ”Do not use
variables with static storage duration”.
Corresponding Ada rule:
Ada does not provide lamdas. It is possible to pass a
subroutine (function or procedure) as a generic parameter.
5.1.5.
Include a (possibly empty) parameter list in
every lambda
The lambda-declarator is optional in a
lambda expression and results in a closure that can be called without any
parameters.
To avoid any visual ambiguity with other C++ constructs, it
is recommended to explicitly include ( ), even though it is not
strictly required.
Corresponding Ada rule:
Ada does not provide lamdas. It is possible to pass a
subroutine (function or procedure) as a generic parameter.
5.1.6.
Do not code side effects into the right-hand
operands of: &&,
||, sizeof, typeid or a function passed to condition variable::wait
For some expressions, the side effect of a sub-expression
may not be evaluated at all or can be conditionally evaluated, that is,
evaluated only when certain conditions are met. For Example:
·
The right-hand operands of the &&
and || operators are only evaluated if the left hand operand
is true
and false respectively.
·
The operand of sizeof is never evaluated.
·
The operand of typeid is evaluated only if
it is a function call that returns reference to a polymorphic type.
Having visible side effects that do not take place, or only
take place under special circumstances makes the code harder to maintain and
can also make it harder to achieve a high level of test coverage.
Conditional Variable:
wait member
Every time a waiting thread wakes up, it checks the condition.
The wake-up may not necessarily happen in direct response to a notification
from another thread. This is called a spurious wake. It is indeterminate how
many times and when such spurious wakes happen. Therefore it is advisable to
avoid using a function with side effects to perform the condition check.
Corresponding Ada rule:
The same rule applies to the Ada “and then” and “or else”
logical operations. The right hand side of the “and then” operation is only
evaluated if the left hand side is True. The right hand side of the “or else”
operation is only evaluated if the left hand side is False.
5.2.
Postfix
expressions
5.2.1.
Ensure that pointer or array access is
demonstrably within bounds of a valid object
Unlike standard library containers, arrays do not benefit
from bounds checking.
Array access can take one of the equivalent forms: *(p + i) or
p[i],
and will result in undefined behavior, unless p and p + i point to
elements of the same array object. Calculating (but not dereferencing) an
address one past the last element of the array is well defined also. Note that
a scalar object can be considered as equivalent to an array dimensioned to 1.
To avoid undefined behavior, appropriate safeguards should
be coded explicitly (or instrumented by a tool), to ensure that array access is
within bounds, and that indirection operations (*) will not result in a null
pointer dereference.
Corresponding Ada rule:
Ada makes these checks automatically, so there is no need for
manually making them or using some separate tool that also needs to be
validated. The compiler will statically
check values for array bounds violations and issue a compilation error when a
bounds violation is detected. The Ada compiler also automatically inserts
bounds checking into array accesses as needed.
Moreover, the range of the index values for an array can be
any contiguous range of discrete values. Ada array indexing is not tied to
pointer arithmetic and the lowest bound for an array need not be 0. Ada array
definitions include a definition of the array type and range as well as the
element type. Arrays have well defined attributes available to the programmer
wherever the array object is visible.
Table 1 Attributes of array A
Array Attribute
|
Description
|
A’Length
|
The
number of elements in the array
|
A’First
|
The
value of the lowest index value for the array
|
A’Last
|
The
value of the highest index value for the array
|
A’Range
|
The
range of index values for the array; equivalent to A’First..A’Last
|
Iterating through an array safely in Ada is done using the
Ada for loop:
For loop syntax
|
Description
|
for I in A’Range loop
… do
something
end loop;
|
Iterate
through the index values for the range of array A, doing something for each
index value.
|
for value of A loop
… do
something
end loop;
|
Iterate
through array A referencing each value of A in turn. This is equivalent to a
“for each” loop in some other languages
|
5.2.2.
Ensure that functions do not call themselves,
either directly or indirectly
As the program stack tends to be one of the more limited
resources, excessive use of recursion may limit the scalability and portability
of the program. Tail recursion can readily be replaced with a loop. Other forms
of recursion can be replaced with an iterative algorithm and worklists.
Corresponding Ada rule:
If you want to enforce this rule in Ada you can use the
profile No_Recursion.
5.3.
Unary
expressions
5.3.1.
Do not apply unary minus to operands of unsigned
type
The result of applying a unary minus operator (-)
to an operand of unsigned type (after integral promotion) is a value that is
unsigned and typically very large.
Prefer to use the bitwise complement (~)
operator instead.
Corresponding Ada rule:
The rule here is the same for Ada. The result for an Ada
unsigned type is well defined, but may not be what the programmer intends.
5.3.2.
Allocate memory using new and release it using
delete
C style allocation is not type safe, and does not invoke
constructors or destructors. For this reason only operators new and
delete
should be used to manage objects with dynamic storage duration.
Note: Invoking delete on a pointer allocated
with malloc
or invoking free on a pointer allocated with new will result in
undefined behavior.
Corresponding Ada rule:
All Ada allocation / deallocation mechanisms are type safe.
5.3.3.
Ensure that the form of delete matches the form
of new used to allocate the memory
The C++ Standard requires that the operand to the delete operator
is either:
·
a null pointer
·
pointer to a non array object allocated with new
·
pointer
to a base class3 subobject
of a non array object allocated with new
Similarly, the operand to the delete[] operator is either:
·
a null pointer
·
pointer to an array object allocated with new[]
In order to avoid undefined behavior, plain and array forms
of delete
and new should not be mixed.
Corresponding Ada rule:
Ada does not have this problem.
There are no special forms of new for allocating arrays in
Ada. An Ada array is a first class type
and the compiler knows the size of an array. The new operator allocates the
memory needed to hold an object of the type specified.
Ada does requires the generic function
Ada.Unchecked_Deallocation be instantiated for the specific type to be
deallocated. The generic parameters for this function are the access type and
the base type being deallocated.
5.4.
Explicit
type conversion
5.4.1.
Only use casting forms: static_cast (excl,
void*), dynamic_cast or explicit constructor call
All casts result in some degree of type punning, however,
some casts may be considered more error prone than others:
·
It is undefined behavior for the result of a static
cast to void* to be cast to any type other than the original from
type.
·
Depending on the type of an object, casting away
const
or volatile and attempting to write to the result is
undefined behavior.
·
Casts using reinterpret cast are
generally unspecified and/or implementation defined. Use of this cast increases
the effort required to reason about the code and reduces its portability.
·
Simplistically, a C-style cast and a non class
function style cast can be considered as a sequence of the other cast kinds.
Therefore, these casts suffer from the same set of problems. In addition,
without a unique syntax, searching for such casts in code is extremely
difficult.
Corresponding Ada rule:
Ada uses the term type conversion when converting a value
from one type to another type. There is only one syntax for type
conversion: subtype-name
(expression-or-identifier)
If the target type of a conversion is an integer type and
the operand is a real type then the result is the result rounded to the nearest
integer (away from zero if exactly half way between two integers). Range
checking is done after the converted value has been calculated.
5.4.2.
Do not cast an expression to an enumeration type
The result of casting an integer to an enumeration type is
unspecified if the value is not within the range of the enumeration. This also
applies when casting between different enumeration types.
For this reason conversions to an enumeration type should be
avoided.
Corresponding Ada rule:
Converting between an integer and an enumeration value is
not performed by type conversion. Instead, enumeration types have the
attributes T’Pos and T’Val. T’Pos takes an argument of the enumeration type and
returns the position number of that argument. T’Val takes an integer as an
argument and returns the value of type T whose position number equals the value
of the argument. If there is no value with the given position number the
exception Constraint_Error is raised.
5.4.3.
Do not convert
from a base class to a derived class
The most common reason for casting down an inheritance
hierarchy, is to call derived class methods on an object that is a reference or
pointer to the base class.
Using a virtual function removes the need for the cast
completely and improves the maintainability of the code.
Corresponding Ada rule:
In Ada casting between levels of an inheritance hierarchy is
called a view conversion. Converting from a base type to a derived type is
valid only when the derived type does not add any data members to the set of
members defined in the base type.
In Ada polymorphic types are called tagged types.
5.5.
Multiplicative operators
5.5.1.
Ensure that the right hand operand of the
division or remainder operations is non-zero
The result of integer division or remainder operation is
undefined if the right hand operand is zero. Therefore, appropriate safeguards
should be coded explicitly (or instrumented by a tool) to ensure that division
by zero does not occur.
Corresponding Ada rule:
The result of division by zero is well defined in Ada. The
result will cause the exception Constraint_Error to be raised.
5.6.
Shift
operators
5.6.1.
Do not use bitwise operators with signed
operands
Use of signed operands with bitwise operators is in some
cases subject to undefined or implementation defined behavior. Therefore,
bitwise operators should only be used with operands of unsigned integral types.
Corresponding Ada rule:
In Ada the bitwise operators are only defined for unsigned
types.
5.7.
Equality
operators
5.7.1.
Do not write code that expects floating point
calculations to yield exact results
Floating point calculations suffer from machine precision
(epsilon), such that the exact result may not be representable.
Epsilon is defined as the difference between 1 and the
smallest value greater than 1 that can be represented in a given floating point
type. Therefore, comparisons of floating point values need to take epsilon into
account.
Corresponding Ada rule:
Floating point number exhibit the same behavior in Ada. If
you want exact equality in a real number then use a fixed point representation
instead of a floating point representation.
5.7.2.
Ensure that a pointer to a member that is a
virtual function is only compared (==) with nullptr
The result of comparing a pointer to member to a virtual
function to anything other than nullptr is unspecified.
Corresponding Ada rule:
Ada polymorphism does not depend upon pointers to members of
a class hierarchy. Parameters to subprograms can be defined as class-wide
parameters, and the actual parameter can be any object with the class
hierarchy.
5.8.
Conditional operator
5.8.1.
Do not use the conditional operator (?:) as a
sub-expression
Evaluation of a complex condition is best achieved through
explicit conditional statements (if/else). Using the result of the conditional
operator as an operand reduces the maintainability of the code.
The only permissible uses of a conditional expression are:
·
argument expression in a function call
·
return expression
·
initializer in a member initialization list
·
object initialize
·
the right hand side operand of assignment
(excluding compound assignment)
The last use is allowed on the basis of initialization of an
object with automatic storage duration being equivalent to its declaration,
followed by assignment.
Corresponding Ada rule:
Ada2012 provides two forms of conditional expression; the
conditional if statement and the conditional case statement:
Note: In
Ada assignment is not an expression. Each dependent expression of a condtional
expression must be the type of the conditional expression.
6.
Statements
6.1.
Selection
statements
6.1.1.
Enclose the body of a selection or an iteration
statement in a compound statement
Follow each control flow primitive (if, else,
while,
for,
do and
switch)
by a block enclosed by braces, even if the block is empty or contains only one
line. Use of null statements or statement expressions in these contexts reduces
code readability and making it harder to maintain.
Corresponding Ada rule:
All Ada selection or iteration statements are always fully
bracketed compound statements.
6.1.2.
Explicitly cover all paths through multi-way
selection statements
Make sure that each if-else-if chain has a final else
clause, and every switch statement has a default clause.
The advantage is that all execution paths are explicitly
considered, which in turn helps reduce the risk that an unexpected value will
result in incorrect execution.
Corresponding Ada rule:
The rule for if-elsif-else in Ada applies.
Ada case statements require all possible values of the
selection type to be covered. If some value or values are not covered
explicitly then an others case must
be included. The compiler will issue an error if this rule is not followed.
6.1.3.
Ensure that a non-empty case statement block
does not fall through to the next label
Fall through from a non empty case block of a switch
statement makes it more difficult to reason about the code, and therefore
harder to maintain.
Corresponding Ada rule:
Ada case statements do not exhibit fall-through.
6.1.4.
Ensure that a switch statement has at least two
case labels, distinct from the default label
A switch statement with fewer than two case labels can be
more naturally expressed as a single if statement.
Corresponding Ada rule:
Ada case statements are intended to handle all the values of
a discrete type, not just one. A simple if statement is appropriate if only one
value of a discrete range of values is being tested.
6.2.
Iteration
statements
6.2.1.
Implement a loop that only uses element values
as a range-based loop
A range-based for statement reduces the amount of
boilerplate code required to maintain correct loop semantics.
A range-based loop can normally replace an explicit loop
where the index or iterator is only used for accessing the container value.
Corresponding Ada rule:
The traditional Ada for loop always iterates over a range of
values. The Ada iterator loop iterates through the values of all standard Ada
containers and all Ada arrays.
6.2.2.
Ensure that a loop has a single loop counter, an
optional control variable, and is not degenerate
A loop is considered ’degenerate’ if:
·
when entered, the loop is infinite, or
·
the loop will always terminate after the first
iteration.
To improve maintainability it is recommended to avoid
degenerate loops and to limit them to a single counter variable.
Corresponding Ada rule:
Ada provides four kinds of loops.
·
The simple loop is the most general loop and can
be used for loops that test termination at the beginning of the loop, at the
end of the loop, or in the middle of the loop
·
The while loop tests a control expression at the
top of the loop
·
The for loop executes for each value in a
specified range
·
The iterator loop executes for every member of a
container or array
Ada does not provide a direct equivalent of the C++ for
loop.
6.2.3.
Do not alter the control or counter variable
more than once in a loop
The behavior of iteration statements with multiple
modifications of control or counter variables is difficult to understand and
maintain.
Corresponding Ada rule:
The Ada for loop has a counter which is modified only at the
top of the loop, and is read-only within the body of the loop. The counter
cannot be modified more than once per loop iteration.
6.2.4.
Only modify a for loop counter in the for
expression
It is expected that a for loop counter is modified for every
iteration. To improve code readability and maintainability, the counter
variable should be modified in the loop expression.
Corresponding Ada rule:
The Ada for loop has a counter which is modified only at the
top of the loop, and is read-only within the body of the loop. The counter
cannot be modified more than once per loop iteration.
6.3.
Jump
statements
6.3.1.
Ensure that the label(s) for a jump statement or
a switch condition appear later, in the same or an enclosing block
Backward jumps and jumps into nested blocks make it more
difficult to reason about the flow through the function.
Loops should be the only constructs that perform backward
jumps, and the only acceptable use of a goto statement is to jump forward to an
enclosing block.
Control can also be transferred forward into a nested block
by virtue of a switch label. Unless case and default labels are placed only
into the top level compound statement of the switch, the code will be difficult
to understand and maintain.
Corresponding Ada rule:
Ada case statements do not allow selection of a value within
an if statement.
Ada loops can be labeled, and the loop exit statement in an
inner loop can identify the label of an outer loop, allowing a direct exit from
the outer loop controlled by a condition in an inner loop. This ability removes
the need to set flag variables throughout a nesting of loops with associated
testing of the flag. The advantage is code simplicity and added readability.
6.3.2.
Ensure that execution of a function with a
non-void return type ends in a return statement with a value
Undefined behavior will occur if execution of a function
with a non void return type (other than main) flows off the end of
the function without encountering a return statement with a value.
Exception:
The main function is exempt from this rule, as an
implicit return 0; will be executed, when an explicit return statement
is missing.
Corresponding Ada rule:
Ada provides two kinds of subprograms. Procedures never
return a value. Functions always return a value. The return value of a function
must always be handled by the calling subprogram. The Ada compiler will issue
an error if a function does not return a value of the specified return type, or
if a procedure attempts to return a value of any type.
The Ada main subprogram is always a procedure and returns no
value.
6.4.
Declaration statement
To preserve locality of reference, variables with automatic
storage duration should be defined just before they are needed, preferably with
an initializer, and in the smallest block containing all the uses of the
variable.
The scope of a variable declared in a for loop
initialization statement extends only to the complete for statement.
Therefore, potential use of a control variable outside of
the loop is naturally avoided.
Corresponding Ada rule:
Ada only allows variables, constants, subprograms, or tasks
to be defined in the declaration section of a block. The one exception is the
for loop control variable which is defined at the start of the for loop and is
not visible outside the for loop.
7.
Declarations
7.1.
Specifiers
7.1.1.
Declare each identifier on a separate line in a
separate declaration
Declaring each variable or typedef on a separate line makes
it easier to find the declaration of a particular identifier.
Determining the type of a particular identifier can become
confusing for multiple declarations on the same line.
Exception:
For loop initialization statement is exempt from this rule, as
in this context the rule conflicts with Rule 6.4.1: ”Postpone
variable definitions as long as possible”, which takes precedence.
Corresponding Ada rule:
Ada only allows variables, constants, subprograms, or tasks
to be defined in the declaration section of a block. The one exception is the
for loop control variable which is defined at the start of the for loop and is
not visible outside the for loop.
7.1.2.
Use const whenever possible
This allows specification of semantic constraint which a
compiler can enforce. It explicitly communicates to other programmers that
value should remain invariant. For example, specify whether a pointer itself is
const, the data it points to is const, both or neither.
Exception:
By-value return types are exempt from this rule. These should
not be const as doing so will inhibit move semantics.
Corresponding Ada rule:
Ada allows an object to be defined as constant, but does not
have the complex const rules of C++. When passing parameters to a subprogram
the passing mode can be defined as IN, which forces the subprogram to view that
parameter as a constant. Parameters passed with the IN mode are read-only
within the subprogram they are passed to.
7.1.3.
Do not place type specifiers before non-type
specifiers in declaration
The C++ Standard allows any order of specifiers in a
declaration. However, to improve readability if a non-type specifier (typedef,
friend,
constexpr,
register,
static,
extern,
thread
local, mutable, inline, virtual, explicit)
appears in a declaration, it should be placed leftmost in the declaration.
Corresponding Ada rule:
Variable declarations always take the form of
7.1.4.
Place CV-qualifiers on the right hand side of
the type they apply to
The const or volatile qualifiers can appear either to the
right or left of the type they apply to. When the unqualified portion of the
type is a typedef name (declared in a previous typedef declaration), placing
the CV-qualifiers on the left hand side, may result in confusion over what part
of the type the qualification applies to.
For consistency, it is recommended that this rule is applied
to all declarations.
Corresponding Ada rule:
Variable declarations always take the form of
7.1.5.
Do not inline large functions
The definition of an inline function needs to be available
in every translation unit that uses it. This in turn requires that the
definitions of inline functions and types used in the function definition must
also be visible.
The inline keyword is just a hint, and compilers in
general will only inline a function body if it can be determined that
performance will be improved as a result.
As the compiler is unlikely to inline functions that have a
large number of statements and expressions, inlining such functions provides no
performance benefit but will result in increased dependencies between
translation units.
Given an approximate cost of 1 for every expression and
statement, the recommended maximum cost for a function is 32.
Corresponding Ada rule:
The inline aspect of a subprogram is optional for a
compiler. It notifies the compiler of the programmer’s desire that the
subprogram be inlined.
7.1.6.
Use class types or typedefs to abstract scalar
quantities and standard integer types
Using class types to represent scalar quantities exploits
compiler enforcement of type safety. If this is not possible, typedefs should
be used to aid readability of code.
Plain char type should not be used to define a typedef name,
unless the type is intended for parameterizing the code for narrow and wide
character types. In other cases, an explicit signed char or unsigned
char type should be used in a typedef as appropriate.
To enhance portability, instead of using the standard
integer types (signed char, short, int, long, long long, and the unsigned
counterparts), size specific types should be defined in a project-wide header
file, so that the definition can be updated to match a particular platform (16,
32 or 64bit). Where available, intN t and uintN t types (e.g. int8 t)
defined in the cstdint header file should be used for this purpose.
Where the auto type specifier is used in a declaration, and
the initializer is a constant expression, the declaration should not be allowed
to resolve to a standard integer type. The type should be fixed by casting the
initializer to a size specific type.
Exception:
The C++ Language Standard places type requirements on
certain constructs. In such cases, it is better to use required type explicitly
rather than the typedef equivalent which would reduce the portability of
the code.
The following constructs are therefore exceptions to this
rule:
·
int main()
·
T
operator++(int)
·
T
operator–(int)
Corresponding Ada rule:
Ada allows the programmer to create simple and detailed
definitions of scalar types and subtypes. New integer types can be simply
defined by creating a type name and specifying the valid range of values for
the type, or they can be derived from an existing type with a restriction on
the range of valid values.
For example:
type Counts is range 0..100;
type Normalized is new Integer range
-100..100;
Ada also allows the programmer to define subtypes of existing
types. Objects of a subtype are members of the base type, usually with a
restricted range of valid values.
subtype Natural is Integer range
0..Integer’Last;
subtype Positive is Integer range
1..Integer’Last;
Subtypes can be used in mixed calculations with any other subtypes
of the same base type. Objects of different base types cannot be used in an
expression without explicit conversion to a common type.
7.1.7.
Use a trailing return type in preference to type
disambiguation using typename
When using a trailing return type, lookup for the function
return type starts from the same scope as the function declarator. In many
cases, this will remove the need to specify a fully qualified return type along
with the typename keyword.
Corresponding Ada rule:
Ada has no equivalent of a trailing return type.
7.1.8.
Use auto id = expr when declaring a variable to
have the same type as its initializer function call
When declaring a variable that is initialized with a
function call, the type is being specified twice. Initially on the return of
the function and then in the type of the declaration.
Using auto and implicitly deducing the type of the initializer will
ensure that a future change to the declaration of foo will not result in the
addition of unexpected implicit conversions.
Corresponding Ada rule:
This rule is contrary to Ada strong typing. Every variable
must be declared to be definite, even if it is a member of an indefinite type.
The type returned by an initializing function must match the type of the object
being declared.
An object of an indefinite type must be initialized to a
definite object. For instance, the type String is an unconstrained array of
character. An unconstrained array is an indefinite type. Each instance of a
String must have a specified length, which is defined when the String object is
initialized. The String object is definite. The String type is indefinite.
7.1.9.
Do not explicitly specify the return type of a lambda
Allowing the return type of a lambda to be implicitly
deduced reduces the danger of unexpected implicit conversions, as well as
simplifying future maintenance, where changes to types used in the lambda would
otherwise result in the need to change the return type.
Corresponding Ada rule:
Ada does not allow lambdas.
7.1.10.
Use static_assert for assertions involving
compile time constants
A static assert will generate a compile error if its
expression is not true. The earlier that a problem can be diagnosed the
better, with the earliest time possible being as the code is written.
Corresponding Ada rule:
When defining a subtype the programmer should use either a
Dynamic_Predicate or a Static_Predicate if the use of a simple range cannot
fully define the characteristics of the subtype.
A Static_Predicate expression must be one of
·
A static membership test where the choice is
selected by the current instance
·
A case expression whose dependent expressions
are static and selected by the current instance
·
A call of the predefined operations =, /=, <,
<=, >, >= where one operand is the current instance
·
An ordinary static expression
A Dynamic_Expression can be any Boolean expression.
Examples:
subtype Even is Integer
with
Dynamic_Predicate => Even mod 2 = 0;
subtype Letter is Character
with
Static_Predicate => Letter in ‘A’..’Z’|’a’..’z’;
7.2.
Enumeration declarations
7.2.1.
Use an explicit enumeration base and ensure that
it is large enough to store all enumerations
The underlying type of an unscoped enumeration is
implementation defined, with the only restriction being that the type must be
able to represent the enumeration values. An explicit enumeration base should
always be specified with a type that will accommodate both the smallest and the
largest enumerator.
A scoped enum will implicitly have an underlying type of int,
however, the requirement to specify the underlying type still applies.
Exception:
An enumeration declared in an extern "C" block
(i.e. one intended to be used with C) does not require an explicit underlying
type.
Corresponding Ada rule:
Ada enumerations are not numeric types and cannot be
converted to numeric types. The compiler chooses the size of the enumeration
representation base upon the number of enumeration values.
7.2.2.
Initialize none, the first only, or all
enumerators in an enumeration
It is error prone to initialize explicitly only some
enumerators in an enumeration, and to rely on the compiler to initialize the
remaining ones. For example, during maintenance it may be possible to introduce
implicitly initialized enumerators with the same value as an existing one
initialized explicitly.
Exception:
When an enumeration is used to define the size and to index
an array, it is acceptable and recommended to define three additional
enumerators after all other enumerators, to represent the first and the last
elements, and the size of the array.
Corresponding Ada rule:
When specifying the enumeration representation all
enumerated values must be specified.
Example:
type Mix_Code is (ADD, SUB, MUL, LDA, STA, STZ);
for Mix_Code use (ADD => 1, SUB => 2, MUL => 3, LDA => 8,
STA => 24, STZ =>33);
7.3.
Namespaces
7.3.1.
Do not use using directives
Namespaces are an important tool in separating identifiers
and in making interfaces explicit.
A using directive, i.e. using
namespace, allows any name to be searched for in the namespace specified
by the using directive.
A using declaration, on the other hand,
brings in a single name from the namespace, as if it was declared in the scope
containing the using declaration.
Corresponding Ada rule:
The unit of encapsulation in Ada is the package. Packages
provide the name space designation of C++ namespaces.
In Ada one can either use the entire package, use only a
specific type defined in the package, or explicitly append the package name to
each element accessed from the package.
Any naming ambiguity will be identified by the compiler.
7.4.
Linkage
specifications
7.4.1.
Ensure that any objects, functions or types to
be used from a single translation unit are defined in an unnamed namespace in
the main source file
Declaring an entity in an unnamed namespace limits its
visibility to the current translation unit only. This helps reduce the risk of
name clashes and conflicts with declarations in other translation units.
It is preferred to use unnamed namespaces rather than the static keyword
to declare such entities.
Corresponding Ada rule:
Each compilation unit specifies its own dependencies in its
dependency clauses. There is no visibility to compilation units not defined in
the dependency clause.
7.4.2.
Ensure that an inline function, a function
template, or a type used from multiple translation units is defined in a single
header file
An inline function, a function template or a user defined
type that is intended for use in multiple translation units should be defined
in a single header file, so that the definition will be processed in exactly
the same way (the same sequence of tokens) in each translation unit.
This will ensure that the one definition rule is
adhered to, avoiding undefined behavior, as well as improving the
maintainability of the code.
Corresponding Ada rule:
Each subprogram, generic compilation unit, type, task, task
type, protected object, or protected type is defined uniquely in a compilation
unit. Dependency upon compilation units which define types or objects with
overlapping identifiers results in ambiguity which is flagged as a compilation
error and must be corrected before an executable is produced by the compilation
process.
7.4.3.
Ensure that an object or a function used from
multiple translation units is declared in a single header file
An object or function with external linkage should be declared
in a single header file in the project.
This will ensure that the type seen for an entity in each translation
unit is the same thereby avoiding undefined behavior.
Corresponding Ada rule:
Objects used by multiple compilation units must be declared
in the public region of a package. Functions and procedures can either be
declared in a package or in a stand-alone compilation unit.
7.5.
The asm
declaration
7.5.1.
Do not use the asm declaration
Use of inline assembly should be avoided since it restricts
the portability of the code.
Corresponding Ada rule:
Machine code insertions should be avoided since they
restrict the portability of the code.
8.
Definitions
8.1.
Type names
8.1.1.
Do not use multiple levels of pointer
indirection
In C++, at most one level of pointer indirection combined
with references is sufficient to express any algorithm or API.
Instead of using multidimensional arrays, an array of
containers or nested containers should be used. Code reliant on more than one
level of pointer indirection will be less readable and more difficult to
maintain.
Corresponding Ada rule:
Use of multidimensional arrays is allowed, or one can choose
containers or nested containers. Ada arrays do not employ pointer indirection
at the source code level.
8.2.
Meanings of declarators
8.2.1.
Make parameter names absent or identical in all
declarations
Although the C++ Standard does not mandate that parameter
names match in all declarations of a function (e.g. a declaration in a header
file and the definition in the main source file), it is good practice to follow
this principle.
Corresponding Ada rule:
All subprogram parameter names must be declared and must match
the corresponding subprogram implementation.
Subprogram parameters may be referenced by position or by
name when the subprogram is called. It is recommended to use named notation
when calling a subprogram. When using named notation the actual parameter is
explicitly matched with the formal parameter, and the order of parameters in
the called subprogram is not relevant.
Example:
Procedure Get_Line(Item :
out String; Last : out Natural);
Length : Natural;
Input : String(1..256);
Get_Line(Item =>
Input, Last => Length);
8.2.2.
Do not declare functions with excessive number
of parameters
A function defined with a long list of parameters often
indicates poor design and is difficult to read and maintain.
The recommended maximum number of function parameters is
six.
Corresponding Ada rule:
While it is seldom useful to declare a subprogram with a
large number of parameters, the problem is greatly relieved through the use of
named notation.
8.2.3.
Pass small objects with a trivial copy
constructor by value
Because passing by const reference involves an indirection,
it will be less efficient than passing by value for a small object with a
trivial copy constructor.
Corresponding Ada rule:
Ada parameters have an associated mode indicating data flow.
The modes are IN, OUT, IN OUT. The compiler determines whether a particular
parameter should be passed by copy or by reference.
8.2.4.
Do not pass std::unique_ptr by const reference
An object of type std::unique ptr should be
passed as a non-const reference, or by value. Passing by non-const reference
signifies that the parameter is an in/out parameter. Passing by value signifies
that the parameter is a sink (i.e. takes ownership and does not return it).
A const reference std::unique ptr parameter
provides no benefits and restricts the potential callers of the function.
Corresponding Ada rule:
Ada parameter modes explicitly state whether the parameter
is passed IN, OUT, or IN OUT.
8.3.
Function
definitions
8.3.1.
Do not write functions with excessive McCabe
Cyclomatic Complexity
The McCabe Cyclomatic Complexity is calculated as the number
of decision branches within a function plus 1.
Complex functions are hard to maintain and test effectively.
It is recommended that the value of this metric does not exceed 10.
Corresponding Ada rule:
Excessive cyclomatic complexity has been shown to produce
code that is difficult to understand and maintain. It is often an indication of
poor code design.
8.3.2.
Do not write functions with a high static
program path count
Static program path count is the number of non-cyclic execution
paths in a function. Functions with a high number of paths through them are
difficult to test, maintain and comprehend. The static program path count of a
function should not exceed 200.
Corresponding Ada rule:
High static path count is an indication of poor software
design. All code should be testable and maintainable.
8.3.3.
Do not use default arguments
Use of default arguments can make code maintenance and
refactoring more difficult. Overloaded forwarding functions can be used instead
without having to change existing function calls.
Corresponding Ada rule:
Overloaded forwarding functions provide no maintenance
benefit. Use of default arguments and named notation for calling subprograms
clearly documents all calls for maintenance purposes without creating a
plethora of overloaded subprograms.
8.3.4.
Define =delete functions with parameters of
rvalue reference to const
A simple model for an rvalue reference is
that it allows for the modification of a temporary. A const rvalue reference therefore defeats the purpose of the
construct as modifications are not possible.
However, one valid use case is where the function is defined
=delete.
This will disallow the use of an rvalue as an
argument to that function.
Corresponding Ada rule:
Ada has no equivalent to defining a subprogram as =delete.
8.4.
Initializers
8.4.1.
Do not access an invalid object or an object
with indeterminate value
A significant component of program correctness is that the
program behavior should be deterministic. That is, given the same input and
conditions the program will produce the same set of results.
If a program does not have deterministic behavior, then this
may indicate that the source code is reliant on unspecified or undefined
behavior.
Such behaviors may arise from use of:
·
variables not yet initialized
·
memory (or pointers to memory) that has been
freed
·
moved from objects
Corresponding Ada rule:
The Ada compiler identifies when the value of a variable is
used before the variable is initialized or assigned a value. The SPARK subset
of Ada requires all variables to be fully initialized.
8.4.2.
Ensure that a braced aggregate initialize
matches the layout of the aggregate object
If an array or a struct is non-zero initialized,
initializers should be provided for all members, with an initializer list for
each aggregate (sub)object enclosed in braces. This will make it clear what
value each member is initialized with.
Corresponding Ada rule:
Aggregates can be constructed for arrays or records. While
aggregates can be constructed using positional notation, similar to C++, named
notation should always be used.
Use of named notation will ensure and document that all
elements of the composite type have been initialized.
9.
Classes
9.1.
Member
functions
9.1.1.
Declare static any member function that does not
require this. Alternatively, declare const any member function that does not
modify the externally visible state of the object
A non-virtual member function that does not access the this pointer
can be declared static. Otherwise, a function that is virtual or does not
modify the externally visible state of the object can be declared const.
The C++ language permits that a const member function
modifies the program state (e.g. modifies a global variable, or calls a
function that does so). However, it is recommended that const member functions
are logically const also, and do not cause any side effects.
The mutable keyword can be used to declare member data
that can be modified in a const function, however, this should only be used
where the member data does not affect the externally visible state of the
object.
Corresponding Ada rule:
Packages are the unit of encapsulation in Ada. Tagged types
are polymorphic types in Ada. Tagged types can be defined in a package, making
them available to multiple compilation units. Tagged types can have primitive
operations, which correspond to virtual member functions in C++. A subprogram
is primitive to some tagged type T if all of the following are true:
·
The subprogram is declared in the visible part
of the package in which tagged type T is declared
·
The subprogram has a parameter of type T, or an
access parameter pointing to an instance of type T, or is a function returning
a result of type T
Any subprogram which is declared in the package where type T
is declared, and does not meet the above requirements, is not a primitive of
type T. Such a subprogram is equivalent to a C++ static member function. Any
procedure with only an parameter of IN mode of type T is equivalent to a C++
const member function.
9.1.2.
Make default arguments the same or absent when
overriding a virtual function
The C++ Language Standard allows that default arguments be
different for different overrides of a virtual function.
However, the compiler selects the argument value based on
the static type of the object used in the function call.
This can result in confusion where the default argument
value used may be different to the expectation of the user.
Corresponding Ada rule:
The Ada Language Standard requires overridden subprograms be conformant in their
use of default parameter expressions. When a subprogram overrides another
subprogram the parameter profile cannot change regarding default expressions.
9.1.3.
Do not return non-const handles to class data
from const member functions
A pointer or reference to non-const data returned from a
const member function may allow the caller to modify the state of the object.
This contradicts the intent of a const member function.
Corresponding Ada rule:
A procedure with an IN mode parameter cannot modify the data passed to it, nor can
it return a value of any kind.
9.1.4.
Do not write member functions which return
non-const handles to data less accessible than the member function
Member data that is returned by a non-const handle from a
more accessible member function, implicitly has the access of the function and not
the access it was declared with. This reduces encapsulation and increases
coupling.
Exception:
Non-const operator [] is exempt from this rule, as in this
context the rule conflicts with Rule 13.2.4: ”When
overloading the subscript operator (operator[]) implement both const and non-const versions”,
which takes precedence.
Corresponding Ada rule:
Ada functions need not return pointers to complex data.
Instead they can return entire complex objects including records and arrays.
This avoids the lifetime issues associated with C++ member functions.
9.1.5.
Do not introduce virtual functions in a final
class
Declaring a class as final explicitly documents
that this is a leaf class as it cannot be used as a base class.
Introducing a virtual function in such a class is therefore
redundant as the function can never be overridden in a derived class.
Corresponding Ada rule:
Ada does not explicitly label a tagged type “final”.
Instead, if no subprograms with class-wide parameters are introduced the tagged
type has the effect of being “final”.
9.2.
Bit-fields
9.2.1.
Declare bit-fields with an explicitly unsigned
integral or enumeration type
To avoid reliance on implementation defined behavior, only
declare bit-fields of an explicitly unsigned type (uintN t) or an
enumeration type with an enumeration base of explicitly unsigned type.
Corresponding Ada rule:
One must define data types with value ranges that can fit
into the specified bit layout of a record. Failure to do so will result in a
compiler error message.
Example:
Word : constant := 4; -- storage element is byte, 4 bytes per word
type State is (A,M,W,P);
type Mode is (Fix, Dec, Exp, Signif);
type Byte_Mask is array (0..7) of Boolean;
type State_Mask is array (State) of Boolean;
type Mode_Mask is array (Mode) of Boolean;
type Program_Status_Word is
record
System_Mask : Byte_Mask;
Protection_Key : Integer range 0 .. 3;
Machine_State : State_Mask;
Interrupt_Cause : Interruption_Code;
Ilc : Integer range 0 .. 3;
Cc : Integer range 0 .. 3;
Program_Mask : Mode_Mask;
Inst_Address : Address;
end record;
for Program_Status_Word use
record
System_Mask at 0*Word range 0 .. 7;
Protection_Key at 0*Word range 10 .. 11; -- bits 8,9 unused
Machine_State at 0*Word range 12 .. 15;
Interrupt_Cause at 0*Word range 16 .. 31;
Ilc at 1*Word range 0 .. 1; -- second word
Cc at 1*Word range 2 .. 3;
Program_Mask at 1*Word range 4 .. 7;
Inst_Address at 1*Word range 8 .. 31;
end record;
for Program_Status_Word'Size use 8*System.Storage_Unit;
for Program_Status_Word'Alignment use 8;
10.
Derived classes
10.1.
Multiple base classes
10.1.1.
Ensure that access to base class subobjects does
not require explicit disambiguation
A class inherited more than once in a hierarchy, and not
inherited virtually in all paths will result in multiple base class subobjects
being present in instances of the derived object type.
Such objects require that the developer explicitly select
which base class to use when accessing members. The result is a hierarchy that
is harder to understand and maintain.
Corresponding Ada rule:
Ada allows multiple inheritance only of interfaces, not of
tagged types. All subprograms in interfaces are abstract, corresponding to
virtual inheritance of multiple base classes in C++.
10.2.
Virtual functions
10.2.1.
Use the override special identifier when
overriding a virtual function
The override special identifier is a directive to the
compiler to check that the function is overriding a base class member. This
will ensure that a change in the signature of the virtual function will
generate a compiler error.
Corresponding Ada rule:
An overriding_indicator is used to declare that an operation is
intended to override (or not override) an inherited operation.
Syntax
overriding_indicator ::= [not] overriding
Legality Rules
·
the
operation shall be a primitive operation for some type;
·
if
the overriding_indicator is overriding,
then the operation shall override a homograph at the place of the declaration
or body;
·
if
the overriding_indicator is not
overriding, then the operation shall not override any homograph (at any
place).
In addition to the places where Legality
Rules normally apply, these rules also apply in the private part of an instance
of a generic unit.
10.3.
Abstract classes
10.3.1.
Ensure that a derived class has at most one base
class which is not an interface class
An interface class has the following properties:
·
all public functions are pure virtual functions
or getters, and
·
there are no public or protected data members,
and
·
it
contains at most one private data member of integral or enumerated type
Inheriting from two or more base classes that are not
interfaces, is rarely correct. It also exposes the derived class to multiple
implementations, with the risk that subsequent changes to any of the base
classes may invalidate the derived class.
On the other hand. it is reasonable that a concrete class
may implement more than one interface.
Corresponding Ada rule:
Ada only allows inheritance from one base class. Ada allows
multiple inheritance of interfaces.
11.
Member access control
11.1.
Access specifiers
11.1.1.
Declare all data member private
If direct access to the object state is allowed through
public or protected member data, encapsulation is reduced making the code
harder to maintain.
By implementing a class interface with member functions
only, precise control is achieved over modifications to object state as well as
allowing for pre and post conditions to be checked when accessing data.
Corresponding Ada rule:
There are many uses for packages. One use is to define
commonly used constants, while another is to define abstract data types. It is
good to use data hiding for abstract data types, but it is also important to
openly share commonly used constants. When declaring abstract data types in Ada
it is advised that the public view of the data type refers to a private
definition of the data structure.
11.2.
Friends
11.2.1.
Do not use friend declarations
Friend declarations reduce encapsulation, resulting in code
that is harder to maintain.
Corresponding Ada rule:
Ada provides child packages rather than friends. Child
packages do not reduce encapsulation. Child packages allow the creation of
package extensions without compromising or altering the code in the parent
package.
12.
Special member functions
12.1.
Conversions
12.1.1.
Do not declare implicit user defined conversions
A user defined conversions can occur through the use of a
conversion operator or a conversion constructor (a constructor that accepts a
single argument).
A compiler can invoke a single user defined conversion in a
standard conversion sequence, but only if the operator or constructor is
declared without the explicit keyword.
It is better to declare all conversion constructors and
operators explicit.
Corresponding Ada rule:
Ada does not perform implicit conversions.
12.2.
Destructors
12.2.1.
Declare virtual, private, or protected the
destructor of a type used as a base class
If an object will ever be destroyed through a pointer to its
base class, then the destructor in the base class should be virtual.
If the base class destructor is not virtual, then the destructors for derived
classes will not be invoked.
Where an object will not be deleted via a pointer to its
base, then the destructor should be declared with protected or private access.
This will result in a compile error should an attempt be made to delete the
object incorrectly.
Corresponding Ada rule:
Ada controlled types allow user-defined finalization of
objects of the type. Finalization is called when an object goes out of scope.
Ada polymorphism does not require the use of pointers to a
base class.
12.3.
Free store
12.3.1.
Correctly declare overloads for operator new and
delete
operator new and operator delete should work
together. Overloading operator new means that a custom memory management
scheme is in operation for a particular class or program. If a corresponding operator
delete (plain or array) is not provided the memory management scheme is
incomplete.
Additionally, if initialization of the allocated object
fails with an exception, the C++ runtime will try to call an operator
delete with identical parameters as the called operator new, except
for the first parameter. If no such operator delete can be found, the memory will not
be freed. If this operator delete does not actually need to perform any
bookkeeping, one with an empty body should be defined to document this in the
code.
When declared in a class, operator new and operator
delete are implicitly static members; explicitly including the static specifier
in their declarations helps to document this.
Corresponding Ada rule:
When creating a custom storage pool one must override the
abstract type Root_Storage_Pool, including overriding its Allocate and
Deallocate procedures and its Storage_Size function. There is no way to
implement a custom storage pool without implementing all three subprograms.
12.4.
Initializing bases and members
12.4.1.
Do not use the dynamic type of an object unless
the object is fully constructed
Expressions involving:
·
a call to a virtual member function,
·
use of typeid, or
·
a cast to a derived type using dynamic
cast
are said to use the dynamic type of the object.
Special semantics apply when using the dynamic type of an
object while it is being constructed or destructed. Moreover, it is undefined
behavior if the static type of the operand is not (or is not a pointer to) the
constructor’s or destructor’s class or one of its base classes.
In order to avoid misconceptions and potential undefined
behavior, such expressions should not be used while the object is being
constructed or destructed.
Corresponding Ada rule:
Ada has no concept of a dynamic type, only dynamic type
identification. Ada can perform view conversions of a child type to its parent
type, but that does not change the type of an object.
12.4.2.
Ensure that a constructor initializes explicitly
all base classes and non-static data members
A constructor should completely initialize its object.
Explicit initialization reduces the risk of an invalid state after successful
construction. All virtual base classes and direct non-virtual base classes
should be included in the initialization list for the constructor. A copy or
move constructor should initialize each non-static data member in the
initialization list, or if this is not possible then in constructor body. For
other constructors, each non-static data member should be initialized in the
following way, in order of preference:
·
non static data member initializer (NSDMI), or
·
in initialization list, or
·
in constructor body.
For many constructors this means that the body becomes an
empty block.
Corresponding Ada rule:
Ada records, including tagged records, can be defined with
default values for all their record components. The default initialization of a
parent tagged record is applied to the child tagged record so that the child
need not explicitly call parent initialization functions.
12.4.3.
Do not specify both an NSDMI and a member
initialize for the same non-static member
NSDMI stands for ’non static data member initializer’. This
syntax, introduced in the 2011 C++ Language Standard, allows for the
initializer of a member to be specified along with the declaration of the
member in the class body. To avoid confusion as to the value of the initializer
actually used, if a member has an NSDMI then it should not subsequently be
initialized in the member initialization list of a constructor.
Corresponding Ada rule:
Ada does not provide for multiple initialization of an
object.
12.4.4.
Write members in an initialization list in the
order in which they are declared
Regardless of the order of member initializers in a
initialization list, the order of initialization is always:
·
Virtual base classes in depth and left to right
order of the inheritance graph.
·
Direct non-virtual base classes in left to right
order of inheritance list.
·
Non-static member data in order of declaration
in the class definition.
To avoid confusion and possible use of uninitialized data
members, it is recommended that the initialization list matches the actual
initialization order.
Corresponding Ada rule:
Since Ada only allows single inheritance from a base class
there is no issue about which base class is first initialized.
12.4.5.
Use delegating constructors to reduce code
duplication
Delegating constructors can help reduce code duplication by
performing initialization in a single constructor. Using delegating
constructors also removes a potential performance penalty with using an ’init’
method, where initialization for some members occurs twice.
Corresponding Ada rule:
Ada does not provide explicit constructors. Ada does provide
an Initialize procedure for controlled types.
12.5.
Copying and moving class objects
12.5.1.
Define explicitly =default or =delete implicit
special member functions of concrete classes
A compiler may provide some or all of the following special
member functions:
·
Destructor
·
Copy constructor
·
Copy assignment operator
·
Move constructor
·
Move assignment operator
The set of functions implicitly provided depends on the
special member functions that have been declared by the user and also the
special members of base classes and member objects.
The compiler generated versions of these functions perform a
bitwise or shallow copy, which may not be the correct copy semantics for the
class. It is also not clear to clients of the class if these functions can be
used or not.
To resolve this, the functions should be defined with =delete or
=default
thereby fully documenting the class interface.
Note: As this rule is limited to concrete classes,
it is the responsibility of the most derived class to ensure that the object
has correct copy semantics for itself and for its sub-objects.
Corresponding Ada rule:
Tagged types inheriting from Controlled types must define
Initialize, Adjust, and Finalize procedures to handle initialization, copy
semantics, and deletion semantics.
12.5.2.
Define special members =default if the behavior
is equivalent
Corresponding Ada rule:
This rule has no Ada equivalent because Ada prohibits
inheritance from multiple base classes.
12.5.3.
Ensure that a user defined move/copy constructor
only moves/copies base and member objects
The human clients of a class will expect that the copy
constructor can be used to correctly copy an object of class type. Similarly,
they will expect that the move constructor correctly moves an object of class
type.
Similarly, a compiler has explicit permission in the C++
Standard to remove unnecessary copies or moves, on the basis that these
functions have no other side-effects other than to copy or move all bases and
members.
Corresponding Ada rule:
Tagged types inheriting from Controlled types must define
Initialize, Adjust, and Finalize procedures to handle initialization, copy
semantics, and deletion semantics.
12.5.4.
Declare noexcept the move constructor and move
assignment operator
A class provides the Strong Exception Guarantee if after an
exception occurs, the objects maintain their original values.
The move members of a class explicitly change the state of
their argument. Should an exception be thrown after some members have been
moved, then the Strong Exception Guarantee may no longer hold as the from
object has been modified.
It is especially important to use noexcept for types
that are intended to be used with the standard library containers.
If the move constructor for an element type in a container
is not noexcept
then the container will use the copy constructor rather than the move
constructor.
Corresponding Ada rule:
Ada has no equivalent to a move constructor. A move
constructor moves ownership of data from one object to another. If the resource
is accessed through an access value then the access value of the data in the
starting object must be copied to the corresponding access field of the target
object. After the access value is copied the access value in the starting
object must be set to null.
12.5.5.
Correctly reset moved-from handles to resources
in the move constructor
The move constructor moves the ownership of data from one
object to another. Once a resource has been moved to a new object, it is
important that the moved-from object has its handles set to a default value.
This will ensure that the moved-from object will not attempt to destroy
resources that it no longer manages on its destruction.
The most common example of this is to assign nullptr to
pointer members.
Corresponding Ada rule:
Ada has no equivalent to a move constructor. A move
constructor moves ownership of data from one object to another. If the resource
is accessed through an access value then the access value of the data in the
starting object must be copied to the corresponding access field of the target
object. After the access value is copied the access value in the starting
object must be set to null.
12.5.6.
Use and atomic, non-throwing swap operation to
implement the copy and move assignment operators
Implementing the copy assignment operator using a non
throwing swap provides the Strong Exception Guarantee for the operations.
In addition, the implementation of each assignment operator
is simplified without requiring a check for assignment to self.
Corresponding Ada rule:
Use of an atomic swap operation assumes the use of pointers
and not full objects. This assumption frequently fails since Ada does not
require the use of pointers to complex data types.
12.5.7.
Declare assignment operators with the
ref-qualifier &
In the 2003 C++ Language Standard, user declared types
differed from built-in types in that it was possible to have a ’modifiable
rvalue’.
The 2011 C++ Language Standard allows for a function to be
declared with a reference qualifier. Adding & to the function
declaration ensures that the call can only be made on lvalue
objects, as is the case for the built-in operators.
Corresponding Ada rule:
Ada does not allow modifiable rvalues.
12.5.8.
Make the copy assignment operator of an abstract
class protected ore define it =delete
An instance of an abstract class can only exist as a
subobject for a derived type. A public copy assignment operator would allow for
incorrect partial assignments to occur.
The copy assignment operator should be protected, or
alternatively defined =delete if copying is to be prohibited in this
class hierarchy.
Corresponding Ada rule:
Ada does not allow instances of abstract types. Each
instance must be a concrete type derived from the abstract type.
13.
Overloading
13.1.
Overload resolution
13.1.1.
Ensure that all overloads of functions are
visible from where it is called
When a member function is overridden or overloaded in a
derived class, other base class functions of that name will be hidden. A call
to a function from the derived class may therefore result in a different
function being called than if the same call had taken place from the base
class.
To avoid this situation, hidden names should be introduced
into the derived class through a using declaration.
A using declaration for a namespace scope identifier, only
brings into the current scope the prior declarations of this identifier, and
not any declarations subsequently added to the namespace. This too may lead to
unexpected results for calls to overloaded functions.
Corresponding Ada rule:
If the programmer wants the base type version of an overloaded
or overridden subprogram to be called then the object must be passed to the
overloaded subprogram name in the form of a view conversion to the base type.
13.1.2.
If a member of a set of callable functions
includes a universal reference parameter, ensure that one appears in the same
position for all other members
A callable function is one which can be called with the
supplied arguments. In the C++ Language Standard, this is known as the set of
viable functions.
A template parameter declared T&& has special rules
during type deduction depending on the value category of the argument to the
function call. Scott Meyers has named this a ’Universal Reference’.
As a universal reference will deduce perfectly for any type,
overloading them can easily lead to confusion as to which function has been
selected.
Exception:
Standard C++ allows for a member of the viable function set
to be deleted. In such cases, should these functions be called then it will
result in a compiler error.
Corresponding Ada rule:
Ada does not provide an equivalent to universal reference.
The closest Ada comes is an class-wide access type, which can reference any
object in the inheritance hierarchy rooted at the tagged type specified in the
definition of the access type.
13.2.
Overloaded operators
13.2.1.
Do not overload operators with special semantics
Overloaded operators are just functions, so the order of
evaluation of their arguments is unspecified. This is contrary to the special
semantics of the following built-in operators:
·
&& – left to right and potentially
evaluated
·
|| –
left to right and potentially evaluated
·
, –
left to right
Providing user declared versions of these operators may lead
to code that has unexpected behavior and is therefore harder to maintain.
Additionally, overloading the unary & (address of)
operator will result in undefined behavior if the operator is used from a
location in the source where the user provided overload is not visible.
Corresponding Ada rule:
The C++ operators && and || correspond to the Ada
logical expressions “and then” and “or else”. Those logical expressions cannot
be overloaded in Ada. Ada has no “address of” operator. It does have an
attribute ‘Access which cannot be overloaded.
13.2.2.
Ensure that the return type of an overloaded
binary operator matches the built-in counterparts
Built-in binary arithmetic and bitwise operators return a
pure rvalue (which cannot be modified), this should be
mirrored by the overloaded versions of these operators. For this reason the
only acceptable return type is a fundamental or an enumerated type or a class
type with a reference qualified assignment operator.
Built-in equality and relational operators return a boolean
value, and so should the overloaded counterparts.
Corresponding Ada rule:
Custom operators can be defined which return complex values.
For instance:
type Matrix is
array(Natural range 1..10, Natural range 1..10) of float;
type Vector is
array(Natural range 1..10) of float;
function “+” (Left, Right
: Matrix) return Matrix;
function “+” (Left, Right
: Vector) return Vector;
In the case above addition operators are declared for types
Matrix and Vector. In this case the acceptable return type is not a scalar and
need not be a tagged type.
13.2.3.
Declare binary arithmetic and bitwise operators
as non-members
Overloaded binary arithmetic and bitwise operators should be
non-members to allow for operands of different types, e.g. a fundamental type
and a class type, or two unrelated class types.
Corresponding Ada rule:
Overloaded binary arithmetic operators should be declared
within the package that a particular numeric type is declared. Integer or real
types are often defined as part of a larger abstraction such as date and time
utilities. Often achieving the same effect in C++ may take the creation of
several classes.
13.2.4.
When overloading the subscript operator (operator[])
implement both const and non-const versions
A non-const overload of the subscript operator should allow
an object to be modified, i.e. should return a reference to member data. The
const version is there to allow the operator to be invoked on a const object.
Corresponding Ada rule:
There is no subscript operator in Ada. Ada requires the
programmer to specify the scalar subtype used to index an array type. Array
indices in Ada may be indexed by any scalar type (signed integer, modular
integer, enumeration) and the lowest index value may be set to any valid value
of the specified index subtype.
Examples:
Type
Days is (Mon, Tues, Wed, Thu, Fri, Sat, Sun);
Type
Weekly_Sales is array(Days) of float;
This array’s index values start at Mon and end at Sun. Each
array element is a float value.
Type
Mod_Index is mod 10;
Type
Circular_Array is array(Mod_Index) of message;
This array is indexed with a modular type. Modular types
exhibit wrap around arithmetic, allowing this array to easily implement a
circular message buffer.
Subtype
Normalized_Index is Integer range -100..100;
Type
Normal_Distribution is array(Normalized_Index) of Count;
This array can be used to count the frequency of some event
plotted to a normal curve.
Ada does allow indexing operators to be defined for tagged
types. This indexing ability in Ada helps greatly in the implementation of
indexable container types such as maps and sets. The container type can be
designated to employ either constant indexing or variable indexing. The aspect
of the type declaring either constant indexing or variable indexing must
indicate one or more functions taking two parameters, one of which is an
instance of the container type or a reference to a container instance.
Ada user defined indexing is not used for array types, which
are not tagged types.
13.2.5.
Implement a minimal set of operators and use
them to implement all other related operators
In order to limit duplication of code and associated
maintenance overheads, certain operators can be implemented in terms of other
operators.
Corresponding Ada rule:
The Ada 2012 Language Reference Manual states that one is
not allowed to overload “/=” directly, however overloading “=” implicitly
overloads “/=”, since not equals is simply the complement of the “=” function.
14.
Templates
14.1.
Template declarations
14.1.1.
Using variadic templates rather than ellipsis
Use of the ellipsis notation ... to indicate an
unspecified number of arguments should be avoided. Variadic templates offer a
type-safe alternative.
Corresponding Ada rule:
The Ada equivalent to a template is called a generic. A
programmer can define a generic subprogram or a generic package. Each generic
unit definition includes a set of generic parameters followed by a normal
subprogram or package specification.
There is no need for an equivalent to C++ variadic parameter
lists.
Generic units are explained in section 12 of the Ada 2012
Language Reference Manual.
Generic parameters can be:
·
Formal objects
·
Formal private types and derived types
·
Formal scalar types
·
Formal array types
·
Formal access types
·
Formal subprograms
·
Formal packages
Example of a generic package:
generic
Size : Positive;
type Item is private;
package Stack is
procedure Push(E : in Item);
procedure Pop (E : out Item);
Overflow, Underflow : exception;
end Stack;
package body Stack is
type Table is array (Positive range <>) of Item;
Space : Table(1 .. Size);
Index : Natural := 0;
procedure Push(E : in Item) is
begin
if Index >= Size then
raise Overflow;
end if;
Index := Index + 1;
Space(Index) := E;
end Push;
procedure Pop(E : out Item) is
begin
if Index = 0 then
raise Underflow;
end if;
E := Space(Index);
Index := Index - 1;
end Pop;
end Stack;
This example implements a generic stack. The package
specification begins with the reserved
word “generic” followed by the list of generic parameters. In this case the
parameter Size is a generic formal object of the subtype Positive, and the type
Item is any non-limited type, private or public.
The package specification only contains two procedures, Push
and Pop, plus the definition of two exceptions, Overflow and Underflow. This
package implements a singleton stack, therefore no stack type is publicly
exposed.
The package body contains the implementation of the Push and
Pop procedures plus the definition of the singleton stack object.
14.2.
Template instantiation and specialization
14.2.1.
Declare template specializations in the same
file as the primary template they specialize
Partial and explicit specializations of function and class
templates should be declared with the primary template.
This will ensure that implicit specializations will only
occur when there is no explicit declaration available.
Corresponding Ada rule:
Ada generics do not undergo specialization, only
instantiation. Examples of instantiation of the generic singleton stack package
shown above are:
Package Stack_Int is new Stack(Size => 200, Item =>
Integer);
Package Stack_Bool is new Stack(100, Boolean);
Note that the generic parameters can be passed by name or by
position. After the instantiations above the package procedures can be called
as follows:
Stack_Int.Push(N);
Stack_Bool.Push(True);
Note that Stack_Int is a singleton stack containing Integer
values while Stack_Bool is a distinct and separate singleton stack containing
Boolean values.
14.2.2.
Do not explicitly specialize a function template
that is overloaded with other templates
Overload resolution does not take into account explicit
specializations of function templates. Only after overload resolution has
chosen a function template will any explicit specializations be considered.
Corresponding Ada rule:
Ada generic formal parameters are much richer and more
explicit than C++ template parameters rendering specialization unnecessary.
Use of a formal package parameter in a generic parameter
list does not necessarily cause overloading of subprograms.
14.2.3.
Declare extern an explicitly instantiated
template
Declaring the template with extern will disable implicit
instantiation of the template when it is used in other translation units,
saving time and reducing compile time dependencies.
Corresponding Ada rule:
Ada generics are never implicitly instantiated.
15.
Exception handling
15.1.
Throwing an exception
15.1.1.
Only use instances of std::exception for
exceptions
Exceptions pass information up the call stack to a point
where error handling can be performed. If an object of class type is thrown,
the class type itself serves to document the cause of an exception.
Only types that inherit from std::exception, should be
thrown.
Corresponding Ada rule:
Only instances of the pre-defined type Exception can be
raised. Each instance of exception that is raised can be accompanied by
exception information in the form of a string.
15.2.
Constructors and destructors
15.2.1.
Do not throw an exception from a destructor
The 2011 C++ Language Standard states that unless a user
provided destructor has an explicit exception specification, one will be added
implicitly, matching the one that an implicit destructor for the type would
have received.
Furthermore when an exception is thrown, stack unwinding
will call the destructors of all objects with automatic storage duration still
in scope up to the location where the exception is eventually caught.
The program will immediately terminate should another
exception be thrown from a destructor of one of these objects.
Corresponding Ada rule:
Ada has no explicit constructors or destructors. Instead,
the procedures Initialize and Finalize are called. When a programmer customizes
Initialize and Finalize care must be taken not to raise exceptions.
15.3.
Handling an exception
15.3.1.
Do not access non-static members from a catch
handler of constructor/destructor function try block
When a constructor or a destructor has a function try block,
accessing a non-static member from an associated exception handler will result
in undefined behavior.
Corresponding Ada rule:
Syntax
exception_choice ::= exception_name | others
Example of an exception handler:
begin
Open(File, In_File, "input.txt");
exception
when E : Name_Error =>
Put("Cannot open input file : ");
Put_Line(Exception_Message(E));
raise;
end;
15.3.2.
Ensure that a program does not result in a call
to std::terminate
The path of an exception should be logical and well defined.
Throwing an exception that is never subsequently caught, or attempting to
rethrow when an exception is not being handled is an indicator of a problem
with the design.
Corresponding Ada rule:
Ada exceptions should be handled wherever possible. It is
considered bad design to use exceptions as a normal path to program
termination.
16.
Preprocessing
16.1.
Source file inclusion
16.1.1.
Use the preprocessor only for implementing
include guards, and including header files with include guards
The preprocessor should only be used for including header
files into other headers or the main source file, in order to form a translation
unit. In particular only the following include directive forms should be used:
·
#include <xyz>
·
#include "xyz"
Corresponding Ada rule:
Ada does not require the use of a preprocessor. File
dependencies are established through the use of a dependency clause (with
clause).
Dependency clauses do not require any equivalent of include
guards. Dependency clauses cannot create macros.
16.1.2.
Do not include a path specifier in filenames
supplied in #include directives
Hardcoding the path to a header file in a #include directive
may necessitate changes to source code when it is reused or ported.
Alternatively, the directory containing the header file
should be passed to the compiler on command line (e.g. –I or /i option).
Corresponding Ada rule:
Ada dependency clauses do not contain filenames. They
contain the names of compilation units.
16.1.3.
Match the filename in a #include directive to
the one on the filesystem
Some operating systems have case insensitive filesystems.
Code initially developed on such a system may not compile successfully when
ported to a case sensitive filesystem.
Corresponding Ada rule:
Ada dependency clauses do not contain filenames. They
contain the names of compilation units.
16.1.4.
Use <> brackets for system and standard
library headers. Use quotes for all other headers.
It is common practice that #include <...> is
used for compiler provided headers, and #include "..." for
user provided files.
Adhering to this guideline therefore helps with the
understandability and maintainability of the code.
Corresponding Ada rule:
Ada dependency clauses do not contain filenames. They
contain the names of compilation units.
16.1.5.
Include directly the minimum number of headers
required for compilation
Presence of spurious include directives can considerably
slow down compilation of a large code base. When a source file is refactored,
the list of included headers should be reviewed, to remove include directives
which are no longer needed.
Doing so may also offer an opportunity to delete from code
repository source files that are no longer used in the project, therefore
reducing the level of technical debt.
Corresponding Ada rule:
It is a good practice to minimize the dependency list for an
Ada compilation unit. Minimized dependencies support code readability and
maintenance.
17.
Standard library
17.1.
General
17.1.1.
Do not use std::vector<bool>
The std::vector<bool> specialization
does not conform to the requirements of a container and does not work as
expected in all STL algorithms.
In particular &v[0] does not return a contiguous array
of elements as it does for other vector types. Additionally, the C++ Language
Standard guarantees that different elements of an STL container can safely be
modified concurrently, except for a container of std::vector<bool> type.
Corresponding Ada rule:
There is no corresponding prohibition for Ada standard
libraries.
17.2.
The C standard library
17.2.1.
Wrap use of the C Standard Library
The C11 standard library, which is included in the C++
standard library, leaves the handling of concerns relating to security and
concurrency up to the developer.
Therefore, if the C standard library is to be used, it
should be wrapped, with the wrappers ensuring that undefined behavior and data
races will not occur.
Corresponding Ada rule:
The C standard library is not included in the Ada 2012
standard library.
17.3.
General utilities library
17.3.1.
Do not use std::move on objects declared with
const or const & type
An object with const or const & type will never
actually be moved as a result of calling std::move.
Corresponding Ada rule:
There is no operation equivalent to C++ std::move defined in
the Ada 2012 standard.
17.3.2.
Use std::forward to forward universal references
The std::forward function takes the value category of
universal reference parameters into account when passing arguments through to
callees.
When passing a non universal reference argument std::move should
be used.
Note: As auto is implemented with
argument deduction rules, an object declared with auto && is
also a universal reference for the purposes of this rule.
Corresponding Ada rule:
There are no Ada library components needed to pass parameter
values to subprograms.
17.3.3.
Do not subsequently use the argument to
std::forward
Depending on the value category of arguments used in the
call of the function, std::forward may or may not result in a move of
the parameter.
When the value category of the parameter is an lvalue, then modifications to the parameter will affect
the argument of the caller. In the case of an rvalue, the
value should be considered as being indeterminate after the call to std::forward
(See Rule 8.4.1: ”Do not access an invalid
object or an object with indeterminate value”).
Corresponding Ada rule:
There are no Ada library components needed to pass parameter
values to subprograms.
17.3.4.
Do not create smart pointers of an array type
Memory allocated with array new must be deallocated with
array delete. A smart pointer that refers to an array object must have this
information passed in when the object is created. A consequence of this is that
it is not possible to construct such a smart pointer using std::make
shared.
A std::array or std::vector can be used in place of the raw array
type. The usage and performance will be very similar but will not have the
additional complexity required when deallocating the array object.
Corresponding Ada rule:
There is not special complexity dealing with allocating or
deallocating arrays in Ada.
17.3.5.
Do not create an rvalue reference of std::array
The std::array class is a wrapper for a C style array.
The cost of moving std::array is linear with each element of the array being
moved. In most cases, passing the array by & or const
& will provide the required semantics without this cost.
Corresponding Ada rule:
There is no standard wrapper for Ada arrays. The cost of
assigning an array or an array slice is equivalent to a memory copy in Ada.
17.4.
Containers library
17.4.1.
Use const container calls when result is
immediately converted to a const iterator
The 2011 C++ Language Standard introduced named accessors
for returning const iterators. Using these members removes an implicit
conversion from iterator to const iterator.
Another benefit is that the declaration of the iterator
object can then be changed to use auto without the danger of affecting program
semantics.
Corresponding Ada rule:
Ada container packages define cursors to traverse a container
objects. The cursor type is a private type defined in each standard container
package.
17.4.2.
Use API calls that construct objects in place
The 2011 C++ Language Standard allows for perfect
forwarding. This allows for the arguments to a constructor to be passed through
an API and therefore allowing for the final object to be constructed directly
where it is intended to be used.
Corresponding Ada rule:
Parameter passing needs no special passing helpers.
17.5.
Algorithms library
17.5.1.
Do not ignore the result of std::remove,
std::remove_if or std::unique
The mutating algorithms std::remove, std::remove
if and both overloads of std::unique operate by swapping or moving elements
of the range they are operating over.
On completion, they return an iterator to the last valid element.
In the majority of cases the correct behavior is to use this result as the
first operand in a call to std::erase.
Corresponding Ada rule:
While Ada does not provide an equivalent to these functions
as separate library components, the concept of ignoring the return value of a
function is foreign to Ada. All function return values must be used as an
rvalue to some expression.
18.
Concurrency
18.1.
General
18.1.1.
Do not use platform specific multi-threading
facilities
Rather than using platform-specific facilities, the C++
standard library should be used as it is platform independent.
Corresponding Ada rule:
Ada tasking is platform independent. There are no platform
specific multi-threading facilities written with an Ada API.
18.2.
Threads
18.2.1.
Use high_intergrity::thread in place of
std::thread
The destructor of std::thread will call std::terminate
if the thread owned by the class is still joinable. By using a wrapper
class a default behavior can be provided.
Corresponding Ada rule:
Ada tasks do not have an explicit join command nor an
explicit detach command.
Ada tasks do have a dependency hierarchy. Each task (other
than the environment task) depends on one or more masters. A task is said to be
completed when the execution of the corresponding task body is completed. A
task is said to be terminated when any finalization of the task body has been
performed. The first step in finalizing a master is to wait for the termination
of any tasks dependent upon the master. The task executing the master is
blocked until all dependents have terminated. Any remaining finalization is
then performed and the master is left.
18.2.2.
Synchronize access to data shared between
threads using a single lock
Using the same lock when accessing shared data makes it
easier to verify the absence of problematic race conditions.
To help achieve this goal, access to data should be
encapsulated such that it is not possible to read or write to the variable
without acquiring the appropriate lock. This will also help limit the amount of
code executed in the scope of the lock.
Note: Data may be referenced by more than one
variable, therefore this requirement applies to the complete set of variables
that could refer to the data.
Special attention needs to be made for const objects. The
standard library expects operations on const objects to be thread-safe. Failing
to ensure that this expectation is fulfilled may lead to problematic data races
and undefined behavior. Therefore, operations on const objects of user defined
types should consist of either reads entirely or internally synchronized
writes.
Corresponding Ada rule:
Ada provides two forms of synchronization for passing data
between tasks.
The Rendezvous mechanism provides a means to synchronously
pass data directly between two tasks.
The Protected Object provides a way to pass data between
tasks through a shared buffer. Protected objects are allowed to have a
combination of three kinds of methods.
Protected procedures allow data in the protected object to
be modified or updated unconditionally. Protected procedures implicitly
manipulate a read/write lock on the protected object. Protected entries allow
data in the protected object to be modified or updated conditionally.
Protected entries
have a boundary condition which must be satisfied. When the boundary condition
evaluates to False the protected entry is blocked and the calling task is
suspended and placed in an entry queue. Protected entries automatically
manipulate a read/write lock on the protected object. Protected functions are
only allowed read access to the protected object. Protected functions may not
modify or update the state of the protected object. Protected functions
automatically manipulate a shared read lock on the protected object allowing
multiple tasks to read from the protected object simultaneously.
18.2.3.
Do not share volatile data between threads
Declaring a variable with the volatile keyword does not
provide any of the required synchronization guarantees:
·
Atomicity
·
Visibility
·
Ordering
Use mutex locks or ordered atomic variables, to safely
communicate between threads and to prevent the compiler from optimizing the
code incorrectly.
Corresponding Ada rule:
Use the Rendevous or protected objects to communicate
between tasks.
18.2.4.
Use the std::call_once rather than the
Double_Checked Locking pattern
The Double-Checked Locking pattern can be used to correctly
synchronize initializations.
However, the C++ standard library provides std::call_once
which allow for a cleaner implementation.
Initialization of a local object with static storage
duration is guaranteed by the C++ Language Standard to be reentrant.
However this conflicts with Rule 3.3.1: ”Do not use variables with static storage duration”,
which takes precedence.
Corresponding Ada rule:
Allow the Ada tasking mechanisms to perform implicit lock
manipulations.
18.3.
Mutual exclusion
18.3.1.
Within the scope of a lock, ensure that no
static path results in a lock of the same mutex
It is undefined behavior if a thread tries to lock a std::mutex
it already owns, this should therefore be avoided.
Corresponding Ada rule:
Allow the Ada tasking mechanisms to perform implicit lock
manipulations.
18.3.2.
Ensure that order of nesting of locks in a
project forms a DAG
Mutex locks are a common causes of deadlocks. Multiple
threads trying to acquire the same lock but in a different order may end up
blocking each other.
When each lock operation is treated as a vertex, two
consecutive vertices with no intervening lock operation in the source code are
considered to be connected by a directed edge. The resulting graph should have
no cycles, i.e. it should be a Directed Acyclic Graph (DAG).
Corresponding Ada rule:
Allow the Ada tasking mechanisms to perform implicit lock
manipulations.
18.3.3.
Do not use stfd::recursive_mutex
Use of std::recursive mutex is indicative of bad design:
Some functionality is expecting the state to be consistent which may not be a
correct assumption since the mutex protecting a resource is already locked.
Corresponding Ada rule:
Allow the Ada tasking mechanisms to perform implicit lock
manipulations.
18.3.4.
Only use std::unique_lock when std::lock_guard
cannot be used
The std::unique lock type provides additional features
not available in std::lock guard. There is an additional cost when using std::unique
lock and so it should only be used if the additional functionality is
required.
Corresponding Ada rule:
Allow the Ada tasking mechanisms to perform implicit lock
manipulations.
18.3.5.
Do not use the members of std::mutex directly
A mutex object should only be managed by the std::lock
guard or std::unique lock object that owns it.
18.3.6.
Do not use relaxed atomics
Using non-sequentially consistent memory ordering for
atomics allows the CPU to reorder memory operations resulting in a lack of
total ordering of events across threads. This makes it extremely difficult to
reason about the correctness of the code.
Corresponding Ada rule:
Allow the Ada tasking mechanisms to perform implicit lock
manipulations.
18.4.
Condition variables
18.4.1.
Do not use std::condition_variable_any on a
std::mutex
When using std::condition variable any, there is potential
for additional costs in terms of size, performance or operating system
resources, because it is more general than std::condition variable.
std::condition variable works with std::unique
lock<std::mutex>, while std::condition variable any can operate
on any objects that have lock and unlock member functions.
Corresponding Ada rule:
Allow the Ada tasking mechanisms to perform implicit lock
manipulations.