Рассказывал на днях своей команде о маленьком трюке, который может использоваться для работы с опциональными значениями. Поясню суть проблемы на примере. Допустим, у нас есть структура 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 и плавного переползания на него.
В С++11 разве нельзя обойтись без указания типа?
ОтветитьУдалитьТам же есть auto...
В типах аргументов auto вроде как использовать нельзя. По крайней мере VS2010 ругается.
ОтветитьУдалитьPS. Думаю, что в С++ сделать такой вывод не просто с учетом наличия шаблонов.
auto_ptr depricated
ОтветитьУдалитьhttp://stackoverflow.com/questions/2404115/is-auto-ptr-deprecated
@Леша Сырников
ОтветитьУдалитьЯ в курсе. Имхо, очередное маразматичное решение коммитета. Оставляют ту же самую байду, но под именем unique_ptr. При этом объявляя устаревшим имя, которое давным-давно используется и грабли которого давно изученны и описанны.
Да, велосипед полезный. Постил как-то подобное на рсдн.
ОтветитьУдалитьнадо бы еще аллокатор добавить :)
а лямбда там не нужна, по-моему.
особого толку от нее не будет.
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 в объявлении функций
А зачем function?
ОтветитьУдалитьУ вас все равно inline-методы, можно и так:
template
void when_defined( T value_handler ) const
{
if( defined() )
value_handler( m_value );
}
@night beast:
ОтветитьУдалитьЛямбда упрощает обработку значения в рамках какого-то контекста.
@имя:
ОтветитьУдалитьНет, макросы привлекать совсем не хочется. К тому же я не понимаю, как decltype может помочь выводу типа аргумента.
@Eugeniy:
ОтветитьУдалитьДа, вы правы, можно и без function обойтись.
Просто я экспериментировал с типами, к которым можно приводить лямбды, вот function и появились в декларациях.
По моему наиболее близкий аналог паттерн матчинга в С++ реализован в boost::variant http://www.boost.org/doc/libs/1_47_0/doc/html/variant.html#variant.motivation.solution правда получается более громоздко чем у тебя, но в принципе терпимо, я например достаточно активно использую.
ОтветитьУдалитьпохоже только как я сказал -- макросом с decltype
ОтветитьУдалитьну или по-старому, юзая boost::function
ОтветитьУдалить@Rustam
ОтветитьУдалитьboost.variant решает более общую задачу. И кроме визитора сложно что-то еще прикрутить. Тем более в рамках C++03.
@имя
ОтветитьУдалитьНе, там какое-то частное решение для векторов. Да еще не понятно, действительно код понятнее и компакнее получается или нет.
он не особо компактнее становится, и это был в основном показ как в макросе использовать dectlype
ОтветитьУдалитьвот вроде бы достаточно окончательный ответ о том, что эта хрень называется polymorphic lambda и что ее в 0х нет:
http://stackoverflow.com/questions/3575901/can-lambda-functions-be-templated
пардон май френч, но вам надо в отпуск или к доктору. Предлагать клиенту вместо ифа писать 2 лямбды это жесть.
ОтветитьУдалить@Miroslav:
ОтветитьУдалитьНе предлагаю, а сам попробовал. С негативным результатом.