среда, 31 августа 2011 г.

Библиотека boost::signals2 изнутри

Введение
Многие C++-программисты слышали о библиотеке boost. Точнее будет сказать, что это целый конгломерат библиотек, которые можно скачать с сайта http://www.boost.org. Наверное, многие изучали код библиотек в качестве упражнения для ума, или с целью доработки их под свои задачи. И, смею предположить, многие из этих любопытных оставили это занятие в связи с весьма нетривиальной структурой кода этих библиотек. Многие классы, выглядящие для пользователя простыми и понятными, на поверку оказываются хитрым сплетением множества шаблонов, назначение которых иногда ставит в тупик даже весьма продвинутого специалиста. Библиотеки часто оказываются связанными между собой, и изучение внутренностей одной из них подталкивает к ковырянию в соседних. Кроме того, если связанные библиотеки были написаны одной и той же группой авторов, они могут использовать недокументированные возможности друг друга. В этом случае простого прочтения tutorial’а точно не хватает.
Одна из библиотек, входящих в состав boost’а, называется signals2. Её назначение описано в документации в разделе Introduction. Если перевести кратко: библиотека signals2 является реализацией инфраструктуры управляемых сигналов и слотов. Сигналы представляют собой вызывающие сущности, поддерживающие множество точек вызова. В некоторых системах они именуются событиями или источниками вызова. Сигналы подключаются к множеству слотов, иногда называемых приёмниками вызова. Слоты вызываются в тот момент, когда соответствующий сигнал активируется. Интересной особенностью реализации является поддержка отслеживаемых (trackable) объектов, чьи методы вызываются слотами. Объекту слота можно передать слабый указатель на объект (weak_ptr). В случае если объект будет удалён, соответствующее подключение будет разорвано.
В статье предпринята попытка разобраться с назначением основных классов библиотеки изнутри.
ПРЕДУПРЕЖДЕНИЕ
1. В тексте часто используются понятия «интеллектуальный указатель» или «сильная ссылка» (smart_ptr) и «слабая ссылка» (weak_ptr). Для понимания статьи желательно знакомство читателя с другой boost-библиотекой «Smart Pointers».
2. Описание назначения и смысла классов отражает мое понимание библиотеки. Консультаций с авторами библиотеки не проводилось.
ПРИМЕЧАНИЕ
Несмотря на пункт 1 предупреждения, приведу вольное объяснение часто используемых далее распространенных терминов. Интеллектуальный указатель (или сильная ссылка) – объект, эмулирующий обычный указатель, управляющий временем жизни объекта, на который указывает. Когда в программе более не остаётся копий интеллектуального указателя, объект, на который все они указывали, автоматически удаляется. Слабая ссылка – вспомогательный объект, из которого может быть получен интеллектуальный указатель, если конечный объект всё ещё существует. Если конечный объект был удалён (в силу того, что в программе не осталось ссылающихся на него интеллектуальных указателей), объект слабой ссылки в том или ином виде вернёт ошибку.

Простой пример использования

В инструкции по библиотеке в секции для новичков приведён пример использования сигналов. Для изучения работы библиотеки используем подобный пример. В нём создаются два вызываемых объекта. Второй делается отслеживаемым и удаляется сразу после подключения. Это позволит рассмотреть, как работает trackable-поддержка.
#include <boost/signals2.hpp>

using namespace std;
using namespace boost::signals2;

struct HelloWorld 
{
  void operator()(int n) const 
  {
    cout << "HelloWorld invocation with param " << n << endl;
  }
};

struct TrackedHelloWorld 
{
  void operator()(int n) const 
  {
    cout << "TrackedHelloWorld invocation with param " << n << endl;
  }
};


int _tmain(int argc, _TCHAR* argv[])
{
  // Сигнал, принимающий 1 целочисленный аргумент и возвращающий void
  boost::signals2::signal<void (int)> sig;

  // Создать вызываемый объект HelloWorld и подключить его
  HelloWorld hello;
  sig.connect(hello);

  // Создать вызываемый объект TrackedHelloWorld и подключить
  // его посредством сильной ссылки; сделать его отслеживаемым.
  // После выхода из блока объект будет удалён, и соответствующее
  // подключение будет разорвано.
  {
    boost::shared_ptr<TrackedHelloWorld> p(new TrackedHelloWorld());
    boost::signals2::signal<void (int)>::slot_type s(*p);
    s.track(p);
    sig.connect(s);

    // Вызвать оба объекта
    sig(2);
  }

  // Вызвать первый объект (слот второго стал «неактивным»)
  sig(3);

  return 0;
}
Вывод примера на консоль будет таким:
HelloWorld invocation with param 2
TrackedHelloWorld invocation with param 2
HelloWorld invocation with param 3
С точки зрения пользователя пример достаточно прост. В стеке создаётся объект hello типа HelloWorld и подключается к сигналу. Далее в куче создаётся объект TrackedHelloWorld, адрес которого сохраняется в интеллектуальном указателе shared_ptr<>. Интеллектуальные указатели в данном случае выбраны для того, чтобы показать, как инфраструктурой библиотеки signals2 обрабатывается ситуация, когда удаляется объект с подключённым в качестве слота методом. В следующей строке создаётся слот, которому в качестве параметра инициализации предлагается собственно вызываемый объект. Указатель на данный объект подключается к слоту через метод track(...), то есть теперь слот будет отслеживать наличие этого объекта. Обратим внимание на то, что этот метод получает слабую ссылку на объект, которая неявно получается из предоставленного указателя. В противном случае объект бы оказался заблокированным в самом слоте, и его нельзя было бы удалить, пока слот существует. Полученный слот подключается к сигналу. Осуществляется первая активация сигнала – оба слота срабатывают. А в следующей строке указатель p и объект ссылки удаляются. Так что при повторной активации сигнала будет вызван только первый слот (объект HelloWorld). При разыменовании слабой ссылки на второй слот будет обнаружено, что его нет, и соответствующий слот будет пропущен.

Внутренние детали

Чтобы понять, что происходит внутри, нужно рассмотреть несколько ключевых классов. Их описание приводится ниже. Некоторые из них представляют собой законченные самостоятельные метафункции и классы, решающие чётко ограниченные задачи. Другие же являются «кубиками» для сборки, и потому имеют сильные зависимости друг от друга. Их рассмотрение осложняется ещё тем, что зачастую в них используются приёмы препроцессорного программирования с использованием другой интересной boost-библиотеки «Препроцессор». Для них приводятся примеры кода, который получается после обработки исходных текстов препроцессором.
ПРИМЕЧАНИЕ
Под метафункцией подразумевается шаблонный код, основной смысл которого в тех действиях, что осуществляются на этапе компиляции, а не на этапе выполнения.
Чтобы не дублировать в статье библиотеку, приводятся только существенные для понимания детали. Где возможно, предлагаются ссылки на сайт библиотеки, по которым можно посмотреть содержимое того или иного файла. Это может быть удобно, если у вас не установлена локальная копия библиотеки, но вы читаете электронную версию статьи.
Прежде чем переходить к описанию, вниманию читателя предлагается упрощённая диаграмма классов на языке UML. На диаграмме представлено далеко не всё, однако она поможет составить общую картину, прежде чем с головой погрузиться в детали.

Рисунок 1.

Класс signal_base

