Итак, приступив к поддержке одного относительно большого проекта на MFC, реализующим тонкий биллинг-клиент, я обнаружил, что каждый раз, когда в БД необходимо передать NULL, присутствует дополнительный код, содержащий проверку типа этой (условно):
//...
void CSomeClass::someFunction(double variable)
{
//...
if (variable == 0)
{
db->addValue(NULL);
}
else
{
db->addValue(variable);
}
//…
Когда я столкнулся с необходимостью реализации новой фичи в данном проекте, требующей чтения и записи значений NULL, мне тут же захотелось встроить в проект поддержку nullable-типов, которая обеспечит простое и безопасное в плане утечек памяти решение для их создания, присвоения им разных значений, копирования и передачи в те уже готовые функции, которые принимают на вход указатель на значение в памяти.
До сих пор мне не приходилось пользоваться библиотекой Boost, но я слышал о ней и подумал: «Отлично, самое время её пощупать». Тем более, что «гугление» на тему «c++ nullable type» так или иначе жирно намекает на Boost::Optional.
Однако я очень быстро понял, что Boost::Optional мне не подходит (и/или не нравится). Во-первых, её изучение и подключение к проекту не входило в оценку времени работы по задаче, во-вторых, я понял (возможно, тут я ошибся) что получу проблемы при конвертации из Boost::Optional в обычный “указательный” тип C (как минимум, это будет выглядеть не очень красиво в коде), иными словами, я совершенно не был готов к тому, что столкнусь со сложностью для меня в её использовании. Может быть, всё дело в том, что я имел за спиной прочитанную книгу Jeff Alger «C++ for real programmers», в голове имелись различные решения для реализация всяких умных (и не очень) указателей и перегрузки операторов, и поэтому мне было не понятно, почему для реализации nullable-типа, которая мне виделась довольно простой (для моих нужд), нужен такой монстр как Boost::Optional.
А нужды мои следующие (сразу на примере использования моего nullable-велосипедика):
class FooModel
{
public:
NullableType<int> intParam;
};
FooModel* model = new FooModel(); // Хочу, чтобы по-умолчанию моя nullable-переменная была равна NULL
bool resBool = model->intParam == nullptr; // Хочу уметь сравнивать значение моей переменной с NULL
model->intParam = 654; // Хочу “просто взять и присвоить” ей значение соотв. типа
resBool = model->intParam == nullptr; // Хочу иметь возможность “просто взять и приравнять” её NULL, не думая об утечках памяти
NullableType<int> globIntParam1 = model->intParam; // Хочу простого копирования значений, не думая об утечках и выделении памяти
int* intVarPtr1 = new int(23); // Хочу полной совместимости с нативными C-указателями
int* intVarPtr2 = NULL;
model->intParam = intVarPtr1; // Хочу чтобы и так можно было
model->intParam = intVarPtr2; // и так тоже. (Корректная передача значения в функции, принимающие указатель)
NullableType<int> globIntParam2(intVarPtr); // А заодно пусть будет и конструктор копирования
Мне нужна простая обёртка над встроенными простыми С-типами, позволяющая присваивать им NULL-значение, нечто вроде int? или double? в C#, без поддержки присвоения NULL «всему, что угодно». (На всякий случай, скажу, что речи об использовании фич C++11 и выше быть не может). Немного погуглив, я нашёл парочку похожих реализаций, но меня смущало отсутствие в них всех нужных мне возможностей и я написал свою реализацию nullable-обёртки.
Если я прав, то нечто подобное нужно во многих C++-проектах. Поэтому тяжело представить, что до сих пор гугл не выдаёт простое решение по этому вопросу.
Моя обёртка имеет проблему — её нельзя бездумно использовать со строками, т.к. при инициализации “обёрнутой” в неё строки значением NULL нужно как-то сделать так, чтобы при попытке конвертации в string получить из NULL пустую строку. В комментариях к исходникам есть вариант быстрого решения (проверено в gcc и clang, всё ок), но он может не всем понравиться. Ещё эту проблему можно решить для MFC в частном порядке, добавив поддержку CString, которые в нём используются вместо обычного string, но я не стал этого делать, чтобы не привязывать её к MFC, да и до сего момента мне просто не потребовалась запись NULL-значения в БД вместо пустой строки.
Буду раз различным предложениям и критике, возможно, я не приложил достаточно усилий чтобы найти готовое простое решение этой задачи.
Исходный код описанного nullable-класса находится тут.
Комментарии (10)
georgthegreat
13.07.2015 13:22+1У вас, по сути, вышел умный указатель с переопределёнными операторами.
boost::optional не использует кучу.
tangro
13.07.2015 13:39+1Непонятно, что именно «монструозного» Вы нашли в boost::optional? Исходники его читали разве что — а зачем? А в использовании он крайне прост.
roman_kashitsyn
13.07.2015 15:07+2Почему методы виртуальные?
Зачем выделять место под примитивные типы на куче?
Почему конструктор принимает объект по значению, но move-семантика не используется?
NullableType<T>(T value) { this->value = new T(value); };
Ну иboost::optional
прекрасно конвертируется в указатель:optional::get_ptr()
. Кроме того, любая итераторо-подобная вещь конвертируется в указатель конструкцией&*it
;
Итого потратили время на медленную негибкую реализацию Nullable, когда в бусте есть эффективная, удобная и отлаженная библиотека optional.ByGandalf Автор
14.07.2015 00:15-1Спасибо, конечно работа с кучей медленнее, но это скажется при большом количестве таких объектов или операций с переприсваиванием.
Методы виртуальные зря, просто по давней привычке чтобы можно было их переопределить в классе наследнике.
В целом пост этот для того, чтобы собрать где-то в одном месте сведения об использовании nullable и дать общее представление об этом вопросе для того, кто с таким не сталкивался, т.к. я в своё время просто не нагуглил бытрого и простого решения.
AMDmi3
13.07.2015 17:16+1У вас действительно получился не optional, а очень обрезанный умный указатель.
На самом деле optional давно есть из коробки в современных c++ библиотеках (libstdc++ и libc++): en.cppreference.com/w/cpp/experimental/optional, но если на них завязываться не хочется, а монструозность boost пугает, вместо того чтобы опускаться до написания велосипеда достаточно взять один заголовочный файл из любой из трёх упомянутых библиотек. Я взял из libc++, потому что у неё самый чистый и понятный код, получилось так:
github.com/AMDmi3/libSDL2pp/blob/master/SDL2pp/external/libcpp_optional.hhByGandalf Автор
13.07.2015 23:43Спасибо, очень хотелось ссылки на нечто подобное. Думаю в целом, благодаря комментариям, данный пост может оказаться полезен
encyclopedist
13.07.2015 18:08+1На всякий случай, скажу, что речи об использовании фич C++11 и выше быть не может
Но nullptr — это же C++11 !?ByGandalf Автор
13.07.2015 23:57Прошу прощения, я имел в виду тот факт, что мой проект будет компилироваться только в VS2012, и ограничен только тем, что поддерживается в ней.
В данном случае nullptr просто лучше выглядит нежели (int*)NULL
MaximChistov
Одна из основных идей nullable — безопасные вложенные вызовы, например на Java:
И никаких проверок на null в коде в итоге. Может ли так ваше решение?
SerCe
Судя по всему, автору требовалась монада, а лишь умный указатель.