EduBrick

Шаблон по геометрии

Структура точки с операторами, ввод-вывод и один конструктор, из-за которого теряют часы.

4 мин

Геометрия пишется поверх заготовки. Написать её один раз и понимать целиком — половина дела: дальше задачи сводятся к вызову готовых функций.

Структура

using ld = long double;

struct Point {
    ld x = 0, y = 0;

    Point() = default;
    Point(ld x, ld y) : x(x), y(y) {}
    Point(Point a, Point b) : x(b.x - a.x), y(b.y - a.y) {}   // вектор из a в b

    Point operator+(Point p) const { return {x + p.x, y + p.y}; }
    Point operator-(Point p) const { return {x - p.x, y - p.y}; }
    Point operator*(ld k) const { return {x * k, y * k}; }

    ld operator*(Point p) const { return x * p.x + y * p.y; }   // скалярное
    ld operator%(Point p) const { return x * p.y - y * p.x; }   // векторное

    ld len2() const { return x * x + y * y; }
    ld len()  const { return sqrtl(len2()); }
};

ld dist(Point a, Point b) { return Point(a, b).len(); }

Одна структура и для точки, и для вектора — они и так одно и то же. Ссылки в аргументах не нужны: два числа копируются дешевле, чем разыменовывается ссылка.

Почему * и %, а не функции. У этих операторов одинаковый приоритет, и выражения вроде a % b == 0 пишутся без лишних скобок. Если взять ^ для векторного, придётся писать (a ^ b) == 0: у побитового исключающего «или» приоритет ниже, чем у сравнения, и a ^ b == 0 означает совсем не то.

Ввод и вывод

istream& operator>>(istream& in, Point& p) { return in >> p.x >> p.y; }
ostream& operator<<(ostream& out, const Point& p) { return out << p.x << ' ' << p.y; }

Поток берётся и возвращается по ссылке, чтобы работали цепочки cin >> a >> b. Подробнее — в статье про свои структуры.

Без этих двух строк каждое чтение точки превращается в cin >> p.x >> p.y, а ошибка компиляции при попытке cin >> p занимает под двести строк, из которых полезна первая.

Конструктор, который стоит часов

Самая коварная ловушка всего раздела.

Соблазн написать конструктор со значениями по умолчанию:

Point(ld x = 0, ld y = 0) : x(x), y(y) {}   // так не надо

Выглядит удобно: Point(), Point(5) и Point(2, 3) — всё работает. Проблема в том, что такой конструктор позволяет компилятору неявно превращать число в точку.

Теперь представьте, что вы забыли написать operator* для умножения на число. Строка

v = v * 2;

не даст ошибки компиляции. Компилятор превратит 2 в Point(2, 0) и вызовет векторное произведение — то есть посчитает число, а потом превратит его обратно в точку. Результат — тихо неверный ответ.

Найти такое чтением кода почти невозможно. Есть два способа не попасться:

explicit Point(ld x = 0, ld y = 0) : x(x), y(y) {}   // либо так
Point(ld x, ld y) : x(x), y(y) {}                    // либо без умолчаний

explicit запрещает неявное преобразование: Point p = 1; перестаёт компилироваться, а Point p(1) работает. Второй вариант проще: без значений по умолчанию конструктор от одного аргумента просто не существует.

Если сомневаетесь — берите второй. Он короче и не требует помнить, что делает explicit.

Целые или вещественные

По умолчанию — целые. Пока в задаче нет пересечений прямых, поворотов и расстояний, вещественные числа не нужны, а целые дают точный ответ и работают быстрее.

using ll = long long;   // вместо ld

При этом произведения обязательно long long: при координатах до 10910^9 они доходят до 210182 \cdot 10^{18}.

Переходить к long double стоит только там, где без деления или корня действительно не обойтись, — и лучше в отдельной функции, а не во всём шаблоне.

Типичное разделение: сравнения, ориентации, площади и принадлежности — в целых; расстояния, углы и точки пересечения — в вещественных.

Что ещё положить в шаблон

Минимальный полезный набор поверх структуры:

Всё остальное дописывается по задаче. Раздувать шаблон до сотен строк не стоит — читать его в стрессовой ситуации будет тяжелее, чем написать нужное заново.