Служит базовым классом для всех сигналов. Фактически, это чистый интерфейс всего с одним защищённым методом lock_pimpl().
class signal_base : public noncopyable
{
  protected:
      ...
      virtual shared_ptr<void> lock_pimpl() const = 0;
};
Каково назначение этого метода? Сигналы в библиотеке реализуются с помощью идиомы pimpl (http://en.wikipedia.org/wiki/Pimpl). То есть, реализиция всей функциональности сигналов выполнена во внутренних классах signal[N]_impl (см. ниже). А класс сигнала, видимый пользователю библиотеки, является всего лишь обёрткой над ней. Он содержит сильную ссылку на внутренний объект реализации, которому и делегируется вся работа. Можно представить случай, когда все обёртки удаляются во время выполнения процедуры вызова слотов. Это приведёт к удалению объекта реализации, что, скорее всего, вызовет падение программы. Метод lock_pimpl() позволяет получить копию интеллектуального указателя на реализацию и тем самым защитить её от возможного удаления до окончания обработки вызова. (Исходный код можно посмотреть в файле signal_base.hpp).

Класс slot_base

Служит базовым классом для всех слотов. Содержит вектор слабых ссылок на отслеживаемые (trackable) объекты. Для того чтобы осуществить вызов, внешний код должен, во-первых, убедиться, что отслеживаемые объекты не удалены, а во-вторых, запретить их удаление на время вызова. Обе задачи решаются с помощью метода lock(). В нём для каждой слабой ссылки получается сильная ссылка, которая кладётся во временный вектор. Если какой-то отслеживаемый объект был удалён, преобразование в сильную ссылку завершится неудачей, и метод сгенерирует исключение. Если же всё прошло успешно, сконструированный вектор возвращается вызывающему коду. Последний, удерживая его, может защитить отслеживаемые объекты от удаления.
Метод expired() просто проверяет, есть ли среди отслеживаемых хотя бы один удалённый объект. (Исходный код можно посмотреть в файле slot_base.hpp).
class slot_base
{
  public:
      typedef std::vector<boost::weak_ptr<void> > tracked_container_type;
      typedef std::vector<boost::shared_ptr<void> > locked_container_type;

      const tracked_container_type& tracked_objects() const;
      locked_container_type lock() const;
      bool expired() const;

  protected:
      void track_signal(const signal_base &signal);
      tracked_container_type _tracked_objects;
};
Блокировка может осуществляться примерно таким кодом:
class slot: public slot_base {...};
...
slot some_slot_obj;
try 
{
  slot_base::locked_container_type l_obj(some_slot_obj.lock());
  ... // осуществить вызов
} 
catch(expired_slot &e) 
{
  // обработать ошибку «неактивный слот»
}

Классы detail::connection_body_base, connection_body, connection и scoped_connection

Объект «подключение» создаётся, когда к сигналу методом connect(...) подключается какой-либо слот. Описываемые здесь классы служат основой для построения объектов подключения. Класс connection_body_base определяет основной интерфейс подключения и реализует семантику блокирования подключения. Он также определяет интерфейс внутренней блокировки в условиях многопоточности с помощью методов lock и unlock.
class connection_body_base
{
  public:
    void disconnect();
    void nolock_disconnect();
    virtual bool connected() const = 0;
    shared_ptr<void> get_blocker();
    bool blocked() const;
    bool nolock_nograb_blocked() const;
    ...

    // Методы для многопоточной блокировки
    virtual void lock() = 0;
    virtual void unlock() = 0;

  protected:
    mutable bool _connected;
    weak_ptr<void> _weak_blocker;
};
Блокирование подключения реализовано с использованием слабых и сильных ссылок. Вызывающий код может в любое время получить экземпляр ссылки через метод get_blocker(). Пока во внешнем коде сохраняется любое количество этих экземпляров, подключение считается заблокированным. Подключение может также быть активным (_connected == true) или неактивным. С помощью метода nolock_nograb_blocked() можно выяснить, является ли подключение заблокированным или неактивным. В обоих случаях оно не может быть использовано.
Класс connection_body реализует основной интерфейс подключения и интерфейс внутренней блокировки, переопределяя виртуальные методы, приведённые выше. Внутренняя блокировка многопоточности реализуется тривиальным образом, перенаправляя вызовы lock() и unlock() соответствующим методам экземпляра класса Mutex.
template<typename GroupKey, typename SlotType, typename Mutex>
class connection_body: public connection_body_base
{
  public:
        virtual bool connected() const;
        ...
        bool nolock_slot_expired() const;
        template<typename OutputIterator>
          void nolock_grab_tracked_objects(OutputIterator inserter) const;
        ...
        SlotType slot;
  private:
      mutable mutex_type _mutex;
      ...
};
SlotType в реализации библиотеки всегда является потомком класса slot_base, описанного выше. Исходя из этого, становится ясным назначение и реализация метода nolock_grab_tracked_objects(...). Метод выполняет сразу две задачи:
а) проверяет, что ни один из отслеживаемых объектов слота не удалён;
б) получая сильные ссылки на них, копирует их через итератор вывода, защищая тем самым объекты от удаления.
Если первое условие нарушено, состояние самого connection_body устанавливается в (_connected = false). Таким образом, подготовка к вызову может быть выполнена таким кодом:
connection_body some_conn_obj;
std::vector<shared_ptr<void> > v_blockers;
some_conn_obj.nolock_grab_tracked_objects(std::back_inserter(v_blockers));
if(some_conn_obj.nolock_nograb_connected())
{
  ... // осуществить вызов
}
v_blockers.clear();
Метод nolock_slot_expired() – вспомогательный. Он служит просто для проверки наличия отслеживаемых объектов без их блокировки. Если какой-то объект удалён, подключение считается неактивным.
Метод connected() делает практически то же самое. Только он устанавливает при этом блокировку на mutex’е и возвращает инверсный результат. (Исходный код можно посмотреть в файлеconnection.hpp).
Классы connection и scoped_connection являются просто value-обёртками над объектом-потомком connection_body_base (в данной реализации библиотеки это может быть только класс connection_body). Причём эти обёртки хранят слабые ссылки на подключение, так как время жизни подключения определяется не пользователем (который может сохранить для себя connection или scoped_connection), а инфраструктурой библиотеки. Обёртка scoped_connection позволяет разрывать подключение автоматически при удалении самой себя.

Класс detail::grouped_list

Только что рассмотренные объекты подключений нужно где-то хранить. Для этой цели в библиотеке существует шаблонный класс grouped_list, интерфейс которого приведён ниже.
template<typename Group, typename GroupCompare, typename ValueType>
class grouped_list
{
  private:
      typedef std::list<ValueType> list_type;
      typedef std::map<group_key<Group>::type, list_type::iterator, ...> map_type;

  public:
      typedef typename list_type::iterator iterator;
      typedef typename list_type::const_iterator const_iterator;
      typedef typename group_key<Group>::type group_key_type;

      ...
      iterator begin();
      iterator end();
      iterator lower_bound(const group_key_type &key);
      iterator upper_bound(const group_key_type &key);
      void push_front(const group_key_type &key, const ValueType &value);
      void push_back(const group_key_type &key, const ValueType &value);
      void erase(const group_key_type &key);
      iterator erase(const group_key_type &key, const iterator &it);

  private:
      ...
      // в хэше – итераторы списка, указывающие на начало каждой группы
      map_type _group_map;
      list_type _list;
      ...
};
Интерфейс класса сильно напоминает типовой STL-контейнер. Собственно, и реализован он с помощью двух контейнеров – map и list. Его задача – хранить экземпляры типа ValueType в подмножествах по группам. Методы позволяют управлять содержимым списка по каждой группе, идентифицируемой соответствующим ключом. Итераторы этого контейнера обходят весь список от начала до конца. group_key_type определяет тип ключа, по которому находится начало и конец определённой группы методами lower_bound(...) и upper_bound(...), соответственно. В данной статье типы групп подробно не рассматриваются. (Исходный код можно посмотреть в файле slot_groups.hpp).

Класс detail::tracked_objects_visitor

Данный шаблонный класс представляет собой пример нетривиального метапрограммирования. Фактически это метафункция, используемая при инициализации нового объекта слота (чуть ниже) для того, чтобы выяснить, является ли переданная пользователем вызываемая сущность отслеживаемым объектом. В библиотеке signals2 сделать объект отслеживаемым можно двумя способами: сделать его наследником базового класса типа trackable или передать слабую ссылку на объект в метод track(...) слота. Описываемый вспомогательный класс реализует первый способ. В конструкторе ему передаётся слот, а в шаблонном методе operator(...) – вызываемая сущность. Далее следует примерно такая последовательность распознавания типа:
  1. Если передан объект типа – boost::ref<T> – снять обёртку ref и получить обычный указатель на объект типа T, ссылка на который хранилась в обёртке.
  2. Если передан не указатель, а какой-то объект T, получить обычный указатель на него.
  3. Если полученный адрес указывает на функцию, дальше объект не рассматривать.
  4. Если адрес указывает на сигнал – подключить его к слоту как отслеживаемый объект.
  5. Если адрес указывает на объект, неявно преобразуемый к trackable – подключить его к слоту.
Во всех остальных случаях ничего не делается, срабатывают пустые заглушки. (Исходный код можно посмотреть в файле tracked_objects_visitor.hpp)

Классы slot, slotN и slot[N]

Наконец теперь можно рассмотреть, что представляет собой законченный объект слота. Первое, что необходимо отметить, разработчики библиотеки столкнулись с типовой проблемой – в языке C++ нет безопасной передачи переменного числа аргументов с проверкой типа. И решение в данном случае такое же, как и везде – определить классы или методы, поддерживающие от 1 до N аргументов. Для этого в библиотеке широко используется препроцессорное программирование.
Для начала повторным включением файла slot_template.hpp в preprocessed_slot.hpp определяются версии класса slot[N] (где N равно от 1 до максимального числа аргументов, установленного директивой). В статье используются обозначения типа slot[N], чтобы показать множество классов slot1, slot2, ... , которые будут существовать в скомпилированной программе.
template<typename R, typename T1, typename T2, ..., typename T[N],
    typename SlotFunction = boost::functionN<R, T1, T2, ..., T[N]> >
class slot[N] : public slot_base, detail::std_functional_base
{
  public:
      typedef SlotFunction slot_function_type;
      typedef R result_type;

      ...
      template<typename F> slot[N](const F& f) { init_slot_function(f); }
      R operator()(T1 arg1, T2 arg2, ...TN argN);
      slot[N]& track(const weak_ptr<void> &tracked);
      slot[N]& track(const signal_base &signal);
      slot[N]& track(const slot_base &slot);

