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())
{
}
}
Вот так можно проверить аргумент на валидность еще в секции инициализации. Если развить идею дальше – можно написать таким же образом функцию проверки целочисленного аргумента на попадание в допустимый диапазон и т.д.
Прибежав из лагеря лисперов, спешу показать аналогичный функционал контрактного программирования на 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