пятница, 16 сентября 2011 г.

[prog.c++] Попробовал было C++11 для проблемки работы с опциональными значениями

Рассказывал на днях своей команде о маленьком трюке, который может использоваться для работы с опциональными значениями. Поясню суть проблемы на примере. Допустим, у нас есть структура A. Экземпляр этой структуры должен входить в какую-то другую структуру, скажем, в config_t:

struct config_t
   {
      A m_a;
      // ... какой-то набор других атрибутов ...
   };

Но в качестве “опционального” значения. Т.е., если, скажем, в конфигурационном файле определены параметры для A, то поле config_t::m_a можно использовать. А если не определены, то работать с config_t::m_a нельзя.

В лоб, без привлечения внешних библиотек, эта проблема решается, например, добавлением еще одного булевского атрибута:

struct config_t
   {
      A m_a;
      bool m_a_defined;
      // ... какой-то набор других атрибутов ...
   };

И работа затем будет вестись следующим образом:

config_t cfg = ...;
if( cfg.m_a_defined )
   handle_A( cfg.m_a );

Проблема здесь в том, что необязательное значение m_a и признак этой опциональности разнесены на разные атрибуты. При сопровождении проекта вполне может произойти ситуация, когда новый разработчик не заметит наличие m_a_defined и начнет использовать m_a без дополнительной проверки m_a_defined. Или же выставит значение m_a, но забудет изменить m_a_defined.

Я придерживаюсь мнения о том, что опциональность m_a в config_t нужно выражать на уровне типа атрибута m_a, а не дополнительных атрибутов. Например, сделать типом m_a тип std::auto_ptr<A>. Хотя с указателями есть свои заморочки – если забудешь его проверить на NULL, то получишь крах программы, а не простое C++ное исключение, которое будет содержать в себе полезную информацию. Плюс к тому у std::auto_ptr специфическая семантика передачи владения и для config_t наверняка потребуется определять собственные конструктор и оператор копирования. Ну и может просто оказаться накладным размещать A в динамической памяти.

Поэтому я в таких случаях предпочел бы простенький класс вида:

class opt_A_t
   {
   public :
      opt_A_t() : m_defined( false ) {}
      opt_A_t( const A & v ) : m_value( v ), m_defined( true ) {}

      bool is_defined() const { return m_defined; }

      const A & value() const
         {
            if( !m_defined )
               throw std::runtime_error( "opt_A has no value" );
            return m_value;
         }

   private :
      A m_value;
      bool m_defined;
   };

И поле config_t::m_a объявлял бы как имеющее тип opt_A_t, а не A.

Примечание. Если религия позволяет использовать Boost, то вместо самописного opt_A_t можно задействовать boost::optional<A>. Тем не менее, вполне могут быть случаи, когда собственный opt_A_t окажется удобнее boost::optional.

Тем не менее, работа с opt_A_t или boost::optional<A> все равно оставляет поле для потенциальных ошибок. Поскольку для безопасного обращения к config_t::m_a нужно сначала сделать проверку на наличие в нем значения (boost::optional имеет еще метод get_value_or, который, однако, не применим в ряде нужных мне сценариев). А такая проверка – это лишнее действие, про которое можно и забыть, и выбросить по ошибке.

Тогда как в функциональных языках с алгебраическими типами и паттерн-матчингом есть возможность возложить контроль за корректностью работы с опциональными значениями на компилятор. Например, в Scala определен тип Option[T]. Если бы config_t::m_a имел тип Option[T], то разработчику пришлось бы задействовать для доступа к m_a паттерн-матчинг и компилятор сам бы проверял корректность обращения к значению:

val cfg: Config = ...;
cfg.m_a match {
    case Some[value] => ... // Безопасная работа со значением
    case None => ; // Отсутствие значения нас не интересует
}

В С++ такое, к сожалению, не возможно.

Однако, захотелось попробовать сварганить какое-то подобие в рамках приобщения к C++11. В частности, заменить паттерн-матчинг на методы, которые получают в аргументами лямбда-функции.

В качестве тривиального эскиза получился вот такой шаблонный класс:

template< class T >
class opt_value_t
   {
   private :
      T m_value;
      bool m_defined;

   public :
      opt_value_t() : m_defined( false ) {}
      opt_value_t( const T & v ) : m_value( v ), m_defined( true ) {}

      bool
      defined() const { return m_defined; }

      void
      when_defined( std::function< void(const T &) > value_handler ) const 
         {
            if( defined() )
               value_handler( m_value );
         }

      void
      when_undefined( std::function< void() > nil_handler ) const
         {
            if( !defined() )
               nil_handler();
         }

      void
      handle_both(
         std::function< void(const T &) > value_handler,
         std::function< void() > nil_handler ) const 
         {
            if( defined() )
               value_handler( m_value );
            else
               nil_handler();
         }
   };

Т.е. если нужно обработать ситуацию, когда опциональное значение гарантированно есть, то используется метод when_defined, которому в качестве параметра передается функция-обработчик значения. Аналогично, если нужна обработка отсутствия значения, то вызывается метод when_undefined. Если же хочется сразу учесть оба варианта – то тогда handle_both.

Однако, все это оказалось довольно многословно. Например, если вместо короткого абстрактного имени A задействовать что-то более реальное, тот же std::string, то получается:

opt_value_t<std::string> value( "Sample" );
value.handle_both(
   [](const std::string & v) { std::cout << "VALUE: " << v << std::endl; },
   []() { std::cout << "NIL" << std::endl; } );

Лично мне указание типа параметра для лямбды (в данном случае это const std::string&) портит всю малину :( Если бы можно было писать так:

opt_value_t<std::string> value( "Sample" );
value.handle_both(
   [](v) { std::cout << "VALUE: " << v << std::endl; },
   []() { std::cout << "NIL" << std::endl; } );

было бы намного интереснее :)

В любом случае, лямбды в C++11 – это вкусно. Пора начинать процесс освоения C++11 и плавного переползания на него.

Отправить комментарий