  private:
      ...
      SlotFunction _slot_function;
};
Как правило, слоты неявно создаются в методе connect(...) сигнала, когда туда передаются вызываемые сущности. Часто эти сущности имеют некоторые параметры, поэтому программист должен связать (bind) сущность с какими-то значениями этих параметров, прежде чем делать вызов метода connect(...). Для облегчения задачи программисту предлагается множество перегрузок конструктора класса slot[N], принимающих различное число параметров дополнительно к самой вызываемой сущности. Эти конструкторы сами связывают сущность и переданные параметры. С помощью препроцессорного программирования в библиотеке создаётся M (задаётся препроцессорной директивой) конструкторов для этих целей:
template<Func, BindArgT1>
slot[N](const Func &f, const BindArgT1 &arg1)
{
    init_slot_function(boost::bind(f, arg1));
}

...
template<Func, BindArgT1, BindArgT2, ...BindArgTM>
slot[N](const Func &f, const BindArgT1 &arg1, const BindArgT1 &arg2, ... const BindArgTM &argM)
{
    init_slot_function(boost::bind(f, arg1, arg2, ... argM));
}
Внутренняя шаблонная функция init_slot_function(...), во-первых, присваивает внутреннему функтору слота вызываемую сущность, полученную из аргумента посредством применения метафункции get_invocable_slot(...). А во-вторых, используя класс tracked_objects_visitor, рассмотренный выше, пытается сделать переданный объект отслеживаемым. Метафункция get_invocable_slot(f) определяет, был ли переданный объект f сигналом или чем-то иным. В первом случае она возвращает экземпляр вспомогательной прослойки weak_signal[N], позволяя при активации перенаправлять вызов объекту сигнала, если последний не был удалён. Во втором случае она просто возвращает переданный ей объект f.
Оператор вызова в классе slot[N] тривиален – он просто передает выполнение объекту _slot_function вместе с параметрами. Исходя из только что сказанного, самое простое использование класса slot[N] могло бы быть таким:
void f(int n) { cout << n; };
...
signal<void (void)>::slot_type s(&f, 1);  // используется связывающий конструктор класса slot1
s();                                      // вызов функции f; будет напечатано «1»
Специализации класса slotN (не slot[N]) служат только для получения конкретного типа slot[N] для заданного числа аргументов. Например, выражение slotN<2, ...>::type приводит к slot2<...>.
Класс slot – просто обёртка (производный класс) над slotN::type, введённая, чтобы из сигнатуры функции получить число параметров и передать его первым шаблонным аргументом в slotN. В обёртке нет ничего, кроме вынужденного переопределения всех конструкторов, конструируемого так же, как и в классе slot[N].

Шаблонные классы Combiner

Библиотека signals2 предусматривает возможность подключения множества слотов к одному сигналу. Возникает вопрос – как объединять результаты вызова этих слотов при возврате из оператора вызова сигнала? Проектное решение состоит в том, чтобы вынести этот вопрос в отдельный класс, предоставить пользователю реализацию по умолчанию или возможность задействовать свой собственный класс. Интерфейс такого класса прост – он должен предоставлять всего один оператор вызова, принимающий диапазон, заданный двумя итераторами в духе STL. В операторе вызова перебираются значения, попадающие в этот диапазон, а значения, получаемые разыменовыванием итераторов, комбинируются так, как это нужно пользователю. Вызывающий код предоставляет итераторы специального вида, которые во время разыменования передают управление нижележащим слотам, и возвращают значения, возвращённые этими слотами. В качестве примера можно обратиться к тривиальному Combiner’у, представленному в файле optional_last_value.hpp.

Шаблон класса итератора вызова detail::slot_call_iterator_t

Данный класс фактически является адаптером итераторов списка подключений. Его работа становится более-менее очевидной, если уточнить, что тип Iterator, которым он специализируется, является типом detail::grouped_list::iterator, то есть:
std::list<shared_ptr<connection_body<...> > >::iterator
Во время инициализации классу передаётся ссылка на кэш slot_call_iterator_cache, являющийся, по сути, просто контейнером, где хранится функтор Function и массив для блокировки отслеживаемых объектов. Выше уже рассматривался класс connection_body и его метод nolock_grab_tracked_objects(...), позволяющий получить временные блокировки отслеживаемых объектов. В кэше как раз размещается буфер, куда кладутся эти временные блокировки на время, пока делается вызов слота.
template<typename Function, typename Iterator, typename ConnectionBody>
class slot_call_iterator_t
  : public boost::iterator_facade<slot_call_iterator_t<Function, Iterator, ConnectionBody>...
{
      ...
  public:
      slot_call_iterator_t(Iterator iter_in, Iterator end_in,
          slot_call_iterator_cache<result_type, Function> &c);

      typename inherited::reference dereference() const {
        ...
        cache->result.reset(cache->f(*iter));
        ...
      }
      void increment();
      bool equal(const slot_call_iterator_t& other) const;

   private:
      ...
      slot_call_iterator_cache<result_type, Function> *cache;
};
При инкременте происходит передвижение позиции внутреннего итератора, а затем осуществляется попытка получить все временные блокировки. Если это не удаётся, попытка повторяется на следующей позиции внутреннего итератора. В итоге либо будет найден итератор подключения, слот которого удалось заблокировать, и он готов к вызову, либо будут исчерпаны все итераторы списка подключений. (Исходный код можно посмотреть в файле slot_call_iterator.hpp).

Классы signal, signalN, signal[N] и signal[N]_impl

Реализация сигналов выполнена с использованием хорошо известной идиомы pimpl, заключающейся в том, что класс, видимый пользователю, представляет всего лишь обёртку, имеющую указатель на объект реализации (pointer-to-implementation). Все операции, которые пользователь выполняет с классом, транслируются в соответствующие вызовы к объекту реализации. Такая архитектура помогает подключать к сигналу другой сигнал, организуя тем самым цепочки вызовов. Происходит это следующим образом. Допустим, программист написал: sig1.connect(sig2). Как уже известно, перед вызовом метода connect(...) неявно создаётся объект слота, инициализируемый сигналом sig2. Этот сигнал попадает в функцию get_invocable_slot(...) (см. описание классов slot[N], slot выше), которая возвращает объект weak_signal[N], полученный из sig2. Он является обёрткой для реализации signal[N]_impl из исходного объекта sig2. Эта обёртка и сохраняется в объекте слота, который подключается к сигналу sig1.

Итак, множество классов signal[N]_impl (где N равно от 1 до максимального числа аргументов, установленного директивой) представляет собой реализацию сигнала, примерный вид которой представлен ниже:
template<typename R, typename T1, typename T2, ...typename TN,
  typename Combiner, typename Group, typename GroupCompare,
  typename SlotFunction, typename ExtendedSlotFunction, typename Mutex>
class signal[N]_impl
{
  public:
    typedef SlotFunction slot_function_type;
    typedef slot[N]<R, T1, T2, ...TN, slot_function_type> slot_type;
    typedef ExtendedSlotFunction extended_slot_function_type;
    typedef slot[N+1]<R, const connection&, T1, T2, ...TN, extended_slot_function_type> extended_slot_type;
    typedef typename nonvoid<typename slot_function_type::result_type>::type nonvoid_slot_result_type;
    typedef slot_call_iterator_cache<nonvoid_slot_result_type, slot_invoker> slot_call_iterator_cache_type;
    typedef typename group_key<Group>::type group_key_type;
    typedef shared_ptr<connection_body<group_key_type, slot_type, Mutex> > connection_body_type;
    typedef grouped_list<Group, GroupCompare, connection_body_type> connection_list_type;
      ...
  public:
    typedef Combiner combiner_type;
    typedef typename result_type_wrapper<typename combiner_type::result_type>::type result_type;
    typedef typename detail::slot_call_iterator_t<slot_invoker,
      typename connection_list_type::iterator,
      connection_body<group_key_type, slot_type, Mutex> > slot_call_iterator;

    ...
    connection connect(const slot_type &slot, connect_position position = at_back);
    connection connect(const group_type &group, const slot_type &slot,
      connect_position position = at_back);
    template <typename T> void disconnect(const T &slot);

    result_type operator ()(T1 arg1, T2 arg2, ...TN argN);

