понедельник, 14 декабря 2009 г.

[comp.prog.flame] null-указатели и притягивание за уши решений из функциональных языков

В RSDN-овском обсуждении Billion-dollar mistake тов.Кодёнок продемонстрировал, как тип Maybe из функциональных языков:

data Maybe a = Nothing | Just a

мог бы использоваться в C++/C# для сокращения количества проблем с null-указателями:

struct Maybe<A> {};
struct Nothing<A> : Maybe<A> {};
struct Just<A> { A value; };

void fun(Maybe<A> x)
{
    print(x.value); // type ERROR

    if (x is Just<A>)
        print( (Just<A>)x.value ); // OK
}

Мои скромные познания в C# позволяют думать, что это пример C#-ного кода. В C++ такой подход выглядел бы более сурово, за счет использования dynamic_cast-ов или каких-то других фокусов. Поэтому я думаю, что подход с типами Maybe, Nothing и Just в C++ не прошел бы. Для C++ нужно было бы иметь что-нибудь более заточенное под C++, а не кальку с функционального языка.

Например, можно было бы использовать Boost-овский Optional. Что-то вроде:

// Здесь мы не должны получать NULL.
void g( A & a ) { ... }

// А вот здесь мы можем получить NULL или не-NULL.
void fun(boost::optional<A*> x)
{
    if (x)
        g( **x ); // OK
}

Но для Boost.Optional null-указатель является допустимым значением. Поэтому можно было бы создать более простой аналог Boost.Optional, заточенный специально под хранение указателей:

// Класс, который не может хранить внутри себя нулевые указатели.
template< class T >
class not_null_ptr
  {
  public :
    not_null_ptr( T * ptr )
      // Вспомогательная функция ensure_not_null порождает исключение
      // если получает нулевой аргумент.
      : p_( ensure_not_null( ptr ) )
      {}
    ...
    T * get() const { return p_; }
    ...
  private :
    T * p_;
  };

// Класс, который может хранить внутри себя нулевые указатели.
template< class T >
class nullable_ptr
  {
  public :
    nullable_ptr( T * ptr )
      : p_( ptr )
      {}
    ...
    bool is_null() const { return !p_; }

    // Безопасный доступ к указателю.
    not_null_ptr<T> get_not_null() const { return not_null_ptr<T>( p_ ); }

    // Прямой доступ к указателю.
    T * get_any() const { return p_; }
    ...
  private :
    T * p_;
  };

// Здесь мы не должны получать NULL.
void g( not_null_ptr< A > a ) { ... }

// А вот здесь мы можем получить NULL или не-NULL.
void fun( nullable_ptr< A > x )
{
    if( !x.is_null() )
        g( x.get_not_null() ); // OK
}

Такой подход мне нравится еще и тем, что если у нас нет желания обрабатывать нулевой указатель, мы можем сразу вызывать get_not_null и получать либо не нулевой указатель, либо исключение.

PS. Это всего лишь набросок того, что я бы хотел видеть в C++ в качестве средств борьбы с нулевыми указателями вместо Maybe. Реальное решение должно быть более продвинутым. Например, должны решаться вопросы владения указателем (совмещение nullable_ptr с auto_ptr/unique_ptr и/или shared_ptr). Кроме того, в C++ null-указатель еще не самая страшная проблема – повисшие и запорченные указатели куда страшнее.

Но я хотел показать, что просто так тянуть Maybe из функциональных языков в C++ не нужно. Поскольку в каждом языке есть свои собственные тараканы, с коими нужно считаться.

PPS. Вот интересно, если бы шаблоны в C++ были с самого начала, могли ли классы вроде nullable_ptr/not_null_ptr появиться в стандартной библиотеке C++ сразу? Чтобы функции std::strcpy имели прототип:

not_null_ptr<char> strcpy(
  not_null_ptr<char> dest,
  const_not_null_ptr<char> src)

:)

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

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

Конечно, под C++ более правильным будет аналог boost::optional, но суть же останется той же. Да и такой тип не обязан вообще-то хранить в себе именно указатели, в том же окамле он скорее хранит значения, хотя может и ссылки конечно.

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

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

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

Void-safety - вот, на мой взгляд, правильное решение из языка Eiffel. Жаль, что работает пока только в экспериментальном режиме. Однако обещают в ближайшее время допилить, ведь необходимо произвести изменения во всем коде. Но тенденция уже видна - все новые дополнения и изменения будут выходить только в void-safety варианте.

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

2Quaker: вроде как на днях вышла EiffelStudio 6.5. Может там статус с экспериментального сменился на стабильный?

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

2Rubanets Myroslav:

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

А можно эту мысль развернуть (слайды! слайды! -- скандировали зрители :)

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

>Может там статус с экспериментального сменился на стабильный?

Пока нет: http://n2.nabble.com/Re-EiffelStudio-6-5-releases-td4146106.html#a4146106

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

2Евгений Охотников:
эхх да где ж его времени столько взять то ... Может быть наскребу на блог пост :) тогда и будет. (те. пока неизвестно когда).

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

2Rubanets Myroslav: ничего, подождем