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":

  1. Can this operation fail, allocate, block, or report an error? Then it is a named function with a throws contract (Ch17.2).
  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).
  3. Is it one of && || , & or a conversion operator? Then it does not exist: explicit operator bool is the only exception.
  4. Does the type model a number? Then arithmetic operators, defaulted == and <=>, all noexcept, all [[nodiscard]] (Ch17.3, Ch17.4).
  5. Is the operation a plain container access that cannot fail? Then operator[]; otherwise a named at / find (Ch17.7).
  6. 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