понедельник, 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)

:)

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