    ...
    mutable shared_ptr<invocation_state> _shared_state;
    mutable typename connection_list_type::iterator _garbage_collector_it;
    ...
};
Некоторые компиляторы не поддерживают возврат значения void. Поэтому во многих местах в библиотеке используется распознавание типа возврата R и реализуются две версии кода. В других же местах используется замена типа – если тип возврата void, используется фиктивный тип void_type. Метафункция nonvoid предназначена для такой замены. В коде встречается ещё одна функция result_type_wrapper, делающая то же самое. Разница только в том, что функциональность последней управляется препроцессорной директивой.
Для хранения подключений используется объект класса detail::grouped_list, уже рассмотренный выше. Причём экземпляры списка и Combiner’а вынесены в виде сильных ссылок во вспомогательный класс invocation_state, ссылка на который хранится в рассматриваемом signal[N]_impl.
Методы connect(...) просто добавляют слот в список подключений с различными параметрами. Метод disconnect(...) меняет статус подключения на статус «не активно», но не удаляет его. Это делается вспомогательным методом nolock_cleanup_connections(...), вызываемым из разных мест класса.
В операторе вызова используется вспомогательный класс slot_invoker, который является тем самым функтором, через который осуществляется вызов итератором detail::slot_call_iterator_t. После расшифровки всех препроцессорных директив получается тривиальная реализация, которая в конструкторе принимает ссылки на параметры, а в операторе вызова осуществляет вызов функтора слота через переданный объект подключения.
class slot_invoker {
  public:
      T1 &arg1;
      T2 &arg2;
      ...
      slot_invoker(T1 &arg1, T2 &arg2, ...) : arg1(arg1), arg2(arg2), ... {}
      nonvoid_slot_result_type operator ()(const connection_body_type &connectionBody) const;
      ...
      nonvoid_slot_result_type m_invoke(const connection_body_type &connectionBody, ...) const
      {
        return connectionBody->slot.slot_function()(arg1, arg2, ...);
      }
};
Оператор вызова выполняет следующие операции. Сперва методом nolock_cleanup_connections() производится очистка списка от устаревших подключений, то есть тех, отслеживаемые объекты которых были удалены. Затем создаётся вспомогательный объект slot_invoker и кэш slot_call_iterator_cache. Создаётся ещё один вспомогательный объект invocation_janitor, единственная цель которого – после завершения оператора вызова очистить список от устаревших подключений, которые стали таковыми за время осуществления вызова сигнала. Кроме того, возможно, он способствует оптимизации возвращаемого Combiner’ом значения, так как позволяет записать одним выражением и вызов Combiner’а, и возврат из функции.
Собственно последним выражением следует вызов Combiner’а с передачей ему двух итераторов slot_call_iterator_t. Combiner осуществит вызов слотов, разыменовывая эти итераторы, как было описано выше. В этом выражении используется ещё одна прослойка detail::combiner_invoker, опять же с целью различить возврат типа void и заменить его возвратом фиктивного типа void_type.
Специализация класса signal[N] является обёрткой над реализацией signal[N]_impl и служит только для того, чтобы предоставить публичниый интерфейс сигналов, делегирующий все вызовы через указатель _pimpl объекту реализации.
С помощью специализаций шаблонного класса signalN можно получить соответствующий тип signal[N], задавая число аргументов. Пример: signalN<2>::type.
Наконец, класс signal – просто обёртка (производный класс) над signalN::type введённая для того, чтобы из сигнатуры функции получить число параметров и передать его первым шаблонным аргументом в signalN. Идея та же, что и с классами slot, slotN и slot[N]. (Исходный код можно посмотреть в файле signal_template.hpp).

Соберём всё вместе

Если уважаемый читатель добрался до этого места, то теперь он имеет все сведения, чтобы понять, что же происходит в примере, приведённом в начале статьи. Рассмотрим работу примера по шагам.
signal<void (int)> sig;
...
sig.connect(hello);
Подключение. Метод connect(…) принимает слоты, а объект hello таковым не является. Следовательно, ещё до вызова метода в программе будет создан временный объект типа slot_type с передачей в его конструктор объекта hello. slot_type в этом примере раскрывается в slot1<void, int>. В конструкторе этого класса вызывается функция init_slot_function, которая присваивает объект внутренней переменной _slot_function. Так как HelloWorld не является отслеживаемым, tracked_objects_visitor ничего не делает. Далее объект слота передаётся сигналу (а это всего лишь обёртка), а тот передаёт вызов реализации signal1_impl<void, int, ...>. Последняя добавляет его в список detail::grouped_list, вызывая его метод push_back(...).
{
    boost::shared_ptr<TrackedHelloWorld> p(new TrackedHelloWorld());
    signal<void (int)>::slot_type s(*p);
    s.track(p);
В куче создаётся второй объект и передаётся новому объекту slot1<void, int>. И теперь также происходит всего лишь присвоение объекта переменной _slot_function, а tracked_objects_visitor ничего не делает. Однако в третьей строке «вручную» осуществляется подключение объекта к системе слежения. В метод track передаётся слабая ссылка weak_ptr<TrackedHelloWorld>, полученная неявным образом, а тот вставляет её в массив _tracked_objects, определённый в базовом классе slot_base.
    sig.connect(s);

    // вызов обоих слотов
    sig(2);
}
Слот подключается к сигналу так же, как и раньше. Затем осуществляется вызов обоих подключённых слотов (первый вызов не рассматривается, так как второй гораздо интереснее). По завершению блока объект, на который указывает p, удаляется, и слабая ссылка, сохранённая в недрах слота, оказывается недействительной. Но об этом пока никто не знает.
sig(3);
Здесь происходит всё самое интересное.
  1. Вызывается operator()(3) у сигнала signal1. Тот передаёт вызов реализации signal1_impl::operator()(3). Для лучшего понимания она представлена ниже.
result_type operator (…) const
{
  shared_ptr<invocation_state> local_state;
  typename connection_list_type::iterator it;
  {
    unique_lock<mutex_type> list_lock(_mutex);
    if(_shared_state.unique())
      nolock_cleanup_connections(false);
    local_state = _shared_state;
  }
  slot_invoker invoker = slot_invoker(…);
  slot_call_iterator_cache_type cache(invoker);
  invocation_janitor janitor(…);
  return detail::combiner_invoker<typename combiner_type::result_type>()
  (
    local_state->combiner(),
    slot_call_iterator(local_state->connection_bodies().begin(),
      local_state->connection_bodies().end(), cache),
    slot_call_iterator(local_state->connection_bodies().end(),
      local_state->connection_bodies().end(), cache)
  );
}
  1. signal1_impl::operator()(...) предварительно производит очистку неактивных подключений методом nolock_cleanup_connections(...). Очистка осуществляется только до момента нахождения первого нормального подключения. В данном случае это и есть первое подключение, поэтому ничего очищено не будет.
  2. Создаются вспомогательные объекты slot_invoker, slot_call_iterator_cache_type и invocation_janitor. В конструкторе slot_invoker’а запоминается ссылка на параметр вызова: int(3).
  3. В последнем выражении функции signal1_impl::operator()(...) создаётся пара итераторов detail::slot_call_iterator_t. Конструктор этих объектов выполняет предварительный поиск итератора подключения, который можно использовать для вызова. Так как с первым добавленным подключением всё в порядке, на нём поиск и прекращается.
  4. Создаётся вспомогательная прослойка combiner_invoker, которая просто вызывает Combiner с новыми итераторами.
  5. Combiner пытается получить значение, разыменовывая итератор, таким образом, вызывается метод slot_call_iterator_t::dereference(...). В специализации Combiner’а для типа возврата void последний не запоминает никакого значения, а просто разыменовывает итератор.
  6. Итератор вызывает объект класса slot_invoker, передавая ему разыменованный внутренний итератор, то есть, в данном случае ссылку на connection_body.
  7. slot_invoker обращается к методу slot_function(...) и получает копию вызываемого объекта, то есть boost::function<void (int)>.
  8. Функтор boost::function<> уже вызывает сохранённый в нём объект HelloWorld с параметром 3.
  9. Управление возвращается в Combiner. Он передвигает (инкрементирует) итератор slot_call_iterator_t.
  10. Операция инкремента, передвигая внутренний итератор, пытается заблокировать объект connection_body, на который тот указывает. Однако после вызова connection_body::nolock_grab_tracked_objects() оказывается, что подключение разорвано, так как при выполнении этого метода возникла попытка получить сильную ссылку на удалённый объект. Операция инкремента переходит на следующую позицию, но во внутреннем диапазоне заканчиваются объекты.
  11. Combiner обнаруживает, что после инкремента итератор стал равен концевому итератору, и возвращает управление.
  12. Происходит возврат управления из прослойки combiner_invoker.
  13. В результате очистки деструктор объекта invocation_janitor подчищает неактивные подключения. Однако в текущей реализации библиотеки делается сравнение количества активных и неактивных подключений. Только если неактивных больше – делается очистка. В примере получается, что и тех и других – по одному, а значит, очистка производиться не будет.
  14. Происходит возврат из signal1_impl, затем из signal1. Всё.
Подобное количество вызовов наводит на вопрос об эффективности работы библиотеки signals2. В качестве небольшого утешения хочу уточнить, что в скомпилированной с оптимизацией программе стек вызовов не должен быть таким большим. Некоторые прослойки не содержат внутренних переменных, и практически нигде не используются виртуальные методы. А значит, некоторые уровни компилятор вообще устранит из исполняемого кода.

Темы, оставшиеся «за бортом»

