Статья рассказывает историю о том, как автора напугал монструозный Boost и как он придумал свой маленький nullable-велосипед. Возможно, статья будет полезна тем, кто хочет использовать простые nullable-типы в своём проекте без использования Boost, отвечающие приведённым в статье не самым простым требованиям.

Итак, приступив к поддержке одного относительно большого проекта на 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)


  1. MaximChistov
    13.07.2015 13:16
    +1

    Одна из основных идей nullable — безопасные вложенные вызовы, например на Java:

    return Optional.ofNullable(this::getApi)
              .map(CAPI::getDepartment)
              .map(CDepartment::getTitle)
              .orElse("");
    

    И никаких проверок на null в коде в итоге. Может ли так ваше решение?


    1. SerCe
      13.07.2015 13:23
      +1

      Судя по всему, автору требовалась монада, а лишь умный указатель.


  1. georgthegreat
    13.07.2015 13:22
    +1

    У вас, по сути, вышел умный указатель с переопределёнными операторами.
    boost::optional не использует кучу.


  1. tangro
    13.07.2015 13:39
    +1

    Непонятно, что именно «монструозного» Вы нашли в boost::optional? Исходники его читали разве что — а зачем? А в использовании он крайне прост.


  1. 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.


    1. ByGandalf Автор
      14.07.2015 00:15
      -1

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

      В целом пост этот для того, чтобы собрать где-то в одном месте сведения об использовании nullable и дать общее представление об этом вопросе для того, кто с таким не сталкивался, т.к. я в своё время просто не нагуглил бытрого и простого решения.


  1. 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.hh


    1. ByGandalf Автор
      13.07.2015 23:43

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


  1. encyclopedist
    13.07.2015 18:08
    +1

    На всякий случай, скажу, что речи об использовании фич C++11 и выше быть не может

    Но nullptr — это же C++11 !?


    1. ByGandalf Автор
      13.07.2015 23:57

      Прошу прощения, я имел в виду тот факт, что мой проект будет компилироваться только в VS2012, и ограничен только тем, что поддерживается в ней.
      В данном случае nullptr просто лучше выглядит нежели (int*)NULL