среда, 5 августа 2009 г.

[comp.prog.cpp] Маленькое изобретение – проверка корректности аргументов в конструкторе

Наверняка все уже украденоизобретено до нас, но я раньше такого не видел. Поэтому порадовался, когда до этого додумался. Итак, конструктор какого-то C++ класса получает набор аргументов. На аргументы накладываются некоторые требования. Нужно порождать исключение в конструкторе, если пользователь не выполняет эти требования. Как это обычно делается? Наверное, как-то вот так:

class some_class_t {
  private :
    // Имя должно состоять из определенного набора символов и
    // длина имени должна быть не меньше N и не больше M.
    const std::string m_name;
    // Час, должен быть в диапазоне [0..23].
    const unsigned int m_hour;
    // Минута, должна быть в диапазоне [0..59].
    const unsigned int m_minute;
  ...
  public :
    // Вот и конструктор.
    some_class_t(
      const std::string & name,
      unsigned int hour,
      unsigned int minute )
      // Вот это важно! Сначала атрибуты получают значения...
      : m_name( name )
      , m_hour( hour )
      , m_minute( minute )
      {
        // ...а уже потом выполняется проверка.
        ensure_name_validity( m_name );
        ensure_hour_validity( m_hour );
        ensure_minute_validity( m_minute );
        ...
      }
  ...
};

Что здесь нехорошо? А то, что если инициализация аргументов дорогая, то мы сначала платим цену инициализации, а только затем начинаем делать проверки. Но ведь ее можно и не платить ;) Вот так:

// Вот такие валидаторы нам нужны:
const std::string &
ensure_name_validity( const std::string & arg ) { ...; return arg; }

unsigned int
ensure_hour_validity( unsigned int arg ) { ...; return arg; }

unsigned int
ensure_minute_validity( unsigned int arg ) { ...; return arg; }

class some_class_t {
  ...
  public :
    // Вот и конструктор.
    some_class_t(
      const std::string & name,
      unsigned int hour,
      unsigned int minute )
      // Вот это важно! Параметры проверяются до
      // инициализации атрибутов.
      : m_name( ensure_name_validity( name ) )
      , m_hour( ensure_hour_validity( hour ) )
      , m_minute( ensure_minute_validity( minute ) )
      {
        ...
      }
  ...
};

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

А еще эту идею можно развить до явной декларации требований к атрибутам. Вот, допустим, так можно явно прописать в коде, какие требования налагаются на параметр name конструктора нашего демонстрационного класса:

// Валидатор параметра name.
struct valid_name_t {
  const std::string & m_name;
  valid_name_t( const std::string & name )
    : m_name( ensure_name_validity( name ) )
    {}
};

// Теперь требование к аргументу name может быть указано
// прямо в описании конструктора.
class some_class_t {
  public :
   some_class_t(
     const valid_name_t & name,
     ... )
     : m_name( name )
     ...
};

По большому счету, развивать этот подход можно и дальше. Таким образом можно прописывать требования не только к параметрам конструктора, но и аргументам обычных функций/методов. В общем, если где-то нужно применять defensive programming, то есть с чем поиграться.

PS. Хочу поделиться маленькой радостью :) Количество загрузок Mxx_ru со страницы RubyForge достигло круглого числа 1400! Если учесть еще и количество загрузок через RubyGems, то получается, что Mxx_ru скачали больше 2100 раз. Что не может не радовать (хотя, смотря с чем сравнивать ;). Так что есть повод еще раз сказать спасибо всем пользователям Mxx_ru: спасибо большое!

3 комментария:

Kodt комментирует...

Я двойственно отношусь к выносу валидаторов в объявления.
С одной стороны, это бьёт по пальцам на как можно более ранней стадии.
С другой стороны - загромождает объявление, добавляет к нему новые зависимости.

Также возникают заморочки, связанные с неявным кастингом.
Т.е. если у нас есть
checked_value<int,0,23> m_hour;
то придётся тщательно следить за его использованием в перегруженных функциях (а особенно - в шаблонных).
А если у нас есть
foo::foo(checked_value<int,0,23> hour, .....)
то это предмет для спотыкания при передаче туда не int непосредственно, а приводимых к int типов.

-----
Ещё одна проблема - это невозможность вводить сложные проверки. Поскольку каждая проверка относится только к своему аргументу, а друг про друга они не знают.

Можно, кстати, вот такой фокус сделать:

#define CHECK_N_GO(cond, value) \
((cond) ? (value) : (throw std::shit_happened))

foo::foo(int x, int y, int z) :
m_x( CHECK_N_GO(x+y+z==0, x) ),
m_y(y),
m_z(z)
{}

Для нелюбителей макросов - пишем аналогичный шаблон
template<class T>
T const& check_n_go(bool cond, T const& value)
{ return CHECK_N_GO(cond,value); }
Только помним, что вызов функции жадный, в отличие от тернарного оператора, и мы рискуем зазря вычислить value.

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

Kodt комментирует...

Повбываб бы этот блоггер...
У жж куда удобнее всё сделано.
Особенно, с комментариями.

eao197 комментирует...

Вероятно, неудобной системе комментариев так же есть смысл. Не позволяет она устраивать многостраничные флеймы. Наверное, смысл блоггера его разработчики видели в том, чтобы автор поделился своей информацией, а читатели что-то ему в ответ сказали. Если нужно что-то проянить, то делать это нужно уже не в комментариях, а в новом блог-посте.

Вероятно, про валидаторы в объявлениях я еще напишу. Поскольку здесь я с тобой почти на 100% согласен.