Делайте интерфейсы точно и строго типизированными
Причина
Типы являются простейшей и лучшей документацией, улучшают читаемость благодаря своему чётко определённому смыслу и проверяются во время компиляции. Кроме того, точно типизированный код нередко оптимизируется лучше.
Пример (не делайте так)
Рассмотрим:
void pass(void* data); // слабый и недостаточно квалифицированный тип void* вызывает подозрения
Вызывающие стороны не уверены, какие типы допустимы и могут ли данные быть изменены, поскольку const не указан. Обратите внимание, что все типы указателей неявно преобразуются в void*, поэтому вызывающим сторонам легко предоставить это значение.
Вызываемая функция должна использовать static_cast для приведения данных к непроверяемому типу для их использования. Это подвержено ошибкам и многословно.
Используйте const void* только для передачи данных в проектах, не поддающихся описанию на C++. Рассмотрите использование variant или указателя на базовый класс вместо этого.
Альтернатива: Зачастую параметр шаблона может устранить void*, превратив его в T* или T&. В обобщённом коде эти T могут быть общими или ограниченными концептами параметрами шаблона.
Пример (плохой)
Рассмотрим:
draw_rect(100, 200, 100, 500); // что задают числа?
draw_rect(p.x, p.y, 10, 20); // в каких единицах 10 и 20?
Ясно, что вызывающая сторона описывает прямоугольник, но непонятно, к каким его частям они относятся. Кроме того, int может нести произвольные формы информации, включая значения многих единиц измерения, поэтому мы должны догадываться о смысле четырёх int. Скорее всего, первые два — это пара координат x, y, но что такое последние два?
Комментарии и имена параметров могут помочь, но мы могли бы быть явными:
void draw_rectangle(Point top_left, Point bottom_right);
void draw_rectangle(Point top_left, Size height_width);
draw_rectangle(p, Point{10, 20}); // два угла
draw_rectangle(p, Size{10, 20}); // один угол и пара (высота, ширина)
Очевидно, мы не можем перехватить все ошибки через статическую систему типов (например, тот факт, что первый аргумент должен быть верхним левым углом, оставлен на усмотрение соглашения (именования и комментариев)).
Пример (плохой)
Рассмотрим:
set_settings(true, false, 42); // что задают числа?
Типы параметров и их значения не сообщают, какие настройки задаются или что означают эти значения.
Этот дизайн более явный, безопасный и читаемый:
alarm_settings s{};
s.enabled = true;
s.displayMode = alarm_settings::mode::spinning_light;
s.frequency = alarm_settings::every_10_seconds;
set_settings(s);
Для набора булевых значений рассмотрите использование перечисления flags; шаблон, выражающий набор булевых значений.
enable_lamp_options(lamp_option::on | lamp_option::animate_state_transitions);
Пример (плохой)
В следующем примере из интерфейса непонятно, что означает time_to_blink: секунды? миллисекунды?
void blink_led(int time_to_blink) // плохо — единицы неоднозначны
{
// ...
// что-то делаем с time_to_blink
// ...
}
void use()
{
blink_led(2);
}
Пример (хороший)
Типы std::chrono::duration помогают сделать единицы времени явными.
void blink_led(milliseconds time_to_blink) // хорошо — единицы явные
{
// ...
// что-то делаем с time_to_blink
// ...
}
void use()
{
blink_led(1500ms);
}
Функцию можно также написать так, чтобы она принимала любые единицы длительности времени.
template<class rep, class period>
void blink_led(duration<rep, period> time_to_blink) // хорошо — принимает любые единицы
{
// предполагая, что миллисекунда — наименьшая актуальная единица
auto milliseconds_to_blink = duration_cast<milliseconds>(time_to_blink);
// ...
// что-то делаем с milliseconds_to_blink
// ...
}
void use()
{
blink_led(2s);
blink_led(1500ms);
}
Контроль
- (Простой) Сообщать об использовании
void*в качестве параметра или возвращаемого типа. - (Простой) Сообщать об использовании более одного параметра типа
bool. - (Сложно реализовать хорошо) Искать функции, использующие слишком много аргументов примитивного типа.