Наверняка все уже украденоизобретено до нас, но я раньше такого не видел. Поэтому порадовался, когда до этого додумался. Итак, конструктор какого-то 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: спасибо большое!
Я двойственно отношусь к выносу валидаторов в объявления.
ОтветитьУдалитьС одной стороны, это бьёт по пальцам на как можно более ранней стадии.
С другой стороны - загромождает объявление, добавляет к нему новые зависимости.
Также возникают заморочки, связанные с неявным кастингом.
Т.е. если у нас есть
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.
Конечно, проверку надо вешать на самый первый конструируемый член (не забываем, что в списке конструирования они могут идти не в том порядке).
Повбываб бы этот блоггер...
ОтветитьУдалитьУ жж куда удобнее всё сделано.
Особенно, с комментариями.
Вероятно, неудобной системе комментариев так же есть смысл. Не позволяет она устраивать многостраничные флеймы. Наверное, смысл блоггера его разработчики видели в том, чтобы автор поделился своей информацией, а читатели что-то ему в ответ сказали. Если нужно что-то проянить, то делать это нужно уже не в комментариях, а в новом блог-посте.
ОтветитьУдалитьВероятно, про валидаторы в объявлениях я еще напишу. Поскольку здесь я с тобой почти на 100% согласен.