В статье рассмотрены не все аспекты проектирования библиотеки signals2. Есть несколько моментов, рассмотрение которых можно рекомендовать очень пытливому читателю или просто пропустить. Во-первых, это использование шаблонов с переменным числом параметров. В коде постоянно встречаются препроцессорные директивы, проверяющие макрос BOOST_NO_VARIADIC_TEMPLATES. Если он не определён, во многих местах подключаются другие файлы с исходными текстами и генерируются другие классы, нежели те, что были рассмотрены выше. Их содержимое не анализировалось.
Во-вторых, в статье не рассматриваются вопросы, связанные с реализацией групп подключений. Это не оказывает существенного влияния на приведённый выше анализ.
В-третьих, в анализе упускаются места, связанные с многопоточностью. Многопоточность влияет только на правильную расстановку в нужных местах объектов блокировки. Данная задача решается в библиотеке так же, как и в любом другом коде.

Зачем?

По ходу анализа у меня возникали вопросы или просто недоумение выбранным проектным решением. Может быть, я просто что-то недопонял. Хочу привести список вопросов. Возможно, ответ на них будет очевиден для читателя.
  1. Зачем делать более одного отслеживаемого объекта на каждый слот? В 99% случаев имеет смысл только объект, в котором непосредственно находится вызываемый метод. Ведь в случае множества подключений итак создаётся множество объектов слотов, каждый из которых может отслеживать «свой» объект.
  2. Зачем в классе connection_body_base блокирование реализовывать посредством слабых и сильных ссылок? Можно же было просто сделать булев флажок.
  3. Множество слотов, подключённых к одному сигналу, можно разбить на подмножества – группы, и управлять ими отдельно. Насколько нужна пользователю поддержка групп подключений? Эта семантика видится надуманной.
  4. Метод connection_body::connected зачем-то для проверки состояния отслеживаемых объектов использует тяжеловесный nolock_grap_tracked_objects, получая сильные ссылки, которые всё равно нигде не используются. Почему было просто не воспользоваться методом nolock_slot_expired()?
  5. Зачем нужно было переусложнять классы arg и набор preprocessed_arg_type[N] с помощью препроцессорного программирования, когда та же задача получения типа из списка типов в библиотеке Loki решается с помощью рекурсивного инстанциирования шаблона. См. в качестве примера метафункцию TypeAt в файле typelist.h библиотеки Loki.
  6. Где-то в коде используется перегрузка для различения возврата void, а где-то – используется фиктивный тип void_type, полученный с помощью метафункции nonvoid. Почему не привести весь код к виду, когда везде используется void_type, а замену сделать на самом нижнем уровне, в slot[N]::operator()(...)? Здесь же: зачем набор из nonvoid и result_type_wrapper?
На сайте www.rsdn.ru в разделе «Философия программирования» есть интересная статья «Закон сохранения сложности». Статья спорная, что видно по размеру переписки в форуме с комментариями. Однако при изучении многих библиотек boost'a вспоминается и её содержимое и эпиграф: «Усложнять - просто, упрощать – сложно». И не покидает ощущение, что сложность реализации этих библиотек вызвана не столько объективной необходимостью, сколько: а) желанием написать по-академически сложно; б) включить зачатки как можно большего числа концепций, которые, скорее всего, никогда не понадобятся; в) просто неумением сделать проще или непониманием концепции до конца; г) может быть, намеренной обфускацией кода, чтобы создать ореол элитарности. Мне не хотелось бы быть категоричным, поэтому хочу подчеркнуть, что это лишь моё ощущение. Однако оно возникает уже не первый раз. Чувство дежавю появилось с того дня, когда я провёл сравнение реализации функторов в boost’е и в библиотеке Loki. Однако сравнительный анализ функторов – это тема уже отдельной статьи.

Список литературы

  1. Скотт Мэйерс. Эффективное использование C++. «ДМК», Москва, 2006
  2. Скотт Мэйерс. Наиболее эффективное использование C++. «ДМК», Москва, 2000.
  3. Андрей Александреску. Современное проектирование на C++. «Вильямс», 2002.
  4. Мэтью Уилсон. Расширение библиотеки STL для C++. Наборы и итераторы. «ДМК», Москва, 2008.
  5. Игорь Ткачёв. Закон сохранения сложности. Статья на сайте www.rsdn.ru, раздел «Философия программирования». 2002-2009.

четверг, 28 июля 2011 г.

Статический анализ Си++ кода и новый стандарт языка C++0x

Аннотация
Введение
1. auto
2. decltype
3. Ссылка на временный объект (R-value reference)
4. Правые угловые скобки
5. Лямбда-функции (Lambdas)
6. Suffix return type syntax
7. static_assert
8. nullptr
9. Новые стандартные классы
10. Новые направления в развитии статических анализаторов кода
Заключение
Библиографический список

Аннотация


В статье рассмотрены новые возможности языка Си++, описанные в стандарте C++0x и поддержанные в Visual Studio 2010. На примере PVS-Studio рассмотрено, как изменения языка отразятся на инструментах статического анализа кода.

Введение


Новый стандарт языка Си++ вот-вот придет в нашу жизнь. Пока его продолжают именовать C++0x, хотя, по всей видимости, его окончательное название — C++11. Новый стандарт уже частично поддерживается современными Си++ компиляторами, например Intel C++ и Visual C++. Поддержка далеко не полна, что вполне естественно. Во-первых стандарт еще не принят, а во-вторых даже когда он будет принят, потребуется время на проработку в компиляторах его особенностей.

Разработчики компиляторов не единственные, для кого важна поддержка нового стандарта. Нововведения языка оперативно должны быть поддержаны в инструментах статического анализа исходного кода. Новый стандарт обещает обратную совместимость. Почти гарантировано старый Си++ код будет корректно скомпилирован новым компилятором без необходимости каких-либо правок. Однако это не означает, что программа, не содержащая новые конструкции языка, сможет быть по-прежнему обработана статическим анализатором, не поддерживающим новый стандарт C++0x. Мы убедились в этом на практике, попытавшись проверить с помощью PVS-Studio проект, созданный еще в бета-версии Visual Studio 2010. Все дело в заголовочных файлах, в которых уже используются новые конструкции языка. Например, в заголовочном файле «stddef.h» можно увидеть использование нового оператора decltype:
namespace std { typedef decltype(__nullptr) nullptr_t; }
Естественно, что такие конструкции являются синтаксически неверными для анализатора, не поддерживающего C++0x, и приводят, либо к остановке его работы или неверным результатом. Стала очевидной необходимость поддержать C++0x в PVS-Studio к моменту выхода Visual Studio 2010, по крайней мере в том объеме, в котором новый стандарт поддерживается этим компилятором.

Можно заявить, что данная задача нами была успешно решена и на момент написания статьи, на сайте доступна версия PVS-Studio 3.50, интегрирующаяся как в Visual Studio 2005/2008, так и в Visual Studio 2010. Начиная с версии PVS-Studio 3.50 в инструменте реализована поддержка той части С++0x, которая реализована в Visual Studio 2010. Поддержка не идеальна, как например, при работе с «right-angle brackets», но мы продолжим работу по поддержке стандарта C++0x в следующих версиях.

В этой статье мы рассмотрим новые возможности языка, поддержка которых реализована в первой редакции Visual Studio 2010. При этом взглянем на эти возможности с различных позиций: что представляет из себя новая возможность, имеется ли связь с 64-битными ошибками, как новая конструкция языка была поддержана в PVS-Studio и как ее появление отразилось на библиотеке VivaCore.

Примечание. VivaCore — библиотека разбора, анализа и трансформации кода. VivaCore является открытой библиотекой и поддерживает языки Си и Си++. На основе VivaCore построен продукт PVS-Studio и на ее же основе могут быть созданы другие программные проекты.

Предлагаемую вашему вниманию статью можно назвать отчетом по исследованию и поддержке нового стандарта в PVS-Studio. Инструмент PVS-Studio диагностирует 64-битные и параллельные OpenMP ошибки. Но поскольку в данный момент более актуальной темой является переход на 64-битные системы, предпочтение будет отдано примерам, демонстрирующих обнаружение с помощью PVS-Studio 64-битных ошибок.

1. auto


В Си++, как и в Си, тип переменной должен быть указан явно. Однако, с появлением в языке Си++ шаблонных типов и техник шаблонного метапрограммирования, частой стала ситуация, когда тип объекта записать не так просто. Даже в достаточно простом случае, при переборе элементов массива, нам понадобится объявление типа итератора вида:
for (vector<int>::iterator itr = myvec.begin(); 
     itr != myvec.end(); 
     ++itr)
Подобные конструкции весьма длинны и неудобны. Для сокращения записи можно использовать typedef, но это порождает новые сущности и мало добавляет с точки зрения удобства.

C++0x предлагает способ для смягчения этой проблемы. В новом стандарте значение ключевого слова auto будет заменено. Если раньше auto означало, что переменная создается в стеке, и подразумевалось неявно в случае, если вы не указали что-либо другое (register, к примеру), то теперь это аналог var в C# 3.0. Тип переменной, объявленной как auto, определяется компилятором самостоятельно на основе того, чем эта переменная инициализируется.

Следует заметить, что auto-переменная не сможет хранить значения разных типов в течение одного запуска программы. Си++ по прежнему остается статически типизированным языком, и указание auto лишь говорит компилятору самостоятельно позаботиться об определении типа: после инициализации сменить тип переменной будет уже нельзя.

Теперь итератор может быть объявлен следующим образом:
for (auto itr = myvec.begin(); itr != myvec.end(); ++itr)
Помимо удобства в написании кода и его упрощения, ключевое слово auto поможет сделать код более безопасным. Рассмотрим пример, где auto сделает код безопасным с точки зрения создания 64-битных приложений:
bool Find_Incorrect(const string *arrStr, size_t n)
{
  for (size_t i = 0; i != n; ++i)
  {
    unsigned n = arrStr[i].find("ABC");
    if (n != string::npos)
      return true;
  }
  return false;
};
Данный код содержит 64-битную ошибку. Функция корректно ведет себя при компиляции Win32 версии и дает сбой при сборке в режиме Win64. Ошибка заключается в использовании типа unsigned для переменной «n», хотя должен использоваться тип string::size_type, который возвращает функция find(). В 32-битной программе тип string::size_type и unsigned совпадают, и мы получаем корректные результаты. В 64-битной программе string::size_type и unsigned перестают совпадать. Когда подстрока не находится, функция find() возвращает значение string::npos, равное 0xFFFFFFFFFFFFFFFFui64. Это значение урезается до величины 0xFFFFFFFFu и помещается в 32-битную переменную. В результате условие 0xFFFFFFFFu == 0xFFFFFFFFFFFFFFFFui64 ложно и получается, что функция Find_Incorrect всегда возвращает true.

В данном примере ошибка не так страшна, так обнаруживается даже компилятором и тем более специализированным анализатором Viva64 (входящим в состав PVS-Studio).

Компилятор:
warning C4267: 'initializing' : 
conversion from 'size_t' to 'unsigned int', possible loss of data
Viva64:
V103: Implicit type conversion from memsize to 32-bit type.
Важнее то, что данная ошибка возможна и часто встречается в коде из-за неаккуратности при выборе типа для хранения возвращаемого значения. Возможно даже, что ошибка возникла из-за нежелания использовать громоздкую конструкцию вида string::size_type.

Теперь подобных ошибок легко избежать, при этом не загромождая код. Используя тип «auto» мы можем написать следующий простой и надежный код:
auto n = arrStr[i].find("ABC");
if (n != string::npos)
  return true;
Ошибка исчезла сама собой. Код не стал сложнее или менее эффективным. Вывод — использование «auto» рационально во многих случаях.

Ключевое слово «auto» сократит количество 64-битных ошибок или позволит исправить ошибки более изящно. Но само по себе использование «auto» вовсе не избавляет от всех 64-битных ошибок! Это всего лишь еще один инструмент языка, облегчающий жизнь программиста, но не делающий за него всю работу по контролю над типами. Рассмотрим пример:
void *AllocArray3D(int x, int y, int z,
                   size_t objectSize)
{
  int size = x * y * z * objectSize;
  return malloc(size);
}
Функция должна вычислить размер массива и выделить необходимое количество памяти. Логично ожидать, что в 64-битной среде эта функция сможет выделить память для работы с массивом размером 2000*2000*2000 типа «double». Однако вызов вида «AllocArray3D(2000, 2000, 2000, sizeof(double));» всегда будет возвращать NULL, как будто выделение такого объема памяти невозможно. Настоящей же причиной, по которой функция возвращает NULL, является ошибка переполнения в выражении «int size = x * y * z * sizeof(double)». Переменная «size» примет значение -424509440 и дальнейший вызов функции malloc не имеет смысла. Кстати, об опасности данного выражения предупредит и компилятор:
warning C4267: 'initializing' : 
conversion from 'size_t' to 'int', possible loss of data
Надеясь на «auto», неаккуратный программист может модифицировать код следующим образом:
void *AllocArray3D(int x, int y, int z,
                   size_t objectSize)

{
  auto size = x * y * z * objectSize;
  return (double *)malloc(size);
}
Однако это вовсе не устранит, а только замаскирует ошибку. Компилятор больше не выдаст предупреждение, но функция AllocArray3D по-прежнему будет возвращать NULL.

Тип переменной «size» автоматически станет "size_t". Но переполнение возникает при вычислении выражения «x * y * z». Это подвыражение имеет тип «int» и только затем тип будет расширен до «size_t» при умножении на переменную «objectSize».

Теперь эту спрятавшуюся ошибку можно будет обнаружить, только используя анализатор Viva64:
V104: Implicit type conversion to memsize type in an 
arithmetic expression.
Вывод — используя «auto», все-равно следует быть внимательным.

Теперь кратко рассмотрим, как новое ключевое слово было поддержано в библиотеке VivaCore, на которой и построен статический анализатор Viva64. Итак, анализатор должен уметь понять, что переменная AA имеет тип «int», чтобы, предупредить (см. V101) о расширении переменной АА до типа «size_t»:
void Foo(int X, int Y)
{
  auto AA = X * Y;
  size_t BB = AA; //V101
}
Прежде всего, была составлена новая таблица лексем, которая включила новые ключевые слова C++0x. Эта таблица находится в файле Lex.cc и имеет имя tableC0xx. Для того чтобы не модифицировать старый код по обработке лексемы «auto» (tkAUTO), лексема «auto» в этой таблице имеет имя tkAUTOcpp0x.

В связи с появлением новой лексемы модификации подверглись следующие функции: isTypeToken, optIntegralTypeOrClassSpec. Появился новый класс LeafAUTOc0xx. В TypeInfoId появился новый класс объектов — AutoDecltypeType.

Для кодирования типа «auto» выбрана литера 'x', что нашло отражение в функциях классов TypeInfo и Encoding. Это, например, такие функции как IsAutoCpp0x, MakePtree.

Эти исправления позволяют разбирать код с ключевым «auto», имеющим новый смысл и сохранять тип объектов в закодированном виде (литера 'x'). Однако это не позволяет узнать, какой тип в действительности представляет переменная. То есть в VivaCore отсутствует функциональность, позволяющая узнать, что в выражении «auto AA = X * Y» переменная AA будет иметь тип «int».

Данная функциональность содержится в исходном коде Viva64 и не включается в состав кода библиотеки VivaCore. Принцип заключается в дополнительной работе по вычислению типа в методе TranslateAssignInitializer. После того, как вычислена правая часть выражения происходит подмена связи (Bind) имени переменной с типом.

2. decltype


В ряде случаев полезно «скопировать» тип некоторого объекта. Ключевое слово «auto» выводит тип, основываясь на выражении, используемом для инициализации переменной. Если инициализация отсутствует, то для определения типа выражения во время компиляции может быть использовано ключевое слово «decltype». Пример кода, где переменная «value» будет иметь тип, возвращаемый функцией «Calc()»:
decltype(Calc()) value;
try {
  value = Calc(); 
}
catch(...) {
  throw;
}
Можно использовать «decltype» для объявления типа:
void f(const vector<int>& a,
       vector<float>& b)
{
  typedef decltype(a[0]*b[0]) Tmp;
  for (int i=0; i<b.size(); ++i)
  {
    Tmp* p = new Tmp(a[i]*b[i]);
    // ...
  }
}
Учтите, что тип, взятый с использованием decltype, может отличаться от типа, выведенного с помощью auto.
const std::vector<int> v(1);
auto a = v[0];decltype(v[0]) b = 1;
// тип a - int  
// тип b - const int& (возвращаемое значение
// std::vector<int>::operator[](size_type) const)
Перейдем к примеру, где «decltype» может быть полезен с точки зрения 64-битности. Функция IsPresent ищет элемент в последовательности и возвращает «true» если он найден:
bool IsPresent(char *array,
               size_t arraySize,
               char key)
{
  for (unsigned i = 0; i < arraySize; i++) 
    if (array[i] == key)
      return true;
  return false;
}
Данная функция неспособна работать в 64-битной системе с большими массивами. Если переменная arraySize будет иметь значение больше UINT_MAX, то условие «i < arraySize» никогда не выполнится и возникнет вечный цикл.

Если мы воспользуемся ключевым словом «auto», то это ничего не изменит:
for (auto i = 0; i < arraySize; i++) 
  if (array[i] == key)
    return true;
Переменная «i» будет иметь тип «int», так как 0 имеет тип «int». Правильным исправлением может стать использование «decltype»:
for (decltype(arraySize) i = 0; i < arraySize; i++) 
  if (array[i] == key)
    return true;
Теперь счетчик «i» имеет тип «size_t» как и переменная «arraySize».

