Encapsulate rule violations
Reason
To keep code simple and safe. Sometimes, ugly, unsafe, or error-prone techniques are necessary for logical or performance reasons. If so, keep them local, rather than "infecting" interfaces so that larger groups of programmers have to be aware of the subtleties. Implementation complexity should, if at all possible, not leak through interfaces into user code.
Example
Consider a program that, depending on some form of input (e.g., arguments to main), should consume input from a file, from the command line, or from standard input. We might write
bool owned;
owner<istream*> inp;
switch (source) {
case std_in: owned = false; inp = &cin; break;
case command_line: owned = true; inp = new istringstream{argv[2]}; break;
case file: owned = true; inp = new ifstream{argv[2]}; break;
}
istream& in = *inp;
This violated the rule against uninitialized variables, the rule against ignoring ownership, and the rule against magic constants. In particular, someone has to remember to somewhere write
if (owned) delete inp;
We could handle this particular example by using unique_ptr with a special deleter that does nothing for cin, but that's complicated for novices (who can easily encounter this problem) and the example is an example of a more general problem where a property that we would like to consider static (here, ownership) needs infrequently be addressed at run time. The common, most frequent, and safest examples can be handled statically, so we don't want to add cost and complexity to those. But we must also cope with the uncommon, less-safe, and necessarily more expensive cases. Such examples are discussed in [[Str15]](https://www.stroustrup.com/resource-model.pdf).
So, we write a class
class Istream { [[gsl::suppress("lifetime")]]
public:
enum Opt { from_line = 1 };
Istream() { }
Istream(czstring p) : owned{true}, inp{new ifstream{p}} {} // read from file
Istream(czstring p, Opt) : owned{true}, inp{new istringstream{p}} {} // read from command line
~Istream() { if (owned) delete inp; }
operator istream&() { return *inp; }
private:
bool owned = false;
istream* inp = &cin;
};
Now, the dynamic nature of istream ownership has been encapsulated. Presumably, a bit of checking for potential errors would be added in real code.
Enforcement
- Hard, it is hard to decide what rule-breaking code is essential
- Flag rule suppression that enable rule-violations to cross interfaces
F: Functions
A function specifies an action or a computation that takes the system from one consistent state to the next. It is the fundamental building block of programs.
It should be possible to name a function meaningfully, to specify the requirements of its argument, and clearly state the relationship between the arguments and the result. An implementation is not a specification. Try to think about what a function does as well as about how it does it. Functions are the most critical part in most interfaces, so see the interface rules.
Function rule summary:
Function definition rules:
- F.1: "Package" meaningful operations as carefully named functions
- F.2: A function should perform a single logical operation
- F.3: Keep functions short and simple
- F.4: If a function might have to be evaluated at compile time, declare it
constexpr - F.5: If a function is very small and time-critical, declare it inline
- F.6: If your function must not throw, declare it
noexcept - F.7: For general use, take
T*orT&arguments rather than smart pointers - F.8: Prefer pure functions
- F.9: Unused parameters should be unnamed
- F.10: If an operation can be reused, give it a name
- F.11: Use an unnamed lambda if you need a simple function object in one place only
Parameter passing expression rules:
- F.15: Prefer simple and conventional ways of passing information
- F.16: For "in" parameters, pass cheaply-copied types by value and others by reference to
const - F.17: For "in-out" parameters, pass by reference to non-
const - F.18: For "will-move-from" parameters, pass by
X&&andstd::movethe parameter - F.19: For "forward" parameters, pass by
TP&&and onlystd::forwardthe parameter - F.20: For "out" output values, prefer return values to output parameters
- F.21: To return multiple "out" values, prefer returning a struct
- F.60: Prefer
T*overT&when "no argument" is a valid option
Parameter passing semantic rules:
- F.22: Use
T*orowner<T*>to designate a single object - F.23: Use a
not_null<T>to indicate that "null" is not a valid value - F.24: Use a
span<T>or aspan_p<T>to designate a half-open sequence - F.25: Use a
zstringor anot_null<zstring>to designate a C-style string - F.26: Use a
unique_ptr<T>to transfer ownership where a pointer is needed - F.27: Use a
shared_ptr<T>to share ownership
<a name="rf-value-return"></a>Value return semantic rules:
- F.42: Return a
T*to indicate a position (only) - F.43: Never (directly or indirectly) return a pointer or a reference to a local object
- F.44: Return a
T&when copy is undesirable and "returning no object" isn't needed - F.45: Don't return a
T&& - F.46:
intis the return type formain() - F.47: Return
T&from assignment operators - F.48: Don't return
std::move(local) - F.49: Don't return
const T
Other function rules:
- F.50: Use a lambda when a function won't do (to capture local variables, or to write a local function)
- F.51: Where there is a choice, prefer default arguments over overloading
- F.52: Prefer capturing by reference in lambdas that will be used locally, including passed to algorithms
- F.53: Avoid capturing by reference in lambdas that will be used non-locally, including returned, stored on the heap, or passed to another thread
- [F.54: When writing a lambda that captures
thisor any class data member, don't use[=]default capture](/roadmap/cpp/guidelines/f-54) - F.55: Don't use
va_argarguments - F.56: Avoid unnecessary condition nesting
Functions have strong similarities to lambdas and function objects.
See also: C.lambdas: Function objects and lambdas
F.def: Function definitions
A function definition is a function declaration that also specifies the function's implementation, the function body.