пятница, 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 и плавного переползания на него.

18 комментариев:

Andrey Valyaev комментирует...

В С++11 разве нельзя обойтись без указания типа?

Там же есть auto...

eao197 комментирует...

В типах аргументов auto вроде как использовать нельзя. По крайней мере VS2010 ругается.

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

Анонимный комментирует...

auto_ptr depricated

http://stackoverflow.com/questions/2404115/is-auto-ptr-deprecated

eao197 комментирует...

@Леша Сырников

Я в курсе. Имхо, очередное маразматичное решение коммитета. Оставляют ту же самую байду, но под именем unique_ptr. При этом объявляя устаревшим имя, которое давным-давно используется и грабли которого давно изученны и описанны.

night beast комментирует...

Да, велосипед полезный. Постил как-то подобное на рсдн.
надо бы еще аллокатор добавить :)
а лямбда там не нужна, по-моему.
особого толку от нее не будет.

имя комментирует...

decltype возможно спасет от указания типа (и да, все это надо обернуть в макрос)

MATCH_OPTIONAL( opt_val, { std::cout << value; }, { std::cout << "null"; })

или даже так (если получится):

MATCH_OPTIONAL opt_val AS value THEN std::cout << value; ELSE std::cout << "null"; END

возможность не проверял, но жцц ховает decltype в объявлении функций

Eugeniy комментирует...

А зачем function?

У вас все равно inline-методы, можно и так:

template
void when_defined( T value_handler ) const
{
if( defined() )
value_handler( m_value );
}

eao197 комментирует...

@night beast:

Лямбда упрощает обработку значения в рамках какого-то контекста.

eao197 комментирует...

@имя:

Нет, макросы привлекать совсем не хочется. К тому же я не понимаю, как decltype может помочь выводу типа аргумента.

eao197 комментирует...

@Eugeniy:

Да, вы правы, можно и без function обойтись.

Просто я экспериментировал с типами, к которым можно приводить лямбды, вот function и появились в декларациях.

Rustam комментирует...

По моему наиболее близкий аналог паттерн матчинга в С++ реализован в boost::variant http://www.boost.org/doc/libs/1_47_0/doc/html/variant.html#variant.motivation.solution правда получается более громоздко чем у тебя, но в принципе терпимо, я например достаточно активно использую.

имя комментирует...

похоже только как я сказал -- макросом с decltype

имя комментирует...

ну или по-старому, юзая boost::function

eao197 комментирует...

@Rustam

boost.variant решает более общую задачу. И кроме визитора сложно что-то еще прикрутить. Тем более в рамках C++03.

eao197 комментирует...

@имя

Не, там какое-то частное решение для векторов. Да еще не понятно, действительно код понятнее и компакнее получается или нет.

имя комментирует...

он не особо компактнее становится, и это был в основном показ как в макросе использовать dectlype

вот вроде бы достаточно окончательный ответ о том, что эта хрень называется polymorphic lambda и что ее в 0х нет:

http://stackoverflow.com/questions/3575901/can-lambda-functions-be-templated

Miroslav комментирует...

пардон май френч, но вам надо в отпуск или к доктору. Предлагать клиенту вместо ифа писать 2 лямбды это жесть.

eao197 комментирует...

@Miroslav:

Не предлагаю, а сам попробовал. С негативным результатом.