Поддержка «decltype» в библиотеке VivaCore во многом схожа с поддержкой «auto». Добавлена новая лексема tkDECLTYPE. Добавлена функция парсинга rDecltype в файле Parser.cc. В связи с появлением новой лексемы модификации подверглась функция optIntegralTypeOrClassSpec. Появился новый класс LeafDECLTYPE.

Для кодирования типа возвращаемого оператором «decltype» выбрана литера 'X' (заглавная буква 'X', в отличие от прописной 'x', используемой для auto). В связи с этим изменилась функциональность классов TypeInfo и Encoding. Например, функции WhatIs, IsDecltype, MakePtree.

Функциональность по вычислению типов для оператора «decltype» реализована в классе Environment и входит в состав библиотеки VivaCore. Вычисление типа осуществляется в момент записи новой переменной/типа в Environment (функции RecordTypedefName, RecordDeclarator, RecordConstantDeclarator). За вычисление типа отвечает функция FixIfDecltype.

3. Ссылка на временный объект (R-value reference)


В стандарте C++98 временные объекты можно передавать в функции, но только как константную ссылку (const &). Следовательно, функция не в состоянии определить, временный это объект или нормальный, который тоже передали как const &.

В C++0x будет добавлен новый тип ссылки — ссылка на временный объект (R-value reference). Его объявление следующее: «ИМЯ_ТИПА &&». Оно может быть использовано как не константный, легально модифицируемый объект. Данное нововведение позволяет учитывать временные объекты и реализовывать семантику переноса (Move semantics). Например, если std::vector создается как временный объект или возвращается из функции — можно, создавая новый объект, просто перенести все внутренние данные из ссылки нового типа. Конструктор переноса std::vector через полученную ссылку на временный объект просто копирует указатель массива, находящийся в ссылке, которая по окончании устанавливается в пустое состояние.

Конструктор переноса или оператор переноса может быть объявлен следующим образом:
template<class T> class vector {
  // ...
  vector(const vector&); // copy constructor
  vector(vector&&);      // move constructor
  vector& operator=(const vector&); // copy assignment
  vector& operator=(vector&&);      // move assignment
};

С точки зрения анализа 64-битных ошибок в коде для нас не имеет значения, обрабатывается при объявлении типа '&' или '&&'. Соответственно поддержка данного нововведения в VivaCore весьма проста. Изменения затронули только функцию optPtrOperator класса Parser. В ней мы равнозначно воспринимаем как '&', так и '&&'.

4. Правые угловые скобки


С точки зрения стандарта C++98 следующая конструкция содержит синтаксическую ошибку:
list<vector<string>> lvs;
Для ее предотвращения необходимо вставить пробел между двумя правыми закрывающимися угловыми скобками:
list<vector<string> > lvs;
Стандарт С++0x узаконил использование двойных закрывающихся скобок при объявлении шаблонных типов, без необходимости вставлять между ними пробел. В результате становится возможным написание чуть более элегантного кода.

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

На данный момент, разбор объявлений шаблонных типов с ">>" пока реализован в VivaCore не лучшим образом. В ряде случаев анализатор ошибается и видимо со временем части анализатора, связанные с разбором шаблонов будут нами существенно переработаны. Пока в коде можно увидеть следующие некрасивые функции, которые эвристическими методами пытаются определить, имеем мы дело с оператором сдвига ">>" или с частью объявления шаблонного типа «A<B<C>> D»: IsTemplateAngleBrackets, isTemplateArgs. Тем, кому интересно, как корректно подойти к решению данной задачи, будет полезен следующий документ: "Right Angle Brackets (N1757)". Со временем мы улучшим обработку правых угловых скобок в VivaCore.

5. Лямбда-функции (Lambdas)


Лямбда-выражения в Си++ — это краткая форма записи анонимных функторов (объектов, которые можно использовать как функцию). Рассмотрим немного историю. В Си для создания функторов используются указатели на функцию:
/* callback-функция */
int compare_function(int A, int B) {
  return A < B;
}
 
/* объявление функции сортировки */
void mysort(int* begin_items,
            int num_items,
            int (*cmpfunc)(int, int));
 
int main(void) {
    int items[] = {4, 3, 1, 2};
    mysort(items,
           sizeof(items)/sizeof(int),
           compare_function);
    return 0;
}
Ранее в Си++ функтор создавали с помощью класса, у которого перегружен operator():
class compare_class {
  public:
  bool operator()(int A, int B) {
    return (A < B);
  }
};
  
// объявление функции сортировки
template <class ComparisonFunctor> 
void mysort (int* begin_items,
             int num_items,
             ComparisonFunctor c);
 
int main() {
    int items[] = {4, 3, 1, 2};
    compare_class functor;
    mysort(items,
    sizeof(items)/sizeof(int),
    functor);
}
В C++0x мы получаем возможность объявить функтор еще более элегантно:
auto compare_function = [](char a, char b)
  { return a < b; };

char Str[] = "cwgaopzq";
std::sort(Str,
          Str + strlen(Str),
          compare_function);
cout << Str << endl;
Мы заводим переменную compare_function которая является функтором и тип которой определяется компилятором автоматически. Затем мы может передать эту переменную в std::sort. Мы можем еще более сократить код:
char Str[] = "cwgaopzq";
std::sort(
  Str,
  Str + strlen(Str),
  [](char a, char b) {return a < b;}
);
cout << Str << endl;
Здесь "[](char a, char b) {return a < b;}" и есть не что иное, как лямбда-функция.

Лямбда-выражение всегда начинается со скобок [], в которых может быть указан список захвата. Затем идет необязательный список параметров и необязательный тип возвращаемого значения. Завершает объявление непосредственно тело функции. В целом формат написания лямбда функций следующий:
'[' [<список_захвата>] ']'
[ '(' <список_параметров> ')' ['mutable' ] ]
[ 'throw' '(' [<типы_исключений>] ')' ]
[ '->' <тип_возвращаемого_значения> ]
'{' [<тело_функции>] '}'
Список захвата указывает, к каким объектам из внешней области видимости имеет доступ лямбда-функция:
  • [] — без захвата переменных из внешней области видимости;
  • [=] — все переменные захватываются по значению;
  • [&] — все переменные захватываются по ссылке;
  • [x, y] — захват x и y по значению;
  • [&x, &y] — захват x и y по ссылке;
  • [in, &out] — захват in по значению, а out — по ссылке;
  • [=, &out1, &out2] — захват всех переменных по значению, кроме out1 и out2, которые захватываются по ссылке;
  • [&, x, &y] — захват всех переменных по ссылке, кроме x.
К сожалению, в рамках данной статьи не представляется возможным уделить лямбда-функциям большего внимания. Вы сможете подробнее познакомиться с лямбда-функциям посетив ресурсы, перечисленные в библиографическом списке в конце статьи. В качестве демонстрации использования лямбда-функций рассмотрим код программы, выводящей строки в порядке увеличения их длинны.

Программа создает массив строк и массив индексов. Затем программа сортирует индексы строк таким образом, что бы строки были расположены по увеличению их длины:
int _tmain(int, _TCHAR*[])
{
  vector<string> strings;
  strings.push_back("lambdas");
  strings.push_back("decltype");
  strings.push_back("auto");
  strings.push_back("static_assert");
  strings.push_back("nullptr");

  vector<size_t> indices;
  size_t k = 0;
  generate_n(back_inserter(indices),
             strings.size(),
             [&k]() { return k++; });

  sort(indices.begin(),
       indices.end(),
       [&](ptrdiff_t i1, ptrdiff_t i2)
       { return strings[i1].length() <
                strings[i2].length(); });

  for_each(indices.begin(),
           indices.end(),
           [&strings](const size_t i)
           { cout << strings[i] << endl; });

  return 0;
}
Примечание.Согласно С++0x можно инициализировать массивы std::vector следующим образом:
vector<size_t> indices = {0,1,2,3,4};
Но пока Visual Studio 2010 не поддерживает подобные конструкции.

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

В PVS-Studio реализована полноценная диагностика ошибок в лямбда-функциях. Рассмотрим пример кода, содержащий 64-битную ошибку:
int a = -1;
unsigned b = 0;
const char str[] = "Viva64";
const char *p = str + 1;

auto lambdaFoo = [&]() -> char
  {
    return p[a+b];
  };

cout << lambdaFoo() << endl;
Данный код работает при компиляции в режиме Win32 и печатает на экран букву 'V'. В режиме Win64 программа аварийно завершается из-за попытки обращению к элементу с номером 0xFFFFFFFF. Подробнее о данном виде ошибок рассказано в уроках по разработке 64-битных приложений на языке Си/Си++ — "Урок 13. Паттерн 5. Адресная арифметика".

При проверке приведенного выше кода, PVS-Studio выводит диагностическое сообщение:
error V108: Incorrect index type: p[not a memsize-type]. Use memsize type instead.
Соответственно анализатор должен был для этого разобрать лямбда-функцию и разобраться с областью видимости переменных. Непростая, но необходимая функциональность.

