forked from Alepha/Alepha
224 lines
13 KiB
Plaintext
224 lines
13 KiB
Plaintext
Alepha library C++ style Guide
|
|
|
|
This guide shall follow the format of "Rule", then "Examples", then "Reasoning", then "Exceptions".
|
|
Some of these clauses are omitted. No portion of the C++ language itself is off-limits, unless explicit mention
|
|
is made banning that feature -- which is rare. The Alepha C++ philosophy sees C++ as a whole langauge and
|
|
does not seek to provide a "safe subset". The few banned features tend to be those which are either deprecated
|
|
or generally unused.
|
|
|
|
Note that this "style guide" covers the style of the code in the sense of how to write the code itself,
|
|
not the rendering/format. For that guide, please see the "Format" file.
|
|
|
|
General Coding and Language Rules:
|
|
|
|
0. The Alepha C++ library shall only contain code written for ISO/ANSI C++23, compilation with any other language
|
|
standard is incidental. Modifying a component of Alepha to compile with a non-compliant compiler (while still
|
|
remaining compliant to the standard) is sometimes necessary. Deprecated features should be avoided, wherever
|
|
possible. C++98 code (with precious few exceptions) will work unchanged in C++23.
|
|
|
|
Reason: Alepha is a modern C++ library technology.
|
|
|
|
Exceptions: None. ("#pragma once" is used, as if it were part of the standard. This is widely supported,
|
|
and when C++20's modules become more prevalent, the library will move to those.)
|
|
|
|
Note: Even among all "compliant" compilers, some quirks prevent perfect, and ideal C++23 code.
|
|
Even in such cases, the code which does compile is still expected to be C++23 compliant.
|
|
|
|
|
|
1. All names and user-defined tokens shall avoid C89, C99, C11, C++98, C++03, C++11, C++14, C++17, C++20, and C++23
|
|
implementation reserved names, in all contexts.
|
|
|
|
- Names shall not begin with an underscore.
|
|
- Names shall not contain two consecutive underscores.
|
|
- User defined names include: functions, macros, classes, constants, and header guards.
|
|
- Be mindful of C++'s little known reserved words: compl or not and xor bit_and bit_or bit_xor
|
|
|
|
Reason: Alepha is a portable C++ library, and thus must be able to compile with any C++23 compiler. The presence
|
|
of implementation reserved names in code can cause potential issues. Even in a context where the name wouldn't
|
|
be reserved, consider it to be reserved.
|
|
|
|
Exceptions: NONE
|
|
|
|
|
|
2. `#define`s of a keyword to anything else are prohibited.
|
|
|
|
Reason: #defining a keyword yields non-portable behavior. On a fully conforming C++23 compiler, all features
|
|
are present, and useful. Not all compilers behave in the same way, when it comes to violating the standard.
|
|
|
|
Exceptions: None. For a while, it was necessary to supplement some compilers with `#define override` or
|
|
similar hacks, for not-yet-supported C++11 features. By this point in time, the new features in C++20 and
|
|
C++23 cannot be so easily "hacked in".
|
|
|
|
|
|
3. No C++23 core-language features shall be proscribed from use, with the exception of avoiding the following
|
|
idioms or features (some are banned outright):
|
|
|
|
- digraphs are forbidden (Trigraphs were removed in 17)
|
|
- prefer `new` over `malloc` (except in implementing `::operator new`)
|
|
- prefer to use `()` instead of `( void )` in empty parameter functions
|
|
- avoid `reinterpret_cast` (except in specific hardware-oriented contexts, or where otherwise necessary)
|
|
- rarely use "old C style" casts. (E.g.: `(char *) variable`) -- this is widely known to be dangerous
|
|
- never throw from a destructor -- this is not expected by any C++23 libraries
|
|
- Avoid the use of general `mutable` members. No good can come from changing values behind const, even locks.
|
|
TOCTIOU bugs abound with observers of threadsafe mechanisms. Properly written Atomics primitives still need
|
|
to be members of classes as mutable.
|
|
|
|
Reason: C++23 is a cohesive, well-reasoned language with many tightly-integrated features. Circumscription of
|
|
a feature is ill-advised. The above prohibitions are mostly encouraged by choices of the standards committee
|
|
over the past 20 or more years. Reinterpret cast, in particular, represents a standards-sanctioned form of
|
|
platform specific behavior. Digraphs and trigraphs were a compatibility feature required by the standard, but
|
|
are bizarre and unintuitive -- many compilers disabled them unless in strict compatibility modes, and they are
|
|
removed in C++23.
|
|
|
|
Exceptions: Malloc can be used in circumstances where that allocator is necessary. "( void )" parameter lists are
|
|
usable in specific 'meant to be included in C' headers -- these must be in their own directory. C style casts
|
|
are dangerous, in general, but '(float) 4 / 2' is more readable than: 'static_cast< float >( 4 ) /
|
|
static_cast< float >( 2 )' or 'std::divide< float >{}( 4, 2 )' -- prefer function-style casting if possible though.
|
|
Never cast with typedefs, or pointers. Destructor throwing is banned, because no C++ STL library can recover
|
|
from it, nor can the exception system itself. Reinterpret_cast has platform dependent behavior, and should
|
|
only be in platform-dependent code. Digraphs should only exist in obfuscation contests. Assume that
|
|
destructors that throw always cause undefined behavior.
|
|
|
|
|
|
4. No non-portable, undefined, implementation defined, or optional behaviors, functions and features shall be
|
|
placed into any non-detail portions of the Alepha library. Detail portions shall push implementation-specifics
|
|
into isolated files. Only the "Alepha::Windows", "Alepha::Posix", or similar namespaces shall contain any
|
|
non-portable constructs.
|
|
|
|
Reasons: Portability is a top concern for the Alepha library.
|
|
|
|
Exceptions: Alepha has namespaces dedicated to specific platforms and systems -- those namespaces are suitable
|
|
for non-portable code, but no core library portions may rely upon those. (Code must compile with a C++11
|
|
compiler and be platform agnostic.) For non-portable namespaces, the code should remain compiler agnostic,
|
|
where possible. Always use macro-guards to prevent accidental compilation of files intended for only one compiler.
|
|
|
|
|
|
5. Avoid "pass-by-non-const-reference" for "out-params". Prefer tuples, pairs and aggregate types for multiple
|
|
return values, and use exceptions to signal failures.
|
|
|
|
Reason: "push-to-argument" style was useful in the 1990s when compilers were very bad at optimizing the
|
|
class return path. In a post-C++11 world, with both NRV and move semantics, this caveat is no longer pertinent.
|
|
Further, this out-parameter style was necessary when exceptions were avoided, as returning an aggregate is
|
|
not conducive to error-checking. Alepha fully embraces the C++ exceptions mindset. Exceptions exist in the
|
|
language to signal out-of-band errors -- return codes are to be avodied.
|
|
|
|
Exceptions: Sometimes arguments have to be passed to be modified, like 'operator <<' on std::ostream. The
|
|
'operator >>' std::istream operators are also examples of this, but technically fall under the "out-params" rule.
|
|
Historically these iostream constructs exist, and we shall still support the iostreams idiom -- it's part of
|
|
the standard.
|
|
|
|
|
|
6. Manage all resources with objects and RAII. Prefer to manage only a single resource per RAII class. Do not
|
|
ever allow unshepherded or unowned resources to exist. Directly callling "new" is strongly discouraged.
|
|
|
|
Reason: It's not just a good idea -- all of C++23 is built around the RAII idiom. It's necessary for exception
|
|
safety, which is a core part of the Alepha philosophy. An RAII class which has to manage several resources
|
|
often represents a design bug. Some classes (like ref-counting-pointer constructs) may need to allocate some
|
|
resources to manage another resource -- this is unavoidable, but the other resources are management metadata.
|
|
|
|
Exceptions: None, whatsoever.
|
|
|
|
|
|
7. Unless a class is intended to directly manage a resource, it ought to have a blank destructor, in the
|
|
ideal situation. Prefer `AutoRAII` as the basis of a class designed to use resource management, in the general
|
|
case.
|
|
|
|
Reason: RAII is most effective when each class that actually manages a resource only manages a single resource,
|
|
and concept abstraction classes don't have to have any explicit resource management.
|
|
|
|
Exceptions: Sometimes a destructor of a class may need to call one of its own functions, like a "cancel"
|
|
function. A network request, or a thread, for example. This is not entirely an exception, since that class
|
|
models that concept as a kind of resource. Conversely, those "cancel" functions are merely an exposition of
|
|
the dtor functionality for early voluntary cleanup.
|
|
|
|
|
|
8. Naked `delete` and other resource release calls outside of destructors are banned. Naked `new` and other
|
|
resource allocation calls that are assigned to raw unmanaged C types, outside of constructors or constructor
|
|
parameters are banned. This rule works hand-in-hand with the previous three rules. Prefer scoped_ptr,
|
|
unique_ptr, shared_ptr, weak_ptr, AutoRAII, vector, list, set, map, and other types over rolling your own
|
|
resource management types, where possible. Calls to 'new' and 'delete' are bad code smells. Use of raw
|
|
primitive pointers is also such a code smell. Consider using `single_ptr` or a native reference when working
|
|
with items held at a distance. Alepha::single_ptr is only constructible from an owned resource, or another
|
|
single_ptr.
|
|
|
|
Reason: C++ RAII is the only safe way to manage resources. By limiting the resource release code to only
|
|
exist in dtors, it limits the scope of code needed to audit for leak-safety. Even RAII classes should only
|
|
handle resource release through the dtor path. Resource release outside of this path should be viewed as a
|
|
glaring bug. Resource acquisition outside of a ctor, or an argument to a ctor should likewise be seen as a bug.
|
|
Raw C types are unsafe for resource management in the face of changing resources.
|
|
|
|
Exceptions: None. Although passing a lambda calling a release function to an AutoRAII object is technically
|
|
an exception, it should be thought of as writing an inline dtor. Note that std::make_unique and
|
|
std::make_shared are suitable replacements for new in almost all situations.
|
|
|
|
|
|
9. Avoid the use of #ifdef/else/endif for conditional compilation of code -- prefer to include headers with
|
|
well defined code for these purposes.
|
|
|
|
Reason: Conditional compilation makes code multiply architecturally dependent not independent.
|
|
|
|
Exceptions: Some small blocks of code (particularly preprocessor control macros) can be if-else-endif for
|
|
small specific variances.
|
|
|
|
|
|
|
|
|
|
Basic Environment Rules:
|
|
|
|
0. Alepha shall compile on any C++23 compliant compilers. At present gcc-13 is a supported minimum,
|
|
and is also the "reference compiler".
|
|
|
|
Exception: Compiler specific code in detail sections need only work with that compiler. Platform specific code,
|
|
in Alepha::<Platform> namespaces need only work on that target platform, but should be portable across compilers
|
|
for that platform. Alepha::Posix sections should try to also work in Cygwin, where possible.
|
|
|
|
1. Header files shall be named with a trailing ".h". Headers shall be entirely compatible with C++.
|
|
|
|
2. Header files which are safe to include from "C" language parsers shall be organized into a specific
|
|
"c_interface" subdirectory of the appropriate domain. Such headers are rarely expected.
|
|
|
|
|
|
Basic Naming Rules:
|
|
|
|
0. Use good judgement in naming things. This cannot be codified as a set of mechanical rules.
|
|
|
|
1. Follow this general naming style (there's some room for flexibility within):
|
|
|
|
* ClassesAndTypedefsLikeThis
|
|
* PublicNamespaceAlsoLikeThis
|
|
* functionsAndVariablesLikeThis
|
|
* ALEPHA_PATH_TO_MACROS_GET_NAMED_IN_VERY_LONG_STYLE_AND_OBNOXIOUS_WITH_CAPS_AND_UNDERSCORES
|
|
* meta_programming_things_like_this
|
|
* TCP_IP_Socket or connectToTCP_IP_Socket-- If the entity name contains abbreviations, separate the abbreviation by `_`,
|
|
don't case flatten things like TCP to Tcp.
|
|
* Follow STL and related C++ Standards names, where appropriate (`push_back` not `pushBack`).
|
|
|
|
2. Follow the general module-namespace technique defined in the NAMESPACES document.
|
|
|
|
3. Name the private module namespace within files as ` ::detail:: FileName_m`, such that `FileName.h`
|
|
provides the namespace. This provides a simple transform:
|
|
Alepha/IOStream/String.h -> Alepha::IOStream::String_m
|
|
|
|
4. General file naming is case-sensitive. Every major modern operating system supports a case-sensitive
|
|
filesystem. Use it. Windows' default filesystem is case-preserving. This case-preserving property
|
|
should suffice for most file naming situations; however, if `foo.h` and `Foo.h` both exist, it might
|
|
cause a problem. That problem is more easily remedied by using a case-sensitive filesystem than by
|
|
putting an onus for name mangling onto a project.
|
|
|
|
5. Name files after the primary component in them, if the file makes a single component available.
|
|
|
|
Example: `class Alepha::Foobar::Quux` should be defined in `Alepha/Foobar/Quux.h`, if defined in a single
|
|
file. The full public name of that class would be `Alepha::Foobar::exports::Quux_m::Quux`. The private
|
|
name can be anything, of course, but would typically be `Alepha::Foobar::detail::Quux_m::exports::Quux`.
|
|
|
|
6. Name files which provide families of facilities without leading capitals. Those names shouldn't be
|
|
confused for classes.
|
|
|
|
7. Name functions with a verb, where appropriate. Don't name observers with a `get` verb.
|
|
|
|
8. Avoid names with `do`, `run`, `compute`, or `execute` in them for functions. Remember functions
|
|
`do`, they aren't things.
|
|
|
|
9. Avoid names with `-er`, `Manager`, `Owner`, or `Holder` in them for classes. Remember that
|
|
classes don't `do`, they're not functions.
|