пятница, 14 августа 2009 г.

[comp.prog.cpp] Замечения Кодт-а к идее проверки аргументов

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

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

Совершенно верно. Если не ошибаюсь, на RSDN-не несколько лет назад обсуждалась идея шаблонов-валидаторов. Например, шаблонный класс checked_value<T, min, max>, который бы проверял попадание аргумента в диапазон [min, max] (да и сам Кодт является автором в чем-то похожей идеи auto_value<>). Я тогда покурил эту тему и решил в работе подобные вещи не использовать. Поскольку код программы засоряется валидаторами, но эффект от них оказывается не очень существенным (на практике они ловили ошибки даже реже, чем assert-ы). При более сложных проверках (например, число должно не просто попадать в диапазон, но и быть, например, четным) объявления валидаторов становились еще более суровыми. Что, в итоге, снижало гибкость программы при изменении требований к ней.

Еще одну проблему с валидаторами озвучил Кодт:

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

Именно поэтому, когда я описывал свою идею, я сделал акцент на проверке параметров конструкторов класса. Чтобы проверка осуществлялась не в месте вызова, а при инициализации атрибутов класса. Поскольку в некоторых случаях инициализация атрибута конструктором по умолчанию может быть слишком дорогой. Т.е. когда вот такой сценарий невыгоден из-за ресурсоемкости:

some_class_t::some_class_t( some_type_t & arg )
  : m_attr() // Инициализация атрибута по умолчанию.
      // Может быть слишком дорогой при выделении
      // памяти, обращении к системным ресурсам и пр.
  {
    // Валидация аргумента.
    if( !is_valid( arg ) )
      throw std::domain_error( ... );
    // Вот теперь правильная инициализация аргумента.
    // Переинициализация еще и потребует освобождения
    // ресурсов, захваченных в конструкторе по умолчанию,
    // что так же скажется на производительности.
    m_attr = arg;
  }

или же когда у атрибута вообще нет конструктора по умолчанию, то описанные мной валидаторы вполне могут помочь.

Но и здесь нужно учесть хорошее замечание Кодт-а:

Ещё одна проблема - это невозможность вводить сложные проверки.
Поскольку каждая проверка относится только к своему аргументу, а друг
про друга они не знают.
 
Можно, кстати, вот такой фокус делать:
 
#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.
 
Конечно, проверку надо вешать на самый первый конструируемый член (не
забываем, что в списке конструирования они могут идти не в том порядке).

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

void do_something_with_name(
  const std::string & name,
  const std::list< std::string > & aliases );

мне кажется неудачной. Глядя на нее невозможно догадаться о связи аргументов name и aliases. Лучше их объединить в структуру, и передавать в do_something_with_name ссылку на ее экземпляр:

struct name_and_aliases_t {
  const std::string & m_name;
  const std::list< std::string > & m_aliases;
  ...
};
void do_something_with_name( const name_and_aliases_t & name );

Конструктор name_and_aliases_t может проверять нужное нам условие. А если в release-версии программы эту проверку нужно будет исключить, то сделать это можно будет при помощи #if-ов в теле конструктора.

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

Комментариев нет: