Время от времени вдруг начинает хотеться именованных параметров в C++. Не так давно была статья, да и сам какое-то время назад писал на эту тему. И вот что удивительно — со времен той своей статьи я участвую в новом проекте без необходимости тащить за собой старый код, и как-то удивительным образом всего этого описанного собой же не использую. Т.е. в вопросе разобрался, восхитился перспективами… и продолжил работать по-старинке! Как же так? Лень? Инерция? Ответ постараюсь дать под катом.
Для начала рассмотрим пример — объявление функции, возвращающей объект даты для заданных дня, месяца и года.
Date createDate(int day, int year, int month);
Проблема очевидна — какой порядок параметров не выбери, через месяц, увидев подобное
Date theDate = createDate(2, 3, 4);
будешь гадать: «Что это? Второе марта 2004-го года, или четвертое 2002-го?». Если же особо повезло, и команда интернациональная, трактовка функции разработчиками может в корне отличаться. Одинаковые типы идут подряд в списке параметров… Вот в таких случаях обычно и хочется именованных параметров, которых в С++, увы, нет.
Многим программистам приходится переключаться с одного языка программирования на другой. При этом что-то в новом языке нравится, что-то нет… Плохое со временем забывается, хорошее же хочется непременно перенести в ту среду, где сейчас работаешь. Вон, в том же Objective C именованные параметры имеются!
+ (UIColor *)colorWithRed:(CGFloat)red
green:(CGFloat)green
blue:(CGFloat)blue
alpha:(CGFloat)alpha
Да, первый параметр идет без имени, но его название часто включают в имя метода. Решение, конечно, не идеальное (например, метода colorWithBlue не существует, такая вот цветовая несправедливость), зато именованные параметры поддерживаются на уровне языка. В VBA все еще лучше — имена можно дать всем параметрам, благодаря им можно не плодить множество похожих методов, а обойтись одним. Посмотрите, например, на Document.PrintOut
А в C++ ничего такого нет! Сразу хочется исправлять ситуацию, искать библиотеки, придумывать костыли и велосипеды. И даже что-то найдется и получится. Но вместо этого можно задуматься, если все так прекрасно, почему именованные параметры не добавили. Столько парадигм поддерживается, а тут такое…
Или, может, добавили? Просто назвали иначе. Например, пользовательскими типами. Самое время привести основную мысль статьи. Примитивным стандартным типам не место в интерфейсах реальной системы. Такие типы — просто строительные блоки, из которых нужно именно строить, а не пытаться жить внутри.
Например, объект типа int — знаковое целое, лежащее в определенном диапазоне. Что описывает этот тип? Только свою реализацию. Это не может быть, например, количество яблок. Потому что яблок не может быть минус 10. Все еще хуже: unsigned int также непригоден для этой задачи. Потому что яблоки вообще не имеют никакого отношения к размерности типа данных на вашей платформе. Привязывая примитивные типы языка к параметрам открытых методов своих моделей, мы делаем ошибку, которую затем пытаемся «замять» с помощью различных костылей. Причем в своем стремлении скрыть оплошность мы часто игнорируем тот простой факт, что язык пытается нам помочь, говоря: «Я не поддерживаю этого напрямую, и неспроста...».
Но главный недостаток примитивных типов — компилятор лишается шанса выявить логическую ошибку. Например, есть у нас метод, принимающий два параметра — имя и фамилию. Если свести их к стандартным строковым типам, то компилятор увидит только два куска текста, от перестановки которых смысл не поменяется. В результате один разработчик передаст первым параметром имя, а другой — фамилию. И оба технически будут правы. Ошибка уничтожается в зачатке, если для имени и фамилии существуют специальные типы. В реальной системе, где имя и фамилия являются до того значимыми сущностями, что входят в интерфейс по отдельности, сводить их просто к строкам — ошибка. Имя — это совсем не произвольная строка. Хотя бы потому, что выбирается из заранее известного множества. Еще оно, скажем, не содержит цифр (хотя тут я не уверен).
Но вернемся к датам. День — это ни в коем случае не unsigned int, не unsigned char не даже std::string. День — это… день! Если мы строим модель, в которой присутствуют даты, то для представления дней имеет смысл создать специальный тип. Пользовательские типы данных — это как раз то, что придает C++ всю его мощь и выразительность. Тогда и костыли становятся не нужны. Вводим класс для представления дней
class Day
{
explicit Day (unsigned char day);
//...
private:
unsigned char mValue;
};
Как-то вот так. Естественно для физического представления значения в памяти мы все равно используем примитивный тип. Но больше это не является частью интерфейса. Сразу же мы получаем полный контроль над содержимым этого поля, исключая ряд возможных ошибок. Да, не зная полной даты, точные ограничения установить не получится, но по крайней мере проверку на попадание в интервал 1..31 уже можно организовать. Самое же главное: реализовав специальные типы данных для месяца и года с явными (explicit) конструкторами для инициализации примитивными типами, мы получим именованные параметры, поддерживаемые непосредственно языком. Функцию теперь можно вызывать следующим образом
Date theDate = createDate(Day(2), Month(3), Year(4));
Никаких окольных путей, никаких дополнительных библиотек. Да, менять местами параметры такой подход не позволит, но это не так и важно. Главная миссия именованных параметров — исключить ошибки при вызове функций и повысить читабельность кода — осуществляется.
В качестве бонуса получаем повышенную гибкость, если, скажем, захотелось задавать месяц еще и в строковом виде. Теперь можно не делать перегружаемый вариант createDate (это функция создания объекта даты, какое ей вообще дело до формата месяца). Вместо этого просто добавляется еще один явный конструктор для типа «месяц»
class Month
{
explicit Month(unsigned char month);
explicit Month(std::string month);
//...
private:
unsigned char mValue;
};
Каждый теперь занимается своим делом — createDate создает дату, а класс Month интерпретирует и контролирует корректность значения месяца.
Date theDate = createDate(Day(2), Month("Jan"), Year(4));
Сразу хочется возразить — а не многовато ли лишних типов будет, если для каждого примитивного типа делать свой тип-обертку? Тут как посмотреть. Если вы студент, которому нужно побыстрее написать лабу, сдать ее и забыть, то да — много лишнего кода и потерянное время. Но если речь идет о реальной системе, в долгой и счастливой жизни которой вы заинтересованы, то называть пользовательские типы для сущностей, используемых в интерфейсе, лишними я бы не стал.
Но как быть с пользовательскими типами? Что, например, делать, если какой-то метод принимает несколько объектов одинакового типа
User user1, user2;
//...
someMethod(user1, user2);
Здесь все зависит от контекста. Если все объекты равнозначны, то и проблемы нет — от порядка их передачи ничего не меняется. Чтобы подчеркнуть это, можно разве что передавать объекты, упакованными в массив или другой контейнер. Если же объекты неравнозначны, например, метод ставит в подчинение user2 объекту user1, то совсем нелишними будут специальные типы, отражающие роли объектов. Должны это быть обертки вокруг объектов пользователей (как в случае с примитивными типами) или проще создать специальные классы-наследники User, зависит от реализуемой системы. Важно каким-то образом выразить средствами языка различные роли user1 и user2, позволяя компилятору отлавливать ошибки, связанные с их возможной путаницей.
Какой можно сделать вывод. Не нужно стремиться выхватить все самое лучшее из всех языков и засунуть это в один, и без того многострадальный C++. Важно уметь побороть инерцию при смене языка программирования. Скажем, да, в Луа можно присваивать значения сразу нескольким переменным
x, y = getPosition()
Сама идея прекрасна, но нужна ли она в C++. Вообще не нужна. Тут проще создать тип Position и присваивать значение его объекту. Язык — это инструмент, не более того. И из того, что инструменты иногда похожи, совсем не следует, что пользоваться ими нужно одинаково вплоть до мелочей.
Автор: vadim_ig