Начать статью я хотел бы с констатации того факта, что прямо за окном находится 2011 год (пруфлинк), середина апреля. Напоминаю я это в первую очередь себе, поскольку меня периодически посещают в этом сомнения. Дело в том, что как по работе, так и ради хобби я часто читаю код на С++, написанный лет 10-20 назад (но поддерживаемый и поныне) или код написанный совсем недавно, но людьми, которые учились программировать на С++ те же 20 лет назад. И вот после этого у меня и возникает ощущение, что никакого прогресса за эти годы не было, ничего не менялось и не развивалось, а по Земле до сих пор бродят мамонты.
Вступление
Из КВН:
-А где тут у вас в Сочи, бабушка, можно комнатку снять долларов за 25 в день?
-А, так тут недалеко, ребятки. В 90-ом году.
Специфика программирования 20 лет назад была совсем другой. Счет памяти и ресурсов процессора шел на байты и такты, многие вещи еще не были изобретены и приходилось выкручиваться. Но это вовсе не повод и сегодня писать код исходя из этих предпосылок. Мир меняется.
Все, что я буду дальше писать касается только программирования на С++ и только mainstream-компиляторов (gcc, Intel, Microsoft) — с другими языками и компиляторами я работал меньше и говорить о положении вещей в них не могу. Также я буду говорить только о прикладном программировании под десктоп-операционки (в кластерах, микропроцессорах и системном программировании тенденции могут отличаться).
TR1
Для тех, кто был в танке последний пяток лет я расскажу великую военную тайну (токо тссс!). Есть такая штука, как TR1. Это может стать откровением, но почти во всех современных компиляторах есть встроенные умные указатели, неплохие генераторы случайных чисел, много специальных математических функций, поддержка регулярных выражений и другие интересные вещи. Вполне неплохо работает. Пользуйтесь.
C++0x
Для тех, кто приобщился к кружку сидения в тяжелой бронетехнике всего пару лет назад я сообщу еще одну благую весть. Есть такая штука, как C++0x. Возрадуйтесь, братья! Да, официально на нём еще не стоят несколько высоких подписей и церемония разбития бутылки шампанского о борт стандарта еще не состоялась, но релиз-кандидат утвержден и поддержка в компиляторах уже есть.
Уже сейчас к Вашим услугам:
- Лямбда-выражения
- Rvalue ссылки
- Обобщённые константные выражения
- Внешние шаблоны
- Списки инициализации
- For-цикл по коллекции
- Улучшение конструкторов объектов
- nullptr
- Локальные и безымянные типы в качестве аргументов шаблонов
- Явные преобразования операторов
- Символы и строки в Юникоде
- «Сырые» строки (Raw string literals)
- Статическая диагностика
- Template typedefs
- Ключевое слово auto
и куча других полезных вещей.
Ну вот посмотрите хотя бы на следующие примеры:
- Вместо
теперь можно написатьvector<int>::const_iterator itr = myvec.begin();
— и это будет работать! Более того, даже строгая типизация никуда не девается (auto — это не указатель и не Variant, это просто синтаксический сахар)auto itr = myvec.begin();
- Теперь можно ходить по коллекциях аналогом цикла for_each
int my_array[5] = {1, 2, 3, 4, 5}; for(int &x : my_array) x *= 2;
Ну красота же, правда? Напомню, это поддерживается в основных, стабильных (не альфа\бета) ветках всех основных компиляторов. И это работает. Почему Вы этого до сих пор не используете?
Передача всего и везде по указателям (ссылкам)
Возможность передавать сущности в функции и методы как по ссылке так и по значению — очень мощный механизм и не стоит его использовать однобоко. Часто я вижу, как по указателю передаётся вообще все и всегда. Аргументы у людей такие:
- Указатель передаётся быстрее, чем структура данных — прирост скорости.
- При передаче по указателю нет нужны в дополнительной копии — экономия памяти.
Оба аргумента несущественны. Выигрыш часто составляет пару байт и тактов (его даже не получается экспериментально измерить), но вылазит целая куча недостатков:
- Функция-приемник вынуждена проверять все аргументы как минимум на NULL. Да и тот факт, что указатель не NULL тоже еще ничего не гарантирует.
- Функция-приемник вправе сделать с передаваемой сущностью все, что угодно. Изменить, удалить — всё. Аргумент о ключевых словах «const» — не аргумент. В С++ масса хаков, дающих возможность изменить данные по константному указателю или ссылке.
- Вызывающая функция вынуждена либо доверять вызываемой в части изменения данных, либо валидировать их после каждого вызова.
- Значительная часть объектов, передачу которых пытаются оптимизировать использованием указателей сами по себе являются почти чистыми указателями. Это касается как минимум классов строк, работа с которыми оптимизировано везде и давно.
Я приведу аналогию: у Вас дома вечеринка, присутствует десяток хороших друзей + пару случайных личностей (как всегда). Вдруг одна из таких личностей замечает на Вашем компьютере
Вычисление констант
Вот кусочек кода:
#define PI 3.1415926535897932384626433832795
#define PI_DIV_BY_2 1.5707963267948966192313216916398
#define PI_DIV_BY_4 0.78539816339744830961566084581988
...
< еще 180 аналогичных дефайнов >
...
Может быть для кого-то я открою великую тайну, но константы нынче вычисляются компилятором при компиляции, а не на рантайме. Так что «PI/2» будет читаться легче, места занимать меньше, а работать так же быстро, как и 180 дефайнов в примере выше. Не недооценивайте компилятор.
Собственные велосипеды
Иногда я вижу в коде что-то типа:
class MySuperVector
{ // моя очень быстрая реализация вектора
...
}
В этот момент меня пронимает дрожь. Существуют STL, Boost (и многие другие библиотеки), в которых лучшие умы планеты уже который десяток лет совершенствуют множество прекрасных алгоритмов и структур данных. Писать что-то своё стоит только в 3-ех случаях:
- Вы учитесь(лабораторная, курсовая)
- Вы пишете серьёзную научную работу именно на эту тему
- Вы наизусть знаете коды STL, Boost, десятка аналогичных библиотек, 3 томика Кнута и четко уверены, что решения для Вашего случая в них нет.
В реальности происходит следующее:
- Люди понятия не имеют о наличии библиотек
- Люди нифига не читают умные книги
- Люди имеют завышенную самооценку, считают себя умнее всех
- «Чукча не читатель, чукча — писатель»
В результате имеем смешные баги, дикие тормоза и искреннее удивление автора при
Ненужные оптимизации
Пример:
int a = 10;
a <<= 1;
Что тут написано? Я верю в читателей Хабра и думаю почти все знают, что это побитовый сдвиг. И многие знают еще и о том, что эти операции для целых чисел эквивалентны умножению и делению на 2. Так было модно писать раньше, поскольку операция побитового сдвига выполняется быстрее, чем операции умножения и деления. Но вот они факты на сегодня:
- Все компиляторы достаточно умны, чтобы самостоятельно заменять умножение и деление на сдвиг в подобных случаях.
- Не все люди достоточно умны, чтобы понимать этот код.
В результате Вы получите хуже читаемый код, без преимуществ в скорости работы и периодические (в зависимости от количества и квалификации коллег) вопросы: «А че за хрень?». Зачем это Вам?
Ненужная экономия памяти
Я дам пару ссылок на случаи, когда люди пытались сэкономить 1-2 байта памяти и что из этого вышло.
Уже сегодня у нас есть в среднем от 2 до 4 Гб ОЗУ. Еще пару лет и все вокруг будет 64-битное и памяти будет еще больше. Думайте наперед. Экономьте мегабайты, а не отдельные биты. Если речь идет о количестве людей, предметов, транзакций, температуре, дате, расстоянии, размерах файлов и т.д. — пользуйтесь типами long, longlong или чем-то специализированным. Забудьте о byte, short и int. Это всего-лишь несколько байт, а переполнение в будущем может стоить очень дорого. Не поленитесь завести отдельные переменные для разных сущностей, а не использовать одну временную с мыслью «а, все равно они никогда одновременно использоваться не будут».
Выводы
Не программируйте наскальную живопись. Потомки не оценят.