С поддержкой лямбда-функций связаны самые значительные изменения в VivaCore. В процессе построения дерева разбора теперь участвует новая функция rLambdas. Функция находится в классе Parser и вызывается из таких функций как rInitializeExpr, rFunctionArguments, rCommaExpression. Функция rLambdas разбирает лямбда-функции и добавляет в дерево новый тип объекта — PtreeLambda. Класс PtreeLambda объвлен и реализован в файлах PtreeLambda.h и PtreeLambda.

Обработку PtreeLambda в построенном дереве осуществляет функция TranslateLambda. Вся логика по работе с лямда-функциями сосредоточена в VivaCore. Внутри TranslateLambda вы встретите вызов функции GetReturnLambdaFunctionTypeForReturn, реализованную в коде PVS-Studio. Но данная функция служит для внутренних целей PVS-Studio и пустая заглушка-функция GetReturnLambdaFunctionTypeForReturn в VivaCore никак не скажется на разборе кода.

6. Suffix return type syntax


Бывают случаи, когда затруднительно указать тип, возвращаемый функцией. Рассмотрим пример шаблонной функции, перемножающей два значения:
template<class T, class U>
??? mul(T x, U y)
{
  return x*y;
}
Возвращаемый тип должен является типом выражения «x*y». Но, непонятно, что можно написать вместо "???". Первой идеей может быть использование «decltype»:
template<class T, class U>
decltype(x*y) mul(T x, U y) //Scope problem!
{
  return x*y;
}
Переменные «x» и «y» объявлены после «decltype(x*y)» и такой код, к сожалению, не может быть скомпилирован.

Решение данной проблемы заключается в использовании нового синтаксиса возвращаемых значений:
template<class T, class U>
[] mul(T x, U y) -> decltype(x*y)
{
  return x*y;
}
Используя скобки [] мы порождаем здесь лямда-функцию и при этом говорим «возвращаемый тип будет выведен или задан позже». К сожалению, хотя приведенный пример корректен, он на момент написания статьи не компилируется в Visual C++. Однако, мы можем использовать альтернативный вариант (где также используется Suffix return type syntax):
template<class T, class U>
auto mul(T x, U y) -> decltype(x*y)
{
  return x*y;
}
Этот код будет успешно собран Visual C++ и мы получим желаемый результат.

В версии PVS-Studio 3.50 поддержка нового формата функций реализована только частично. Конструкции полностью разбираются библиотекой VivaCore, но PVS-Studio при анализе не учитывает типы данных, возвращаемые этими функциями. Познакомиться с поддержкой альтернативной записи функций в библиотеке VivaCore можно в функции Parser::rIntegralDeclaration.

7. static_assert


В стандарте C++0x появилось новое ключевое слово static_assert. Синтаксис:
static_assert(выражение, "сообщение об ошибке");
В случае если выражение ложно, то выводится указанное сообщение об ошибке и компиляция прекращается. Рассмотрим пример использования static_assert:
template <unsigned n>
struct MyStruct
{
  static_assert(n > 5, "N must be more 5");
};

MyStruct<3> obj;
При компиляции данного кода компилятор Visual C++ выдаст сообщение:
error C2338: N must be more 5
  xx.cpp(33) : see reference to class template
  instantiation 'MyStruct<n>' being compiled 
  with
  [
    n=3
  ]
С точки зрения анализа кода, осуществляемого PVS-Studio, конструкция static_assert не представляет интереса и поэтому игнорируется. В VivaCore добавлена новая лексема tkSTATIC_ASSERT. Встречая эту лексему, лексер игнорирует ее и все параметры, относящиеся к конструкции static_assert (реализация в функции Lex::ReadToken).

8. nullptr


До стандарта C++0x в Си++ не было ключевого слова для обозначения нулевого указателя. Для его обозначения использовалось число 0. Однако хорошим стилем считалось использование макроса NULL. При раскрытии макрос NULL превращается в 0, и между ними нет практической разницы. Вот как объявлен макрос NULL в Visual Studio:
#define NULL    0
В ряде случаев отсутствие специального ключевого слова для обозначения нулевого указателя было неудобно и даже провоцировало возникновение ошибок. Рассмотрим пример:
void Foo(int a)
{ cout << "Foo(int a)" << endl; }

void Foo(char *a)
{ cout << "Foo(char *a)" << endl; }

int _tmain(int, _TCHAR*[])
{
  Foo(0);
  Foo(NULL);
  return 0;
}
Хотя программист может ожидать, что в данном коде будут вызваны разные функции Foo, это не так. Вместо NULL будет подставлен 0, имеющий тип «int» и при запуске программы на экране будет распечатано:
Foo(int a)
Foo(int a)
Для устранения подобных ситуаций в C++0x введено ключевое слово nullptr. Константа nullptr имеет тип nullptr_t и неявно приводится к любому типу указателя или к указателю на члены класса. Константа nullptr неявно не приводится к целочисленным типам данных за исключением типа «bool».

Вернемся к нашему примеру и добавим вызов функции «Foo» с аргументом nullptr:
void Foo(int a)
{ cout << "Foo(int a)" << endl; }

void Foo(char *a)
{ cout << "Foo(char *a)" << endl; }

int _tmain(int, _TCHAR*[])
{
  Foo(0);
  Foo(NULL);
  Foo(nullptr);
  return 0;
}
Теперь на экране будет распечатано:
Foo(int a)
Foo(int a)
Foo(char *a)
Хотя ключевое слово nullptr не представляет интереса с точки зрения поиска 64-битных ошибок, необходима его поддержка при разборе кода. Для этого в VivaCore был добавлена новая лексема tkNULLPTR, а также класс LeafNULLPTR. Создание объектов типа LeafNULLPTR происходит в функции rPrimaryExpr. При вызове функции LeafNULLPTR::Typeof тип «nullptr» кодируется как «Pv», то-есть «void *». С точки зрения существующих задач по анализу кода в PVS-Studio этого достаточно.

9. Новые стандартные классы


Стандарт C++0x вводит новые стандартные классы, относящиеся к namespace std. Ряд из этих классов уже поддерживаются в Visaul Studio 2010. В качестве примера можно привести:
  • std::array;
  • std::shared_ptr;
  • std::regex.

Поскольку перечисленные сущности являются обыкновенными шаблонными классами, то их появление не потребовало какой либо модификации PVS-Studio или библиотеки VivaCore.

10. Новые направления в развитии статических анализаторов кода


В конце хочется отметить один интересный момент, связанный с использованием C++0x. Новые возможности языка с одной стороны, исправляя старые недочеты, делают код безопаснее и эффективнее, но при этом также создают новые, пока неизвестные ловушки, в которые может попасть программист. Правда, о них рассказать я пока ничего не могу.

Но можно попасть и в уже известные ловушки из-за того, что их диагностика в новых конструкциях C++0x реализована намного хуже или вообще не реализована. Рассмотрим небольшой пример, демонстрирующий использование неинициализированной переменной:
{
  int x;
  std::vector<int> A(10);
  A[0] = x; // Warning C4700
}

{
  int x;
  std::vector<int> A(10);
  std::for_each(A.begin(), A.end(),
    [x](int &y)
    { y = x; } // No Warning
  );
}
Программист может надеяться получить предупреждение от компилятора как в первом, так и во втором случае. Но в примере с лямбда-функцией никакого предупреждения выдано не будет (испытано на Visual Studio 2010 RC, /W4). Как не было ранее и многих других предупреждений на различные опасные ситуации. Требуется время на реализацию подробной диагностики.

Можно ожидать новый виток развития статических анализаторов, с точки зрения поиска потенциально опасных конструкций, которые возникают при использовании конструкций C++0x. Мы позиционируем наш продукт PVS-Studio как средство для проверки современных программ. В настоящий момент мы понимаем под этим 64-битные и параллельные технологии. В будущем мы планируем провести исследования вопроса о том, какие потенциальные проблемы можно ожидать при использовании C++0x. Если подводных камней будет достаточно много, то возможно мы приступим к созданию нового инструмента для их диагностики.

Заключение


На наш взгляд C++0x привносит много положительных моментов. Старый код не требует немедленной модернизации, хотя может быть со временем модифицирован в ходе рефакторинга. Новый же код может уже писаться с использованием новых конструкций. Таким образом, начало использования C++0x выглядит рациональным уже сейчас.

Библиографический список


  1. Bjarne Stroustrup. C++0x — the next ISO C++ standard. http://www.viva64.com/go.php?url=304
  2. Visual C++ Team Blog. Rvalue References: C++0x Features in VC10, Part 2. http://www.viva64.com/go.php?url=305
  3. Сергей Олендаренко. C++0x. Лямбда-выражения. http://www.viva64.com/go.php?url=306
  4. Максим. С++0x и решение проблем инициализации. http://www.viva64.com/go.php?url=307
  5. Wikipedia. C++0x. http://www.viva64.com/go.php?url=301