1
0
forked from Alepha/Alepha
Files
Alepha/docs/Style
ADAM David Alan Martin 6d60639b1c Move some docs to a docs dir.
This lets me have a dir named `Format`, which I want.
2024-04-04 18:12:54 -04:00

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.