Свои структуры
Почему пара пар — плохая идея, как научить структуру сравниваться и вводиться, и что делает const в объявлении оператора.
5 мин
В задаче есть объект с несколькими полями: у школьника имя, класс и балл; у отрезка левая и правая границы; у события время и тип. Хранить это можно по-разному, и разница огромна.
Пара пар — так не надо
Для двух полей pair уместен. Для трёх начинается вот это:
vector<pair<pair<string, string>, int>> people;
cin >> item.first.first >> item.first.second >> item.second;
Через день вы уже не помните, что такое first.second. Через два поля добавится ещё уровень, и код станет нечитаемым окончательно. tuple не лучше: доставать поля через get<2>(t) ничем не понятнее.
Правильно так:
struct Person {
string name;
string surname;
int grade = 0;
long long score = 0;
}; // точка с запятой обязательна
Теперь person.grade вместо item.first.second. Точка с запятой после закрывающей скобки — самая частая ошибка компиляции у тех, кто пишет структуру впервые.
Значения по умолчанию (= 0) стоит указывать всегда. Без них поля неинициализированной структуры содержат мусор, и это неопределённое поведение.
Сравнение
sort по вектору структур не скомпилируется: язык не знает, какая структура «меньше». Ошибка при этом будет на четыре сотни строк, и полезной информации в ней почти нет — ищите первую строчку со словом error.
Учим сравниваться:
struct Person {
string name;
int grade = 0;
long long score = 0;
bool operator<(const Person& other) const {
return score < other.score;
}
};
Два const здесь не украшение.
Первый, в const Person& other, означает две вещи: аргумент передаётся по ссылке (не копируется — а копирование структуры со строками стоит дорого) и изменять его нельзя. Без ссылки при каждом сравнении копировались бы все строки; сортировка мгновенно стала бы вдвое медленнее.
Второй, после скобок, запрещает менять текущий объект. Без него можно случайно написать score = 0 внутри сравнения и очень долго искать, почему массив сортируется в мусор.
Альтернатива — определить оператор снаружи структуры:
bool operator<(const Person& left, const Person& right) {
return left.score < right.score;
}
Работает одинаково. Внутри короче, снаружи нагляднее — дело вкуса.
Сравнение должно быть строгим
Оператор обязан возвращать строгое «меньше». return score <= other.score — не оптимизация, а неопределённое поведение: sort на массиве из равных элементов с таким компаратором падает с ошибкой сегментации.
Для сравнения по нескольким полям удобно сравнивать кортежи — они уже умеют лексикографический порядок:
bool operator<(const Person& other) const {
return tie(grade, score, name) < tie(other.grade, other.score, other.name);
}
Одна строка вместо цепочки if, и ошибиться негде.
С C++20 есть ещё короче — auto operator<=>(const Person&) const = default; сравнивает все поля по порядку объявления. Но проверьте, что судья поддерживает стандарт.
Ввод и вывод
Чтобы cin >> person работало, нужен оператор ввода:
istream& operator>>(istream& in, Person& p) {
return in >> p.name >> p.grade >> p.score;
}
ostream& operator<<(ostream& out, const Person& p) {
return out << p.name << ' ' << p.grade << ' ' << p.score;
}
Почему всё по ссылке и почему возвращается поток: чтобы работали цепочки cin >> a >> b. Каждый оператор возвращает тот же поток, и следующий читает из него дальше. Возьмёте поток по значению — не скомпилируется.
Обратите внимание на асимметрию: в operator>> структура не const (её меняют), в operator<< — const (только читают).
Разбор на переменные
С C++17 структуру можно разложить на именованные части прямо в объявлении:
for (auto& [name, grade, score] : people)
cin >> name >> grade >> score;
Поля привязываются по порядку объявления, а не по имени. Поменяете местами поля в структуре — переменные тихо поменяются смыслом, и компилятор не скажет ни слова. Приём удобен в коротких циклах и опасен в длинных.
Так же разбираются пары, что особенно приятно при обходе словаря:
for (const auto& [key, value] : counts)
cout << key << ": " << value << '\n';
Конструкторы
Пока полей мало, структура создаётся списком в фигурных скобках: Person p{"Аня", 9, 100}. Когда полей много, а заполнять нужно два, пишут конструктор:
struct Segment {
long long left = 0, right = 0, length = 0;
Segment() = default;
Segment(long long l, long long r) : left(l), right(r), length(r - l) {}
};
Строка после двоеточия — список инициализации: поля заполняются до входа в тело. Это и быстрее, и единственный способ инициализировать ссылки и константы.
Важная деталь: как только вы написали хоть один конструктор, конструктор без аргументов исчезает. Строка Segment() = default; возвращает его. Забудете — и vector<Segment> v(n) перестанет компилироваться.
emplace_back
При конструкторе появляется смысл у emplace_back:
v.push_back(Segment(1, 5)); // создали, потом скопировали в вектор
v.emplace_back(1, 5); // сразу построили внутри вектора
Разница ощутима, когда в структуре лежит что-то тяжёлое — строка или вложенный вектор. Для структур из чисел разницы почти нет.
Структуры в контейнерах
С определённым operator< структура кладётся в set и работает ключом map без дополнительных усилий. Без него — не скомпилируется: упорядоченным контейнерам нужен порядок.
Для unordered_set и unordered_map operator< не нужен, зато нужны operator== и своя хеш-функция. Проще не связываться и взять обычный set, а если нужна скорость — свести структуру к числу или к строке и хешировать его.