среда, 30 марта 2011 г.

Валидация параметров конструктора класса в секции инициализации

Принимая параметры-указатели в конструктор, иногда полезно их проверять на валидность:


MyClass::MyClass (A* a, B* b)
{
if (!a) throw std::invalid_argument("a");
if (!b) throw std::invalid_argument("b");
a_ = a;
b_ = b;
}

Красивее (да и правильнее) было бы выполнять присвоение в секции инициализации:

MyClass::MyClass (A* a, B* b)
:a_(a), b_(b)
{
if (!a) throw std::invalid_argument("a");
if (!b) throw std::invalid_argument("b");
}

Недостатки налицо: некрасиво и не очень функционально. Присвоение выполняется еще до проверок. Кроме того, бывает еще такая ситуация:


class MyClass
{
A* a_;
B& b_;

MyClass(A* a)
:b_(a->getRefB())
{
}
}

Проблема в том, что если в качестве параметра будет передан невалидный указатель, то будет сгенерировано системное исключение еще до вызова конструктора. А переносить инициализацию внутрь конструктора нельзя: ссылка может быть проинициализирована только в секции инициализации класса.
Возможный выход в том, чтобы не попадать в подобную ситуацию. То есть – не инициализировать ссылку через указатель на объект. Но если мы хотим использовать Dependency Injection вместе со средствами C++ на полную катушку, то такая ситуация рано или поздно возникнет.
Идея состоит в том, чтобы проверить валидность указателя именно в секции инициализации. Помогут функции с шаблонными параметрами.

Итак, возможные ошибки с параметрами:
- передан нулевой указатель
- передан указатель на объект несовместимого типа
- передан «мусор»: значение, не являющееся указателем вовсе
Для проверки на соответствие указателя ожидаемому типу будем использовать RTTI.
Напишем функции с шаблонным параметром, которые смогут проверить указатель на ошибки:


template<typename T>
T NotNullChecker (T value, std::string variableName)
{
if (value == NULL)
throw std::invalid_argument(variableName + " cannot be NULL");

return value;
}

template<typename T>
T ConvertableTypeChecker(T value, std::string variableName)
{
if (dynamic_cast<T>(value) == NULL)
throw std::invalid_argument(variable + " must be convertible to type " + std::string(typeid(T).name()));

return value;
}

Для удобства допишем пару макросов для автоматической подстановки имен параметров:


#define NotNull(p) NotNullChecker(p,#p)
#define ConvertableType(p) ConvertableTypeChecker(p,#p)
#define ValidArgument(p) ConvertableTypeChecker(NotNullChecker(p,#p),#p)

Использование выглядит очень лаконично на мой взгляд:

class MyClass
{
A* a_;
B& b_;

MyClass(A* a)
:b_(ValidArgument(a) ->getRefB())
{
}
}

Вот так можно проверить аргумент на валидность еще в секции инициализации. Если развить идею дальше – можно написать таким же образом функцию проверки целочисленного аргумента на попадание в допустимый диапазон и т.д.

1 комментарий:

  1. Прибежав из лагеря лисперов, спешу показать аналогичный функционал контрактного программирования на clojure. Не спора ради, а пользы только для.
    Итак, функция f(x), которая определена на промежутке (0, 24), иначе генерируется java.какой-то-там.Exception.

    (defn f [x]
    {:pre [(pos? x)]
    :post [(> % 0), (< % 24)]}
    (* x x))


    Ссылки

    http://alexott.net/ru/clojure/clojure-intro/#sec12
    http://blog.fogus.me/2009/12/21/clojures-pre-and-post/
    http://blog.fogus.me/2010/05/25/trammel-contracts-programming-for-clojure/
    http://my-clojure.blogspot.com/2011/01/java-null.html

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