Ch17.9: Rules and a Checklist
1. The Decision, in Order
When you are about to write an operator, ask these in this order and stop at the first "yes":
- Can this operation fail, allocate, block, or report an error? Then it
is a named function with a
throwscontract (Ch17.2). - Is the glyph's everyday meaning exactly this operation? If not —
/for joining paths,|for unions,<<for formatting — it is a named function (Ch17.8). - Is it one of
&& || , &or a conversion operator? Then it does not exist:explicit operator boolis the only exception. - Does the type model a number? Then arithmetic operators, defaulted
==and<=>, allnoexcept, all[[nodiscard]](Ch17.3, Ch17.4). - Is the operation a plain container access that cannot fail? Then
operator[]; otherwise a namedat/find(Ch17.7). - Otherwise — a name.
2. A Type That Passed the Checklist
config has equality because equality is meaningful and free, and it has
nothing else. The two operations that do real work are functions, and their names carry
the information an operator would have hidden: with_port returns a modified
copy, and validate can fail.
#include <fast_io.h>
#include <fast_io_dsal/string.h>
#include <cstdint>
struct config
{
::fast_io::string name{};
::std::int_least32_t port{};
/* equality: yes, and the compiler writes it */
friend bool operator==(config const &, config const &) = default;
/* ordering: no. Two configs are not "less" than each other. */
/* arithmetic: no. Adding two configurations is not a thing. */
/* stream operator: no. Formatting is print_define's job. */
/* conversion operator: no. Use named accessors. */
/* operator new: no. See Ch18.7. */
[[nodiscard]] constexpr bool is_valid() const noexcept
{
return port > 0 && port < 65536;
}
};
/*
The two operations that exist are named, so the reader knows a copy is made and that
validation has a contract.
*/
[[nodiscard]] inline config with_port(config c, ::std::int_least32_t port)
{
c.port = port;
return c;
}
inline void validate(config const &c) throws
{
if (!c.is_valid())
{
::fast_io::herbceptions::throws_errc(::std::errc::invalid_argument);
}
}
int main()
{
try
{
config const http{::fast_io::string{"http"}, 80};
config const alt{with_port(http, 8080)};
::fast_io::io::println("http == alt: ", http == alt, " alt.port = ", alt.port);
validate(alt);
::fast_io::io::println("alt is valid: ", alt.is_valid());
}
catch throws (::std::error e)
{
::fast_io::io::print("error: ");
::fast_io::io::println(e);
}
}
http == alt: 0 alt.port = 8080
alt is valid: 1
Note what is absent from the type: no operator+ (adding
configurations is meaningless), no operator< (two configurations are not
ordered), no operator<< (formatting belongs to
print_define), no conversion operator, no operator new. Two of
those absences are the whole design: they turn "somebody might misuse this" into a compile
error.
3. The Checklist, as Grep
Rules that only exist in a document do not survive contact with a codebase. These are mechanical, and each one is worth a CI job:
| Check | What it finds |
|---|---|
grep -rn "operator&&\|operator||\|operator,\|operator&(" |
the four operators that must never be overloaded |
grep -rn "operator<<\|operator>>" |
stream operators, including the ones in new code that were copied from old |
grep -rn "operator new\|operator delete" |
class-level allocation (Ch18.7) |
grep -rn "operator .*()" | grep -v explicit |
implicit conversions |
grep -rn "operator+" | grep -v noexcept |
value operators without a contract |
The last one is the smallest and the most useful: in this book every function has an error
contract (Ch6.16), and an operator that
is neither noexcept nor throws has not been thought about.
4. Summary
- An operator is a function with a punctuation name. The name carries less information than any identifier, so the operation must be obvious, total, and free.
- Prefer a named function.
concatandappendsay what+cannot: whether a new value is produced and whether anything is copied. - Define the compound assignment first; define the binary operator in terms of it;
provide both operand orders; mark results
[[nodiscard]]. - Use
= defaultfor==and<=>; usepartial_orderingin a named function when the order is not total. operator[]is arithmetic and cannot fail; anything else gets a name.- Never overload
&&,||,,,&,<<,>>, oroperator new. explicit operator boolis the only conversion operator you should own.