*** ВНИМАНИЕ: Блог переехал на другой адрес - demin.ws ***

четверг, 29 октября 2009 г.

Искусственная типизация однородных параметров в C++

Допустим есть вот такой класс:
class Date {
public:
Date(int year, int month, int day) {
...
}
};
К сожалению, не весь мир пользуется логичной нотацией Год/Месяц/День или День/Месяц/Год. Иногда люди пишут Месяц/День/Год. Хотя и первые два легко перепутать. Вот к чему я веду: где-то в далеком от описания класса коде кто-то пишет:
Date d(2009, 4, 5);
Что он этим хотел сказать? 4-е Мая или 5-е Апреля? Сложно быть уверенным, что пользователь такого класса когда-нибудь не перепутает порядок аргументов.

Можно улучшить дизайн? Да.

Например, так:
class Year {
public:
explicit Year(int year) : year_(year) {}
operator int() const { return year_; }
private:
int year_;
};
И аналогично:
class Month { ... };
class Day { ... };
Интерфейс самого класса Date может быть таким:
class Date {
public:
Date(Year year, Month month, Day day);
Date(Month month, Day day, Year year);
Date(Day day, Month month, Year year);
}
И использовать класс надо так:
Date d(Year(2010), Month(4), Day(5));
или
Date d(Month(4), Day(5), Year(2010));
Результат будет всегда предсказуем и виден в вызывающем коде. Тут все inline'овое, так что эти три "лишние" класса никакого замедления не дадут.

Согласен, писанины немного больше, но зато полная гарантия от опечаток и, как следствие, глупых, но коварных ошибок.

Возражения есть?

18 комментариев:

  1. >>Возражения есть?

    Все верно написано - в таких местах можно и удобно это применять. Можно даже расширить классы и проверять месяц и день на правильность, кидать исключения в дебаге при ошибках и т.п.

    ОтветитьУдалить
  2. А просто месяц надо записывать буквами. Тоже вариант.

    ОтветитьУдалить
  3. Date d(2009, 4, 5);

    а что будет если опять вот так написать со всеми этими перегруженными конструкторами?

    ОтветитьУдалить
  4. а вообще наверно лучше всегда придерживаться http://ru.wikipedia.org/wiki/ISO_8601

    ОтветитьУдалить
  5. Будет ошибка компиляции, так как конструкторы вспомогательных типов являются explicit, то есть их неявное использование, например, для преобразования int в Year, запрещено. Если explicit убрать, то съест. Вообще, есть негласное правило хорошего тона - все конструкторы с одним аргументом (а фактически - это конструкторы преобразования или conversion constructors) следует делать explicit, чтобы не иметь их скрытых вызовов, как тут, например. И только когда это явно необходимо, то убирать explicit. Тут как с const - ставь везде, где только можно и убирай потом, если сильно мешает.

    А по поводу ISO - головы людей сложно переделать. Порой проще дать им надежный интерфейс.

    ОтветитьУдалить
  6. Хороший способ. Можно немного уменьшить количество писанины например вот так
    http://codepad.org/rqCE6irX

    ОтветитьУдалить
  7. Для перечислимых типов лучше использовать enum. Это исключает неоднозначтность в толковании (номера месяца, номера дня недели и т.п.). Кроме того, чтобы не перепутать константы месяцев с локальными переменными.именами классов и т.п., желательно поместить их в соответствующий namespace или область видимости класса (что лучше, т.к. у пользователя не получится сказать "using" и устроить бардак):
    class Date {
    public:
    enum Month {
    January, February, March,
    April, May, June, July, August,
    September, October, November, December
    };
    Date (Year year, Month month, Day day);
    // ...
    }; // class Date

    Если не использовать "наглых" преобразований (типа Date::Month(i)), код получится читабельнее и проще.
    Пользователю класса не нужно искать ответ на вопросы типа "4 - это апрель или май?", его программы тоже становится легче читать и сопровождать.

    ОтветитьУдалить
  8. Поздравляю, вы изобрели именованные параметры :)
    Welcome to Objective-C!

    Ну а вообще, удобная штука.

    ОтветитьУдалить
  9. задумка правильная, но мне не нравится это решение.
    во-первых потому, что нужно создавать дополнительные классы (в основном мое недовольство заключается в синтаксическом мусоре + создании лишних объектов, хотя не уверен, что тут это сильно попортит)
    во-вторых тем, что нужно создать 3! (факториал) конструкторов для всех вариантов дат.
    а если в другом случае будет 10 чисел?

    имхо, есть 2 более логичных способа.
    1) (мне этот нравится больше) основан на идиоме именованного параметра: http://www.parashift.com/c++-faq-lite/ctors.html#faq-10.18

    нужно ввести всего 3 метода ВНУТРИ класса и переменную bool is_inited если все 3 значения переданы и они корректны.

    2) основан на идиоме именнованного конструктора: http://www.parashift.com/c++-faq-lite/ctors.html#faq-10.8

    вообщем-то нужно так же опредять факториал таких методов с именами типа DateYMD() как и в оригинальном способе, но тут это все внутри класса -- этот способ мне нравится меньше, хотя, говорят в с# такой способ создания объектов постоянно используется.


    хотя вообщем-то до питоновский именованных аргументов foo(year=2000, month=12, day=31) им всем далеко))

    ОтветитьУдалить
  10. Способ №1 весьма красивый. Только как мне кажестся, тут одной переменной is_inited не обойтись, так как изначально она false и функция для задания каждого аргумента должна сказать, что она вызвана, а is_inited только одна. Получается для каждого аргумента нужна своя is_XXX_inited, или сделать is_inited битовым полем, но тут уже какая-то измена наблюдается ;-)

    И кстати, фишка метода, что я описал в том, что в принципе мне не нужно делать факториал конструкторов, и я могу описать только один. Конечно, всем придется пользоваться моей нотацией, и люди могу жаловаться на нелогичность с их точки зрения в порядке параметров, но будет решена полюбому главная задача -- никто не сможет вызвать мой конструктор неправильно, а это уже 99% задачи.

    ОтветитьУдалить
  11. Как насчет такой реализации идеи различимости смысла параметров? Вроде тоже самое в итоге, но код попроще на мой взгляд. И конечно месяцы можно проименовать, как в комментарии до меня.

    enum Year {};
    enum Month {};
    enum Day {};

    class Date
    {
    public:
    Date(Year year, Month month, Day day);
    Date(Day day, Month month, Year year);
    Date(Year year, Day day, Month month);
    };

    используем так же:
    Date d(Year(2009), Month(1), Day(28));

    ОтветитьУдалить
  12. Неплохой вариант. Странно как-то получается инициализировать enum числом...

    ОтветитьУдалить
  13. >>полная гарантия от опечаток

    Полная гарантия от опечаток бывает только если ничего не писать совсем.

    ОтветитьУдалить
  14. Встретил вчера в сети "C++ FAQ Lite".
    Хотел дать ссылку на идиомы именованных параметров методов и конструктора, но теперь вижу, что товарищ f0b0s меня опередил)) Поразмыслю над универсализацией способа с именованными параметрами метода.

    ОтветитьУдалить
  15. А я бы поступил так: www.pastebin.org/73505
    Это, я, конечно, кофию перепил, но всё же чем не вариант. Применять так.
    Date pigDay;
    ...
    pigDay.year(1961).month(4).day(15);

    ОтветитьУдалить
  16. Красиво, но тут нарушается атомарность конструирования и инициализации.

    ОтветитьУдалить
  17. статическая функция создания вас спасет, но писаныни будет ещё больше

    ОтветитьУдалить