第17.3章 值类型上的算术

English

1. + 正确的场合

第17.2章 说当含义不是显然的那个时不要重载运算符。这里说另一半: 当这个类型就是一个数时,算术记号不是语法糖,它是读者本来就掌握的记号。 一个二维向量应该写成 (a + b) * 2,而不是 scale(add(a, b), 2)。

判断标准不是品味,而是四条性质。当操作满足下面全部条件时才重载算术:

  1. 全域 —— 对值的所有组合都有定义,没有非法状态,没有定义域错误。
  2. 不分配 —— 它产出一个固定大小的栈上的值, 所以代价光看类型就知道(sizeof(vec2))。
  3. 不失败 —— 它不会抛,所以可以是 noexcept, 调用者不需要错误路径。
  4. 守规律 —— 它满足读者预期的恒等式:a + b == b + a、 (a + b) + c == a + (b + c)、a + vec2{} == a。

任何一条不满足,你就回到 第17.2章 的地盘, 而且你要的是名字。

2. 一个完整正确的值类型

两个类型:一个是 vec2,完整算术是正当的;一个是 price, 其中只有一部分算术正当 —— 而必须不存在的那个运算符才是重点。


#include <fast_io.h>
#include <cstdint>

struct vec2
{
	::std::int_least32_t x{};
	::std::int_least32_t y{};

	constexpr vec2 &operator+=(vec2 other) noexcept
	{
		x += other.x;
		y += other.y;
		return *this;
	}
	constexpr vec2 &operator-=(vec2 other) noexcept
	{
		x -= other.x;
		y -= other.y;
		return *this;
	}
	constexpr vec2 &operator*=(::std::int_least32_t s) noexcept
	{
		x *= s;
		y *= s;
		return *this;
	}
};

[[nodiscard]] inline constexpr vec2 operator+(vec2 a, vec2 b) noexcept
{
	return a += b;
}
[[nodiscard]] inline constexpr vec2 operator-(vec2 a, vec2 b) noexcept
{
	return a -= b;
}
[[nodiscard]] inline constexpr vec2 operator*(vec2 a, ::std::int_least32_t s) noexcept
{
	return a *= s;
}
[[nodiscard]] inline constexpr vec2 operator*(::std::int_least32_t s, vec2 a) noexcept
{
	return a *= s;
}

struct price
{
	::std::int_least64_t cents{};

	constexpr price &operator+=(price other) noexcept
	{
		cents += other.cents;
		return *this;
	}
};

[[nodiscard]] inline constexpr price operator+(price a, price b) noexcept
{
	return a += b;
}

[[nodiscard]] inline constexpr price operator*(price p, ::std::int_least32_t units) noexcept
{
	return price{p.cents * units};
}

/*
No operator* between two prices. cents * cents is not money; scaling a price by a rate
needs a rounding rule, so it is a named function with a documented contract.
*/
[[nodiscard]] inline constexpr price apply_rate(price p, ::std::int_least64_t num, ::std::int_least64_t den) noexcept
{
	return price{(p.cents * num + den / 2) / den};
}

static_assert(noexcept(operator+(vec2{}, vec2{})));
static_assert(noexcept(operator+(price{}, price{})));

int main()
{
	try
	{
		vec2 a{3, 4};
		vec2 b{1, 2};
		vec2 const sum{a + b};
		::fast_io::io::println("a + b = (", sum.x, ", ", sum.y, ")");
		::fast_io::io::println("a - b = (", (a - b).x, ", ", (a - b).y, ")");
		::fast_io::io::println("a * 2 = (", (a * 2).x, ", ", (a * 2).y, ")  2 * a = (", (2 * a).x,
							   ", ", (2 * a).y, ")");
		a *= 10;
		::fast_io::io::println("after a *= 10 = (", a.x, ", ", a.y, ")");

		price const net{10000};
		price const tax{2500};
		price const total{net + tax};
		::fast_io::io::println("total = ", total.cents, " cents; 7/8 of it = ",
							   apply_rate(total, 7, 8).cents, " cents; 7 units = ",
							   (total * 7).cents, " cents");
	}
	catch throws (::std::error e)
	{
		::fast_io::io::print("error: ");
		::fast_io::io::println(e);
	}
}

a + b = (4, 6)
a - b = (2, 2)
a * 2 = (6, 8)  2 * a = (6, 8)
after a *= 10 = (30, 40)
total = 12500 cents; 7/8 of it = 10938 cents; 7 units = 87500 cents

3. 每一行为什么在那儿

4. 定点数是最能说明问题的地方

钱、物理引擎里的坐标、音频采样、游戏的时间步,都是定点数。 用一个类加运算符来建模它们的理由在于:错误的算术会变成编译错误,而不是静默的 bug:

注意这一章反复出现的模式:类型的价值不在于它能加 —— ::std::int_least64_t 也能加 —— 而在于它拒绝那些没有意义的操作。

5. 经验法则