Split Notation
Identifier naming convention for object-oriented software development
Developed by Jeremy Kelly
Version 5.0
This page includes a two-column print style. For best results, print in landscape, apply narrow margins, change the Scale setting in your browser’s print options to 70%, and enable background graphics. Firefox and Chrome each have their own set of printing bugs, but one of them usually works.
Contents
- Introduction
- Separating concerns
- Other benefits
- The Notation
- Prefix reference
- Design notes
- Scope prefixes and types
- Scope prefixes and overrides
- Macro parameters
- Return values
- Cleanup obligations
Introduction
Split Notation is an identifier naming convention for object-oriented software development. It was designed for use with C++ and C#, but is largely applicable to other object-oriented languages. Its purpose is to:
- Make identifiers more descriptive, easier to generate, and easier to read by separating low-level technical details from high-level concepts;
- Avoid name collisions;
- Avoid common programming mistakes by marking language features that require special attention.
It was previously known as ‘Iowan Notation’.
Separating concerns
Software development requires careful management of details from varied domains. Business concerns must be modeled, computing resources must be usefully abstracted, and the programming language — with its particular and sometimes perverse way of representing or transforming data — must be correctly applied.
Names that are confusing or incomplete are likely to produce errors, so variable names, function names, and other identifiers must document these details while remaining concise and legible. Identifier generation should also be easy, yet deterministic, so that different developers can hope to produce or predict the same name for a given element.
Parts of this problem are too difficult to solve in a general way, particularly business concerns and platform details, which vary too much to be categorized from afar. Language features are well-suited to categorization, however, because they are clearly defined and relatively few in number.
Split Notation honors this distinction by enforcing a separation of concerns within identifiers. It labels important language features with a range of one-character prefixes, leaving the identifier ‘root’ to document business concerns and other high-level concepts.
Consider an application with a custom stream type. The ‘stream’ concept might be represented by the root Stm
. Throughout the program, streams could be used and referenced in many different ways. Applying some of the more common prefixes:
Identifier | Referent |
tStm |
Stream type |
oStm |
Local stream instance |
gpStm |
Global pointer to a stream instance |
esStm |
Class-private static stream instance |
arStm |
Function parameter passing a non-const
reference to a stream instance
|
The prefixes allow a single root to produce a range of identifiers that are concise and descriptive, yet obviously related. They also prevent name collisions between these identifiers, and they do this without polluting the root with noisy modifiers like ‘My’, ‘Type’, or ‘Param’. The result is semantically dense but highly legible code.
Other benefits
Split Notation also helps developers by marking elements with special or unusual properties:
- References, static objects, and union members are marked as a reminder that changes to such objects affect other objects or scopes;
- Virtual functions are marked to show that their behavior may change when invoked by subclasses;
- Macros are marked to warn against side-effects from duplicated parameter expressions.
Note that Split Notation does not document specific types. This is because:
- Compilers for languages like C++ and C# already flag most type safety violations;
- Types sometimes change during the course of development, and many such changes (replacing a number type with a larger equivalent, or replacing a class with another that implements the same interface) can be made to well-written code without detailed review. Changing the identifier in these cases would create unnecessary work.
Though types are not explicitly documented, the identifier root strongly hints at the object’s type by describing its role.
The Notation
In Split Notation, identifiers begin with zero or more lowercase prefix characters, with at most one prefix drawn from each of the following groups:
Non-static class-public objects and functions meet no criterion, and their identifiers are not prefixed. Other identifiers are prefixed at least once. Only qualities inherent to the identified object are labeled. Pointers to pointers, for instance, are prefixed with one p, not two. When multiple prefixes apply, they are affixed in the order specified above, with the exception of z, which may be placed anywhere within the prefix.
The identifier root follows the prefixes; it contains one or more words describing the business concern or other high-level concept that is being referenced. The first letter of every word within the root is capitalized, as in ‘CamelCase’:
string oNameLast;
It is acceptable and occasionally desirable to omit the root, producing an identifier of prefixes only, as in the loop index below:
for (int o = 0; o < eFlds.Ct(); ++o)
cout << eFlds[o].Name() << endl;
Prefix reference
z (no criterion) |
The z prefix has no set meaning. It may be used to resolve name collisions with reserved words or third-party code, or for any other reason. |
---|---|
t Type |
The t prefix applies to user-defined types and typedefs. It allows the same root to be shared by a type and an instance of that type:
It does not apply to template type parameters, for which the x prefix is used instead. |
x Template parameter |
The x prefix applies to template parameters, both type and non-type. By distinguishing template parameters, it clarifies template design:
|
f Interface |
In languages that support them, the f prefix applies to interfaces. It allows the same root to be shared by an interface and a type or instance implementing that interface. |
m Macro |
The m prefix applies to macros. It warns against side-effects from duplicated parameter expressions, and other preprocessor oddities:
|
n Namespace |
The n prefix applies to namespaces. It helps distinguish namespaces from containing types:
|
g Global element |
The g prefix applies to global objects and functions, and optionally, global types and interfaces. It also applies to global enumerators, which are essentially global constants:
In C#, the prefix can be omitted from enumerators, since these are qualified with the type name when used. |
y Element with internal linkage |
The y prefix applies to objects, functions, and enumerators — and optionally types and interfaces — with internal linkage:
|
c Protected class member |
The c prefix applies to class-protected objects and functions, and optionally, protected types and interfaces. Outside of C#, it also applies to protected enumerators, which are essentially protected constants. |
e Private class member |
The e prefix applies to class-private objects and functions, and optionally, private types and interfaces. Outside of C#, it also applies to private enumerators, which are essentially private constants. Distinguishing protected from private entities is helpful when implementing parent classes. |
a Function parameter |
The a prefix applies to function parameters. Though they are essentially local objects, it is useful to distinguish them because modifications to reference parameters change data outside the local scope. This prefix does not apply to macro parameters, as these are not necessarily objects, and macros do not define scopes. |
o Local element |
The o prefix applies to local objects. In languages that support them, it also applies to local functions, and optionally, types. |
s Static class member or local static object |
The s prefix applies to static class members and local static objects. It warns that changes to such objects have effects outside the current invocation or instance. It also warns against static initialization and deinitialization order fiascos.
The prefix does not apply to elements declared |
v Virtual function |
The v prefix applies to virtual class functions. It warns that a function’s behavior may change in subclasses, and prevents virtual functions from being unknowingly called within constructors. The prefix obviously cannot be applied to virtual destructors. |
u Union member |
The u prefix applies to union members. It warns that changes to such objects overwrite other parts of the union. |
r Non-const reference |
The r prefix applies to non-
In C#, |
q Object reference |
In languages that support them, the q prefix applies to ‘object references’ and object-referenced types. It shows that changes to a referenced instance persist outside the current scope. In C#, it also distinguishes classes, which use the prefix, from structures, which do not. This guards against unwanted copying and boxing.
Note that ‘references’ in C++ differ fundamentally from the ‘object references’ found in C#, Java, and Delphi. C# does offer C++-like references in the form of |
p Data pointer |
The p prefix applies to data pointers and data pointer types:
|
d Function pointer or delegate |
The d prefix applies to function pointers and function pointer types, including those referencing class functions. In languages that support them, it also applies to delegates. Distinguishing function pointers from data pointers allows the same root to be used by identifiers of both types:
|
b Managing object |
The b prefix applies to objects (like auto pointers) that are used to manage other objects. Distinguishing objects from managing objects allows the same root to be used by both varieties of element. It also avoids confusion about the element’s type:
|
h Handle |
The h prefix applies to handles and handle types. Though not a language feature, handles are a common means of address, and labeling them allows the same root to be used when a concept is referenced in distinct ways:
|
i Iterator |
The i prefix applies to iterators and iterator types. Though not a language feature, iterators are a common means of address, and labeling them allows the same root to be used when a concept is referenced in distinct ways:
|
Design notes
Scope prefixes and types
Some versions of this notation have applied scope prefixes (like g, c, and e) to types and interfaces. Such elements can produce the same name-hiding problems that affect objects and functions, so it makes sense to label their scopes. On the other hand, types are defined less often than instances, somewhat limiting the chance of a type name collision. Longer prefix strings are also less legible.
The decision ultimately may be better left to the programmer. Those who define many nested types may prefer to document type scopes. Those who do not may prefer to prefix types with t alone.
Scope prefixes and overrides
It is possible to change the access level of a virtual function when overriding it, rendering the scope prefix assigned to that function invalid. There is no way for a naming convention to account for this, however, and the practice is questionable from a design perspective, so it seems best simply to avoid it.
Macro parameters
It would occasionally be helpful to label macro parameters, but the a prefix would be misleading here, and it seems wasteful to dedicate a new prefix to this concern. To prevent name collisions, macro parameters may be prefixed with z.
Return values
In earlier versions of the notation, functions were labeled with r or p if they returned non-const
references or pointers. This clarified the effect of modifying such return values, but it made function identifiers somewhat difficult to interpret. The current version is simpler, and even without help, it seems unlikely that return value modification could cause much confusion. If the value is used right away, the effect is obvious:
IDNext() = oID;
Conversely, if the value is stored for later use, the object receiving the return value must bear the appropriate prefixes:
int* opID = IDNext();
*opID = oID;
Cleanup obligations
Most resource management can and should be handled with RAII; this approach is occasionally impractical, however, and it cannot be implemented in a meaningful sense within C# or Java. It therefore might be useful to label types and functions that incur cleanup obligations; this would go somewhat beyond documenting ‘language features’, however, and prefix strings in C# already seem too long. For now, cleanup obligations remain undocumented.