воскресенье, 6 декабря 2009 г.

Многопоточность в C++0x

Поддержка работы с потоками была добавлена в С++. Энтони Уильямс предлагает ознакомиться с новыми возможностями. Перевод одноименной статьи из журнала Overload’93.

Согласованность и многопоточность, именно этим должны обладать фрагменты кода, выполняющиеся параллельно. Если у вас многоядерный процессор или многопроцессорная система, тогда код действительно выполняется параллельно, в обратном случае, многопоточность достигается искусственным способом, посредством возможностей операционной системы поочередно выделять кванты времени на выполнение задач, которые должны выполняться параллельно. Это очень хорошо, но вам нужно как-то указать, какой код выполнять на всех этих потоках.
Высокоуровневые конструкции, как, скажем, параллельные алгоритмы в Intel Treading Building Block [Intel] управляют разделением кода между потоками за вас, в С++0х все по другому. Вместо этого, мы должны сами управлять потоками. Инструментом для этого является std::thread. (За полной документацией и подробностями библиотеки, смотрите мою реализацию [JustThread]).

Выполнение простейшей функции в другом потоке.

Позвольте начать с простой функции в другом потоке, который мы создали как новый объект std::thread и передали в конструктор требуемую функцию. std::thread находится в заголовочном файле , таким образом вначале нужно его подключить. (Листинг 1).

  #include < thread >  
  void my_thread_func()  
  {}  
  int main()  
  {  
  std::thread t(my_thread_func);  
  }
 

Листинг 1.

Если вы скомпилируете и запустите это маленькое приложение, оно не сделает многого: запустит новый поток, тело функции которого пустое. Единственное, что оно сделает это завершится, причем не совсем подобающим образом, потому что мы запускаем поток а потом уничтожаем объект std::thread безо всякого ожидания. Оставляя пока этот момент в стороне, давайте пока добавим в функцию хоть что-то, например вывод на экран «hello» (Листинг 2)

  #include < thread >  
  #include < iostream >  
  void my_thread_func()  
  {  
  std::cout<< "hello" << std::endl;  
  }  
  int main()  
  {  
  std::thread t(my_thread_func);  
  }  


Листинг 2

Если вы скомпилируете а потом запустите это маленькое приложение, что случится? Напечатает ли оно hello как мы этого хотим? Так, на самом деле нельзя дать однозначный ответ. Может напечатать, а может и нет. Я запускал это приложение несколько раз на своем компьютере и результат был непостоянный: иногда надпись «hello» выводилась, с переводом на новую строку, иногда выводилась без новой строки, иногда не выводилось вообще ничего. В чем же дело? Должно ли такое простое приложение вести себя предсказуемо?

Ожидание завершения потока

Ну, на самом деле, нет, это приложение не должно вести себя предсказуемо. Проблема здесь заключается в том, что мы не ждем завершения нашего потока. Когда процесс выполнения доходит до конца функции main() программа завершается, вне зависимости от того, чем заняты другие потоки. Пока планирование потока непредсказуемо, мы не может знать, как далеко зашел другой поток. Он уже мог завершиться, мог уже напечатать “hello”, но еще не выполнить печать std::endl, или мог вообще еще не начать свое выполнение. В любом случае он будет внезапно остановлен при завершении работы приложения.
Если мы хотим что бы сообщение точно вывелось на экран, мы должны убедиться, что наш поток завершился. Мы это делаем с помощью соединения (joining) потоков, путем вызова функции join() члена нашего объекта потока (Листинг 3.)

  #include < thread >  
  #include < iostream >  
 
  void my_thread_func()  
  {  
  std::cout<< "hello" << std::endl;  
  }  
 
  int main()  
  {  
  std::thread t(my_thread_func);  
  t.join();  
  }  


Листинг 3

Теперь, main() будет ожидать, пока поток завершится перед выходом, и код выведет “hello” на экран с добавлением перехода на новую строку каждый раз. Это подчеркивает важный момент: если вы хотите, чтобы поток завершился в конкретной точке вашего кода, вы должны ожидать этого. Как и необходимость в убеждении, что поток завершился, перед тем как выполнение программы завершается, также важно еще это и в том случае, когда поток имеет доступ к локальным переменным: мы хотим, что бы поток завершился до того, как локальные переменные выйдут за пределы области видимости. Также необходимо избегать некорректных завершений программы – не вызвали join() или явно указали, что не собираетесь ожидать поток путем вызова detach(), в этом случае деструктор std::thread вызовет std::terminate().

Выполнение функции объекта в другом потоке

Было бы немного ограниченным, если бы новый поток был способен выполнять только простые функции без аргументов – вся необходимая нам информация в этом случае передавалась бы только через глобальные переменные, что негативно сказалось бы на чистоте кода. К счастью, у нас нет таких ограничений.
В соответствии со стандартной библиотекой C++, вы не ограничены только лишь простыми функциями при запуске потока – конструктор std::thread так же можно вызывать с экземплярами классов, в которых реализован оператор (). Давай переделаем предыдущий пример, в соответствии с этим (Листинг 4) .

  #include < thread >  
  #include < iostream >  
 
  class SayHello  
  {  
  public:  
  void operator()() cons  
  {  
  std::cout<< "hello" << std::endl;  
  }  
  };  
 
  int main()  
  {  
  std::thread t((SayHello()));  
  t.join();  
  }  


Листинг 4

Если вам интересно, для чего нужны дополнительные скобки при вызове конструктора SayHello, то я вам расскажу: что бы избежать проблем при парсинге данного кода компилятором. Без скобок объявление представляет собой вызов функции t, которая принимает указатель на функцию без параметров и возвращающей экземпляр SayHello, и который возвращает объект std::thread, а не объект t типа std::thread. Есть несколько других способов избежать проблемы. Во первых вы можете создать именованную переменную типа SayHello и передать ее в конструктор std::thread:

  int main()  
  {  
  SayHello hello;  
  std::thread t(hello);  
  t.join();  
  }  


Или же вы можете использовать инициализацию копированием:

 int main()  
 {  
  std::thread t=std::thread(SayHello());  
  t.join();  
  }
 

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

  int main()  
  {  
  std::thread t{SayHello()};  
  t.join();  
  }  


В данном случае, это абсолютный эквивалент нашему первому примеру с двойными скобками.
Так или иначе, хватит об инициализации. Вне зависимости от того, какой способ вы используете, идея та же: ваш объект-функция (function object) копируется во внутреннее хранилище, доступное для нового потока, и новый поток вызывает ваш operator(). Ваш класс, разумеется, может иметь другие члены с данными или другие члены-функции, и это является способом передачи данных в функцию потока: передать их как аргументы конструктора и сохранить во внутренней переменной класса (Листинг 5).

  #include < thread >  
  #include < iostream >  
  #include < string >  
 
  class Greeting  
  {  
  std::string message;  
  public:  
  explicit Greeting(std::string const& message_):  
  message (message_)  
  {}  
  void operator()() const  
  {  
  std::cout << message << std::endl;  
  }  
  };  
 
  int main()  
  {  
  std::thread t(Greeting("goodbye"));  
  t.join();  
  }
 

Листинг 5

В этом примере, наше сообщение хранится в переменной класса, таким образом, когда экземпляр Greeting копируется в поток, сообщение также копируется, и этот пример выведет на экран не “hello”, а “goodbye”.
Этот пример также демонстрирует способ передачи информации в новый поток вне вызова функции – включает член класса для данных объекта-функции. Если вам подходит способ использование объекта-функции – это идеальное решение, иначе, нам необходим другой способ.

Передача аргументов в функцию потока

Как мы видели ранее, одним из способов передачи параметров функции потока является вариант с членами класса. Хорошо, но необходимости каждый раз писать специальный класс нету; стандартная библиотека предлагает простой метод сделать это с использованием std::bind. Шаблонная функция std::bind принимает переменное число параметров. Первым параметром всегда является функция или вызываемый объект, которой нужны параметры, а оставшиеся аргументы являются параметрами для этой функции. Результатом является объект-функция, которая содержит копии предоставленных аргументов и оператор, который вызывает пользовательскую функцию. Мы можем использовать такой метод для передачи сообщения нашему новому потоку (Listing 6).

  #include < thread >  
  #include < iostream >  
  #include < string >  
  #include < functional >  
  void greeting(std::string const& message)  
  {  
  std::cout << message << std::endl;  
  }  
  int main()  
  {  
  std::thread t(std::bind(greeting,"hi!"));  
  t.join();  
  }  


Листинг 6

Этот код работает хорошо, но на самом деле мы может сделать его лучше – мы можем передать аргументы непосредственно конструктору std::thread и они будут скопированы в локальное хранилище для нового потока и предоставлены функции потока. Таким образом, наш код может принять следующий вид (Листинг 7)

  #include < thread >  
  #include < iostream >  
  #include < string >  
  void greeting(std::string const& message)  
  {  
  std::cout << message << std::endl;  
  }  
  int main()  
  {  
  std::thread t(greeting,"hi!");  
  t.join();  
  }  


Листинг 7

Этот код не только проще, но также он более эффективен, так как аргументы непосредственно копируются в локальное хранилище для потока, в отличие от предыдущего варианта, когда сначала они копировались в объект, генерируемый std::bind, который потом копировался во внутреннее хранилище потока.
Несколько аргументов может быть передано в конструктор std::thread (Листинг 8)

  #include < thread >  
  #include < iostream >  
  void write_sum(int x,int y)  
  {  
  std::cout << x << " + " << y << " =  
  " << (x+y) << std::endl;  
  }  
 
  int main()  
  {  
  std::thread t(write_sum,123,456);  
  t.join();  
  }  


Листинг 8

Конструкктор std::thread – шаблон с переменным числом параметров, таким образом, максимальное количество передаваемых параметров ограничено настройками компилятора, но если вам понадобится передать еще больше параметров, тогда очевидно нужно пересмотреть дизайн вашей функции.

Вызов функции-члена в новом потоке

Что если вы хотите вызвать функцию-член, который не является оператором, описанным выше?
Чтобы запустить новый поток, который вызывает функцию-член существующего объекта, вы просто передаете указатель на эту функцию и указатель на родительский объект в конструктор std::thread (Листинг 9).

  #include < thread >  
  #include < iostream >  
  class SayHello  
  {  
  public:  
  void greeting() const  
  {  
  std::cout << "hello" << std::endl;  
  }  
  };  
  int main()  
  {  
  SayHello x;  
  std::thread t(&SayHello::greeting,&x);  
  t.join();  
  }
 

Листинг 9

Разумеется, вы можете передать дополнительные аргументы в вызываему функцию (Листинг 10).

  #include < thread >  
  #include < iostream >  
  class SayHello  
  {  
  public:  
  void greeting(std::string const& message) const  
  {  
  std::cout << message << std::endl;  
  }  
  };  
  int main()  
  {  
  SayHello x;  
  std::thread t(  
  &SayHello::greeting,&x,"goodbye");  
  t.join();  
  }  


Листинг 10

Сейчас, оба предшествующие примера используют обыкновенный указатель на локальный объект; если вы так поступаете, вам необходимо быть уверенным в том, что объект, на который вы ссылаетесь, будет действительным в течение всего периода жизни потока, иначе возникнут проблемы. Как вариант, можно использовать динамическое создание объекта на куче и использовать «умный» указатель как std::shared_ptr для уверенности в том, что объект будет оставаться действительным на протяжении всего времени жизни потока:

  #include < thread >  
  int main()  
  {  
  std::shared_ptr p(new SayHello);  
  std::thread t(&SayHello::greeting,p,"goodbye");  
  t.join();  
  }
 

Итак, все примеры, с которыми мы имели дело, копировали аргументы и функцию потока во внутреннее хранилище потока, даже, если аргументами были указатели, как в случае с указателем this для члена-функции. Но что если нам необходимо передать ссылку на существующий объект, а не указатель? Это задача для std::ref.

Передача функции и аргументов в поток по ссылке

Предположим у вас есть объект, в котором реализован оператор-функция (operator()) и вы хотите вызвать его в новом потоке. Здесь нюанс заключается в том, что вы хотите вызвать функцию-оператор именно этого конкретного объекта, а не его копии. Вы можете использовать поддержку функции-члена что бы явно вызвать operator(), но это выглядит немного неестественно. Таким образом, это случай для использования std::ref – если x вызываемый объект, то std::ref(x) тоже, таким образом, мы можем передать std::ref(x) как нашу функцию, когда мы запускаем поток (Листинг 11).

  #include < thread >  
  #include < iostream >  
  #include < functional > // for std::ref
 
  class PrintThis  
  {  
  public:  
  void operator()() const  
  {  
  std::cout << "this=" << this << std::endl;  
  }  
  };  
  int main()  
  {  
  PrintThis x;  
  x();  
  std::thread t(std::ref(x));  
  t.join();  
  std::thread t2(x);  
  t2.join();  
  }
 

Листинг 11

В данном случае, функция-оператор выводит на экран адрес объекта. Реальные значения будут отличаться от случая к случаю, но принцип один и тот же: эта маленькая программа выведет три строки. Первый две будут одинаковыми, тогда как третья будет отличаться, так как она представляет собой результат вызова функции-оператора копии x. Вот такой результат можно получить:

  this=0x7fffb08bf7ef  
  this=0x7fffb08bf7ef  
  this=0x42674098


Разумеется, std::ref можно использовать и для других аргументов тоже – следующий код в листинге 12 выведет «x=43».

  #include < thread >  
  #include < iostream >  
  #include < functional >  
 
  void increment(int& i)  
  {  
  ++i;  
  }  
 
  int main()  
  {  
  int x=42;  
  std::thread t(increment,std::ref(x));  
  t.join();  
  std::cout << "x=" << x << std::endl;  
  }  


Листинг 12

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

Защита данных общего пользования с помощью std::mutex

Мы видели, как запустить поток для выполнения задачи «в фоне» и дождаться его завершения. Вы можете выполнить много полезной работы, подобной этой, передавая данные через параметр функции потока, и потом, получая результат, когда поток завершен. Однако, вы так не будете поступать, если вам необходимо взаимодействие между потоками пока они выполняются – совместный доступ к общей памяти для нескольких потоков может вызвать неопределенное поведение, если какой-нибудь поток будет модифицировать данные. Здесь вам необходим какой способ, позволяющий убедиться, что доступ к данным взаимно исключающий, таким образом, один поток может получить доступ к данным в одно время.
Концепция мьютексов проста. Он может быть «заблокирован» или «незаблокирован», и поток пытается заблокировать мьютекс, когда хочет получить доступ к некоторым защищенным данным. Если мьютекс уже заблокирован, тогда все потоки пытающиеся заблокировать мьютекс будут вынуждены ожидать. Как только поток завершил все свои операции с защищенными данными, он освобождает мьютекс и другой поток может его заблокировать. Если вы будете контролировать, что поток всегда блокирует мьютекс, до того как обращается к данным общего пользования, то другие потоки не смогут получить доступ к этим данным, пока мьютекс заблокирован другим потоком. Это предотващает совместный доступ одновременно несколькими потоками, и позволяет избежать неопределенного поведения при доступе к данным. Самый простой мьютекс, который есть в С++ это std::mutex, который описан в заголовчном файле с другими типами мьютексов и классов блокировок.
В std::mutex есть функции для явной блокировки и освобождения, и как правило мьютекс используется для блокировки доступа к конкретному региону в коде. В данном случае удобно использовать шаблон std::lock_guard<>, который подходит для данного сценария. Конструктор блокирует мьютекс, а деструктор освобождает его, таким образом, что бы заблокировать мьютекс для блока кода, необходимо только сконструировать объект std::lock_guard<> как локальную переменную в начале этого блока. Например, для того что бы защитить счетчик общего доступа, можно воспользоваться std::lock_gurad<> , что бы быть уверенным, что мьютекс заблокирован как для приращения, так и для получения его значения. (Листинг 13)

  #include < mutex >  
  std::mutex m;  
  unsigned counter=0;  
  unsigned increment()  
  {  
  std::lock_guard lk(m);  
  return ++counter;  
  }  
  unsigned query()  
  {  
  std::lock_guard lk(m);  
  return counter;  
  }  


Листинг 13

Это упорядочивает доступ к счетчику – если более чем один поток одновременно вызывает метод query(), то один из них заблокирует мьютек на время выполнения функции, а все остальные будут ожидать своей очереди. Подобным образом будет выполняться метод increment(). Поскольку обе функции используют для блокировки один и тот же мьютекс, в случае если один поток вызовет query() а другой в тоже время increment(), то только один сможет заблокировать мьютекс, заставив таким образом второго ожидать. Это взаимное исключение и есть суть мьютекса.

Безопасность исключений и мьютексы

Использование std::lock_gurad<> для блокировки мьютекса имеет еще одно преимущество над ручной блокировкой и освобождением, когда речь заходит о безопасности исключений. С ручной блокировкой, вы обязаны быть уверенными в том, что мьютекс освобожден корректно, вне зависимости от того как завершилось выполнение региона кода, который нуждается в мьютексе, включая случай когда выполнение кода завершается из-за возникшего в нем исключения. Предположим на момент, что вместо защиты целочисленной переменной нам необходима защита доступа и добавление данных в std::string. Добавление данные в строку может потребовать выделения памяти, а эта операция в свою очередь может вызвать исключение, если память не может быть выделена. При использовании std::lock_guard<> это не проблема – если возникает исключение, мьютекс освобождается. Чтобы получить такое же поведение для ручной блокировки мы должны использовать блок catch, как показано в Листинге 14.

  #include < mutex >  
  #include < string >  
  std::mutex m;  
  std::string s;  
  void append_with_lock_guard(  
  std::string const& extra)  
  {  
  std::lock_guard lk(m);  
  s+=extra;  
  }  
  void append_with_manual_lock(  
  std::string const& extra)  
  {  
  m.lock();  
  try  
  {  
  s+=extra;  
  m.unlock();  
  }  
  catch(...)  
  {  
  m.unlock();  
  throw;  
  }  
  }
 

Листинг 14

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

Гибкая блокировка с std::unique_lock<>

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

  1. Вы можете передавать владение блокировкой между объектами, и
  2. Объект std::unique_lock<> не обязан владеть блокировкой мьютекса, с которым он ассоциирован. 

Давайте разберем каждый из пунктов по очереди, начиная с передачи владения.

Передача владения блокировкой мьютекса между экземплярами std::unique_lock<>

Существует несколько последовательностей передачи владения блокировкой мьютекса между экземплярами std::unique_lock<>: вы можете возвратить блокировку из функции, вы можете сохранить блокировку в стандартном контейнере, и т.д.
Например, вы можете написать простую функцию, которая получает блокировку мьютекса:

  std::unique_lock acquire_lock()  
  {  
  static std::mutex m;  
  return std::unique_lock(m);  
  }  


Возможность передачи владения блокировкой между объектами также обеспечивает простой способ написания классов, которые сами по себе могут перемещаться, но хранить блокировку, это продемонстрировано на листинге 15:

  #include < mutex >  
  #include < utility >  
  class data_to_protect  
  {  
  public:  
  void some_operation();  
  void other_operation();  
  };  
  class data_handle  
  {  
  private:  
  data_to_protect* ptr;  
  std::unique_lock lk;  
  friend data_handle lock_data();  
  data_handle(data_to_protect* ptr_,  
  std::unique_lock lk_):  
  ptr(ptr_),lk(lk_)  
  {}  
 
  public:  
  data_handle(data_handle && other):  
  ptr(other.ptr),lk(std::move(other.lk))  
  {  
  other.ptr=0;  
  }  
  data_handle& operator=(data_handle && other)  
  {  
  if(&other != this)  
  {  
  ptr=other.ptr;  
  lk=std::move(other.lk);  
  other.ptr=0;  
  }  
  return *this;  
  }  
  void do_op()  
  {  
  ptr->some_operation();  
  }  
  void do_other_op()  
  {  
  ptr->other_operation();  
  }  
  };  
 
  data_handle lock_data()  
  {  
  static std::mutex m;  
  static data_to_protect the_data;  
  std::unique_lock lk(m);  
  return data_handle(&the_data,std::move(lk));  
  }  
 
  int main()  
  {  
  data_handle dh=lock_data(); // lock acquired
  dh.do_op(); // lock still held
  dh.do_other_op(); // lock still held
  data_handle dh2;  
  dh2=std::move(dh); // transfer lock to
  // other handle
  dh2.do_op(); // lock still held
  } // lock released  


Листинг 15

В данном случае lock_data() приобретает блокировку мьютекса, предназначенного для защиты данных, а затем передает ее по указателю на данные data_handle. Эта блокировка храниться в data_handle до тех пор, пока обработчик не будет уничтожен, позволяя выполнения операций с данными без освобождения блокировки. Поскольку std::unique_lock<> перемещаемый (movable), достаточно просто сделать перемещаемым и data_handle, что необходимо в случае, когда он возвращается из lock_data.
Хотя возможность передачи владения между объектами полезна, большего внимания заслуживает простота возможности управлять владением блокировки раздельно от времени жизни экземпляра std::unique_lock<>.

Явная блокировка и освобождение мьютекса с помощью std::unique_lock<>

Как вы видели раньше, std::lock_guard<> очень прямолинейно ведет себя по отношению к владению блокировкой – он владеет блокировкой от создания до разрушения, без дополнительных возможностей. std::unique_lock<> немного гибче. Получив блокировку в конструкторе, так же как и в случае с std::lock_guard<>, вы можете:

• Создать экземпляр без ассоциации с мьютексом вовсе (с помощью конструктора по умолчанию);
• Создать экземпляр с ассоциированием ему мьютекса, но оставив мьютекс незаблокированным (с помощью конструктора с отложенной блокировкой);
• Создать экземпляр, который пытается заблокировать мьютекс, но оставляет его незаблокированным, в случае если блокировка не удалась (с конструктором пытающимся применить блокировку);
• Если у вас есть мьютекс, поддерживающий блокировку с таймаутом (как например, std::timed_mutex), то вы можете сконструировать экземпляр, пытающийся применить блокировку в назначенное время, или до какого-то времени, и оставить мьютекс незаблокированным, если заданное время вышло;
• Заблокировать мьютекс, если экземпляр std:unique_lock<> не владеет в данный момент времени блокировкой (с помощью функции lock());
• Попытаться и получить блокировку мьютекса, если экземпляр std:unique_lock<> не владеет в данный момент блокировкой (возможно с таймаутом, если мьютекс имеет такую поддержку)(используя функции try_lock(), try_lock_for() и try_lock_until());
• Освободить мьютекс, если в данный момент времени std::unique_lock<> владеет блокировкой (используя функцию unlock());
• Проверить, владеет ли экземпляр блокировкой (с помощью вызова owns_lock());
• Убрать связь между экземпляром и мьютексом, оставляя мьютекс в своем текущем состоянии (заблокированном или освобожденном) (используя функцию release());
• Передать владение другому экземпляру, как было описано выше.

Как вы видите std::unique_lock<> достаточно гибок: он дает вам полный контроль над подчиненным мьютексом, и удовлетворяет всем требованиям для блокируемого (Lockable) объекта. Вы можете также использовать конструкции вида std::unique_lock<>> если хотите! Однако, несмотря на всю свою гибкость, вам также предоставляется безопасность исключений: если при уничтожении объекта мьютекс все еще заблокирован, он освобождается в деструкторе.

std::unique_lock<> и условные переменные

Вся гибкость std::unique_lock<> может использоваться вместе с std::condition_variable. std::condition_variable предоставляют реализацию условных переменных, которые позволяют потоку ожидать до тех пор, пока он не будет уведомлен, что желаемое условие истинно. Во время ожидания, вы должны указать эти условия экземпляру std::unique_lock<>, который владеет блокировкой мьютекса. Условные переменные используют гибкость std::unique_lock<> для высвобождения мьютекса во время ожидания, а затем блокируют его опять, перед возвращением вызывающему коду. Это делает возможным другим потокам получить доступ к защищенным данным пока поток заблокирован. Полное обсуждение условных переменных – это тема отдельной статьи, поэтому сейчас мы на это и остановимся.

Другие приемы гибкой блокировки

Ключевым преимуществом гибкой блокировки является то, что время жизни заблокированного объекта не зависит от времени, когда к нему применяется блокировка. Это значит, что вы можете освободить мьютекс перед завершением функции, если требуемые условия выполнены, или освободить его, пока выполняется операция, требующая длительного времени (например, ожидание условных переменных, как это было описано выше) и затем заблокировать мьютекс опять, как только данная операция завершилась. Оба эти случая олицетворяют главный совет делать блокировку на как можно меньшее время, не принося в жертву безопасность исключений, когда осуществлена блокировка, и без необходимости писать сложный код, что бы получить время жизни блокировки объекта, что бы поставить его в соответствие со временем, когда нужна эта блокировка.
Например, в следующем примере мьютекс освобождается на время выполнения операции load_strings(), несмотря на то, что должен осуществляться контроль доступа к переменной strings_to_process (Листинг 16).

  std::mutex m;  
  std::vector strings_to_process;  
  void update_strings()  
  {  
  std::unique_lock lk(m);  
  if(strings_to_process.empty())  
  {  
  lk.unlock();  
  std::vector  
  local_strings=load_strings();  
  lk.lock();  
  strings_to_process.insert(  
  strings_to_process.end(),  
  local_strings.begin(),  
  local_strings.end());  
  }  
  }
 

Листинг 16

Следует отметить, что в данном фрагменте мы полагаемся на то, что функция update_strings(), которая добавляет строки в список выполнятся одним потоком – если она вызывается несколькими потоками одновременно, необходимо быть уверенным и в том, что load_strings() тоже потокобезопасна, и ее поведение соответствует желаемому. Например, если вы хотите, что бы только один поток вызывал load_strings() тогда может потребоваться дополнительная проверка. Как правило, если вы освобождаете мьютекс, вы должны полагать, что когда вы осуществите блокировку опять, защищаемые данные могли быть изменены.

Выводы

В С++0х вы можете управлять потоками с помощью класа std::thread. Существует много способов запустить поток, но есть только один дождать его завершения – join(). Если вы забудете вызвать этот метод библиотека времени исполнения принудительно завершит ваше приложение.
Как только вы создали и запустили на выполнение ваши потоки, вам необходимо быть уверенными, что данные совместного доступа синхронизированы должным образом, и самый простой способ сделать это – использовать мьютекс std::mutex. Наиболее безопасный способ заблокировать мьютекс - это использовать экземпляр std::lock_guard<>, однако вы можете использовать и std::unique_lock<> который предоставляет гораздо большую гибкость.
Синхронизировать данные в C++0x можно не только с помощью мьютексов, есть и другие способы осуществлять блокировку, кроме как использовать std::lock_guard<> и std::unique_lock<>, но им еще предстоит немного подождать.

Ссылки

[Intel] http://www.threadingbuildingblocks.org/ 
[JustThread] http://www.stdthread.co.uk/doc/


суббота, 12 сентября 2009 г.

Исследование фреймворков для Юнит тестирования на C++

Одной из моих предыдущих тем обсуждения было сказано о применимости методики разработки через тестирование (Test Driven Development) для игровых приложений. Каждый раз эта тема всплывает в обсуждениях и почтовых рассылках. Почти каждый проявляет неимоверное любопытство об этой методике и хочет узнать об этом еще больше. Обещаю в скором времени об этом написать.

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

В данной статье будут рассмотрены следующие Фреймворки:

CppUnit

Boost.Test

CppUnitLite

NanoCppUnit

Unit++

CxxTest

Вместо введения

Как мы выбираем фреймворк для юнит-тестов?Все зависит от того как мы собираемся им пользоваться, и для чего мы собираемся его использовать. Если бы я использовал Java для своей работы, выбор был бы очевиден - JUnit – оптимальный выбор для Java программистов. Я не слышал, что бы программисты обсуждали и предлагали что-то новое, поэтому данный фреймворк, похоже, лучшее, что сейчас существует для Java.

К сожалению, в случае C++ дела обстоят несколько иначе. Существует представитель семейства XUnit, CppUnit, но он не достаточно хорош в отдельных случаях. Существуют также другие фреймворки, которые можно использовать. Также, многие команды используют свои собственные изобретения. Почему так? Может C++ не совсем отвечает требованиям для разработки юнит-тестов, из-за чего имеются проблемы при использовании XUnit для этого языка?Не похоже, что это так. Очевидно лишь одно: разнообразие - это хорошо. Иначе при написании статьи мне пришлось бы использовать Windows, а вам для чтения использовать только Internet Explorer. В любом случае, я не первый кто задается этим вопросом. Эта страница пытается ответить на этот вопрос, и приходит к закономерным и логичным выводам: различия в компиляторах, платформах и стилях программирования. C++ не достаточно однозначен, он не является полностью поддерживаемым языком с единым стандартом кодирования.

Хорошей отправной точкой может послужить создание списка возможностей, которые имеют большое значение для того типа работы, которую я собираюсь сделать. В частности, я хочу заняться разработкой через тестирование (TDD), которая подразумевает, что я собираюсь написать и выполнить много маленьких тестов. Их планируется использовать в разработке игровых приложений, соответственно мне бы хотелось выполнить тесты на различных платформах (PC, Xbox, PS2, консолях следующего поколения и т.д.) Они должны соответствовать моему персональному стилю TDD (много тестов, массовое использование наработок и пр.)

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

  • Минимальное количество работы, требуемое для создания нового теста. Я собираюсь создавать тесты постоянно, следовательно, мне не хотелось бы, что бы добавление теста сопровождалось длительным процессом набора теста на клавиатуре, а в особенности, было бы неплохо избавить себя он необходимости дублирования исходного кода. Краткость и простота написания сделают возможным более прозрачным рефакторинг исходного кода, которые в случае TDD имеет огромное значение.
  • Простота в модификации и портировании. Не должно быть как-то зависимостей от нестандрартных библиотек или каких то «экзотических» особенностей языка (RTTI, обработка исключений и пр.). Некоторые компиляторы, которые используются для консольной разработки далеко не современны. Что бы убедиться в этом, я создавал юнит-тесты для каждого из фреймворков под Linux с использованием g++. Большинство тестов было написаны с учетом возможности их использования под Windows и Visual Studio.
  • Поддержка установки/удаления шагов (установщики(fixtures)). Я перенял стиль, рекомендованный Девидом Астелсом в его книге Test Driven Development, Практическом руководстве об использовании только одного утверждения на один тест. Это действительно делает тесты несколько проще для понимания и поддержки, но с другой стороны возрастает необходимость в тяжелом использовании установщиков. Фреймворки без таковых вычеркиваются незамедлительно. Дополнительным плюсом для фреймворков является возможность объявления объектов используемых в установщиках на стеке (или же создающиеся непосредственно перед вызовом теста) в противовес необходимости выделять их динамически.
  • Хорошая обработка исключений и крешей. Нам не хотелось бы останавливать тесты только из-за того, что какой то код пытается получить доступ к некорректному адресу памяти или в случае деления на нуль. Фреймворк для юнит-тестов должен информировать об возникающих исключительных ситуациях настолько детально, насколько это возможно. Должна также существовать возможность запускать выполнение повторно и иметь возможность установить брейкпоинт в месте возникновения исключительной ситуации.
  • Хорошая функциональность ASSERT. Ложное тестовое выражение должно распечатывать содержимое переменных, которые вовлечены в данное сравнение. Также должно существовать расширенное множество выражений проверки для случая «практически равно» (необходимо для переменных с плавающей запятой), меньше чем, больше чем и пр. Дополнительным плюсом будет являться наличие проверки на предмет, было выброшено исключение или нет.
  • Поддержка различных методов вывода информации. По умолчанию мне бы хотелось иметь формат, который был бы понятен для таких IDE, как Visual Studio или Kdevelop, тогда было бы просто определять непройденные тесты, когда имеют место синтаксические ошибки. Но я также хотел бы иметь возможность различных методов вывода информации (более детализированные, более короткие, более читабельные и пр.)
  • Поддержка TestSuit'ов. Этот пункт имеет очень маленький приоритет в моем списке, в то время как о нем заявлено в списках возможностей большинства фреймворков. Откровенно говоря, у меня было мало необходимости в данной возможности в прошлом. Да, это хорошо, но я рассмотрел много библиотек, каждая из которых имеет собственное множество тестов, таким образом, я едва в этом нуждаюсь. Тем не менее, было бы хорошо иметь окружение для запуска тестов в заданной точке выполнения.

Что еще: поддержка тайминга. Как для определения общего времени выполнения тестов, так и для каждого теста в отдельности. Мне нравится, когда я знаю, что сколько времени выполняется. Не столько в целях производительности, сколько для того, что бы избежать ситуаций, когда выполнение может быть неконтролируемым. Я предпочитаю, что бы выполнение тестов ограничивалось 3-4 секундами (это логичное желание, если необходимо запускать тест очень часто). В идеале, было бы неплохо, что бы выводилось предупреждение, когда выполнение теста занимает больше времени, чем допустимо.

Простота установки не учитывалась в списке приоритетов; в конце концов, это делается лишь один раз – а создание новых тестов занимает целые дни. Некомереческие лицензии (GPL или LGPL) так же не были учтены в моем списке требований, потому что фреймворк для юнит-тестов мы можем не включать в нашу конечную программу, таким образом, у нас не будет каких-либо ограничений из-за этого в финальной версии создаваемого приложения.

Кстати, в ходе моих исследований для этой статьи, я обнаружил, что список подобных требований был составлен уже до меня. Интересно сравнить эти требования и пожелания с моими.

Идеальный Фреймворк

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

Максимально простой тест должен быть тривиален. Лишь одна строка для создания теста и дальше само тело теста:

TEST (SimplestTest)

{

float someNum = 2.00001f;

ASSERT_CLOSE (someNum, 2.0f);

}

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

SETUP (FixtureTestSuite)

{

float someNum = 2.0f;

std::string str = "Hello";

MyClass someObject("somename");

someObject.doSomethng();

}

TEARDOWN (FixtureTestSuite)

{

someObject.doSomethingElse();

}

TEST (FixtureTestSuite, Test1)

{

ASSERT_CLOSE (someNum, 2.0f); someNum = 0.0f;

}

TEST (FixtureTestSuite, Test2)

{

ASSERT_CLOSE (someNum, 2.0f);

}

TEST (FixtureTestSuite, Test3)

{

ASSERT_EQUAL(str, "Hello");

}

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

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

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

CppUnit

CppUnit вероятно наиболее распространенный фреймворк для юнит-тестов на C++, поэтому будет логичным сравнивать его с другими системами тестирования. Я использовал CppUnit три или четыре года назад и мои впечателния были менее приятными. Я помню, мой код вперемешку с кодом MFC составлял какую-то непонятную чачу, все примеры в фреймворке были запутаны, и ничтожная GUI панель кое-как взаимодействовала с программой. Я даже было создал патч, который обеспечивал консольный вывод и убирал зависимости от MFC. Теперь же я возвращаюсь к данному Фреймворку, уже зная его слабые места.

Я допускаю, что CppUnit прошел огромный путь с тех пор. Я ожидал наихудшего, но в данный момент он стал намного проще для использования и конфигурации, чем было раньше. Хотя и сейчас, он не является совершенным, но он стал намного, намного лучше чем когда я использовал его раньше. Документация вцелом достаточная, но вам так или иначе придется погружаться глубоко в описание модуля, только для того, что бы разобраться с существующей функциональностью.

  • Минимальное количество работы, требуемое для создания нового теста. Это один из самых больших проблем CppUnit, которое, по иронии, является самой важной для меня. CppUnit требует проделать немного работы, для того что бы создать примитивный тест.

// Simplest possible test with CppUnit

#include

class SimplestCase : public CPPUNIT_NS::TestFixture

{

CPPUNIT_TEST_SUITE( SimplestCase );

CPPUNIT_TEST( MyTest );

CPPUNIT_TEST_SUITE_END();

protected:

void MyTest();

};

CPPUNIT_TEST_SUITE_REGISTRATION( SimplestCase );

void SimplestCase::MyTest()

{

float fnum = 2.00001f;

CPPUNIT_ASSERT_DOUBLES_EQUAL( fnum, 2.0f, 0.0005 );

}

  • Простота в модификации и портировании. По этому поводу здесь двойственное ощущение. С одной стороны, CppUnit работает на Windows и Linux, и функциональность хорошо смодулирована (результаты, исполнители (runners), данные вывода). С другой стороны, CppUnit по-прежнему требует RTTI, STL и (я думаю) механизм обработки ошибок. Конечно, это не конец света, что данные технологии требуются, но очевидно, что если я захочу использовать библиотеки, которые не поддерживают RTTI или откажусь о STL, то у меня появятся проблемы.
  • Поддержка установщиков. Да. Если вы хотите, что бы объекты создавались перед каждым тестом, необходимо, что бы они были динамически созданы в setup(), таким образом бонуса здесь нету.

#include

#include "MyTestClass.h"

class FixtureTest : public CPPUNIT_NS::TestFixture

{

CPPUNIT_TEST_SUITE( FixtureTest );

CPPUNIT_TEST( Test1 );

CPPUNIT_TEST( Test2 );

CPPUNIT_TEST( Test3 );

CPPUNIT_TEST_SUITE_END();

protected:

float someValue;

std::string str;

MyTestClass myObject;

public:

void setUp();

protected:

void Test1();

void Test2();

void Test3();

};

CPPUNIT_TEST_SUITE_REGISTRATION( FixtureTest );

void FixtureTest::setUp()

{

someValue = 2.0;

str = "Hello";

}

void FixtureTest::Test1()

{

CPPUNIT_ASSERT_DOUBLES_EQUAL( someValue, 2.0f, 0.005f );

someValue = 0;

//System exceptions cause CppUnit to stop dead on its tracks

//myObject.UseBadPointer();

// A regular exception works nicely though myObject.ThrowException();

}

void FixtureTest::Test2()

{

CPPUNIT_ASSERT_DOUBLES_EQUAL( someValue, 2.0f, 0.005f );

CPPUNIT_ASSERT_EQUAL (str, std::string("Hello"));

}

void FixtureTest::Test3()

{

// This also causes it to stop completely

//myObject.DivideByZero();

// Unfortunately, it looks like the framework creates 3 instances of MyTestClass

// right at the beginning instead of doing it on demand for each test. We would

// have to do it dynamically in the setup/teardown steps ourselves.

CPPUNIT_ASSERT_EQUAL (1, myObject.s_currentInstances);

CPPUNIT_ASSERT_EQUAL (3, myObject.s_instancesCreated);

CPPUNIT_ASSERT_EQUAL (1, myObject.s_maxSimultaneousInstances);

}

  • Хорошая обработка ошибок и крешей. Да. CppUnit использует концепцию «протекторов» (protectors), которые являются обертками над тестами. Встроенный протектор перехватывает все исключения (и идентифицирует некоторые из них). Вы можете написать ваш собственный протектор и поместить его в стек, для того, что бы скомпоновать его с уже существующими. Я не обрабатывал системные исключения под Linux, но с добавлением нового протектора у меня не было проблем. Похоже, что тут есть возможность отключить обработку исключений и позволить отладчику остановить выполнение, когда возникнет исключение (нет дефайнов или параметров коммандной строки).
  • Хорошая функциональность ASSERT. Хорошая. Здесь есть минимальный набор выражений ASSERT, включающий работу с числами с плавающей запятой. Нету выражений для «меньше чем», «больше чем» и т.д. Содержимое переменных, которые сравниваются, выводится в поток, если ASSERT выполняется, предоставляя вам, столько информации, насколько это возможно о неудачном выполнении теста.
  • Поддержка различных методов вывода информации. Да. Имеется очень хорошо определенная функциональсть для вывода информации (содержит результаты выполнения теста), которая работает также хорошо и для слушателей (listener) (которые уведомляются в момент выполнения конкретного теста). Вывод информации имеет хороший формат, который замечательно интегрирован в Visual Studio. Также имеется поддержка прогресс баров.
  • Поддержка TestSuit'ов. Да.

Таким образом, CppUnit разочаровывает, поскольку она предоставляет практически все, что мне нужно, за исключением особенностей, критичных для меня. Я действительно не могу поверить, что необходимо набирать столько кода вручную (к тому же писать дублирующий код), что бы добавить новый тест. Кроме этого, главное недовольство вызывает необходимость наличия RTTI или исключений, и соответствующая сложность исходного кода, которая может понадобиться в случае портирования на различные платформы.

Boost.Test

Обновлено: Я пересмотрел свой рейтинг и коментарии к фреймворку Boost.Test в свете комментариев Геннадия Розенталя, который показал мне, как просто добавлять установщик в Boost.

Я большой фанат Boost'а, но я хочу отметить, что это было не так год назад, до того как я узнал, что Boost предоставляет библиотеку для юнит-тестирования. Несомненно, я должен был в ней разобраться.

Первый сюрприз заключается в том, что Boost.Test не только фреймворк для юнит-тестирования. Он так же содержит в себе другие инструменты для тестирования. Ничего плохого в этом нету, но для меня это первый знак «дурного запаха». Другим сюрпризом является тот факт, что данный фреймворк не основан на семействе Xunit. Хммм.. В этом случае, он должен лучше предоставлять функциональность.

Документация была на высшем уровне. Одна из наилучших, которые я только видел среди многих фреймворков для тестов. Концепция была подробно описана и имела массу небольших примеров, демонстрирующих различные особенности. Интересно было заметить, что, изучая документацию Boost.Test, я обнаружил, что многие вещи спроектированы таким образом, что я отнес бы это к плохим практикам, например, зависимости между тестами или длинные тесты.

  • Минимальное количество работы, требуемое для создания нового теста. Почти. Boost.Test требует действительно минимум действий, которые необходимо произвести для добавления нового теста. Это очень сильно похоже на идеальный фреймворк для тестирования, который был описан ранее. К сожалению, добавление тестов, которые являются частью TestSuite требует больше работы и явной регистрации каждого теста.

#include <boost/test/auto_unit_test.hpp>

#include

BOOST_AUTO_UNIT_TEST (MyFirstTest)

{

float fnum = 2.00001f;

BOOST_CHECK_CLOSE(fnum, 2.f, 1e-3);

}

  • Простота в модификации и портировании. Фреймворк получил смешанную оценку по данному пункту, по тем же причинам, что и CppUnit. Являясь частью библиотеки Boost, к портированию здесь подошли очень серьезно. Фреймворк работает безупречно на Linux (лучше чем большинство фреймворков). Но у меня есть вопрос по поводу того, насколько легко в самом деле добраться до исходного кода и начать делать изменения. Также случается, что необходимо добавить поддержку некоторых дополнительных заголовочных файлов из других библиотек Boost, таким образом, все вместе это уже не выглядит таким простым и самодостаточным.
  • Поддержка установщиков. Boost.Test не содержит структуры установки/удаления, подобной той, что имеется в тестах CppUnit в стиле конструкторов/деструкторов простого C++. Поначалу это притормозила скорость моего исследования. После длительного использования подхода установки/удаления и удобной комплексной установки, я не видел обычного пути использования построения установщиков. Теперь я разобрался с данным фреймворком и пришел к выводу, что это даже лучше привычного мне подхода установки/удаления. Одним из наиболее значимых преимуществ данного подхода, это то, что вам не нужно создавать объект установщика динамически, а вместо этого вы можете поместить установщик на стеке. С другой стороны, эта необходимость ссылаться на переменные в установщике через имя объекта несколько раздражает. Было бы лучше, если бы они могли бы появиться каким то магическим образом в той же области видимости, что и сам тест. Также было бы немного понятнее, если бы установщик, мог бы быть установленным на стеке с помощью макроса BOOST_AUTO_UNIT_TEST вместе необходимости явно помещать его на стек для каждого теста.

#include

#include

#include "MyTestClass.h"

struct MyFixture

{

MyFixture()

{

someValue = 2.0;

str = "Hello";

}

float someValue;

std::string str;

MyTestClass myObject;

};

BOOST_AUTO_UNIT_TEST (TestCase1)

{

MyFixture f;

BOOST_CHECK_CLOSE (f.someValue, 2.0f, 0.005f);

f.someValue = 13;

}

BOOST_AUTO_UNIT_TEST (TestCase2)

{

MyFixture f;

BOOST_CHECK_EQUAL (f.str, std::string("Hello"));

BOOST_CHECK_CLOSE (f.someValue, 2.0f, 0.005f);

// Boost deals with this OK and reports the problem

//f.myObject.UseBadPointer();

// Same with this

//myObject.DivideByZero();

}

BOOST_AUTO_UNIT_TEST (TestCase3)

{

MyFixture f;

BOOST_CHECK_EQUAL (1, f.myObject.s_currentInstances);

BOOST_CHECK_EQUAL (3, f.myObject.s_instancesCreated);

BOOST_CHECK_EQUAL (1, f.myObject.s_maxSimultaneousInstances);

}

  • Хорошая обработка ошибок и крешей. Это один аспект, где Boost.Test на голову перепрыгнул всех своих конкурентов. Он не только обрабатывает исключения корректно, но и печатает некоторую информацию о них, он перехватывает системные исключения Linux, и даже имеет аргументы командной строки, которые запрещают обработку исключений, позволяя таким образом решать данные проблемы в вашем отладчике при повторном запуске. В этом плане, мне действительно здесь больше нечего и добавить.
  • Хорошая функциональность ASSERT. Да. В фреймворке имеются подобные выражения для практически любых операций, которые только могут понадобиться (равенство, приближение, меньше чем, больше чем, битовое равенство и т.д.). Кроме того, имеется поддержка проверок, было ли выброшено исключение. Выражения ASSERT корректно выводят содержимое проверяемых переменных. Соответственно, здесь можно поставить наивысшую оценку.
  • Поддержка различных методов вывода информации. Вероятно, но не так-то просто это реализовать. Как минимум, вывод по умолчанию достаточно комфортен. Я подозреваю, что мне нужно было поглубже поковыряться в unit_test_log_formatter, но я, разумеется, не видел разнообразие возможных вариантов вывода информации, которые можно было бы встроить сюда.
  • Поддержка TestSuit'ов. Да, но с большой уловкой. Если я ничего не упустил (что вполне возможно, если так и есть, то дайте знать), создание TestSuit требует приличного количества дополнительного кода и также требует модификации исполнителей (runner) в main. Взгляните на пример. Не могло бы ли это быть попроще? Не то, что бы это было большой проблемой - это требование для меня наименее важное, но мне бы хотелось помечать все тесты в одном файле, как часть TestSuit простым макросом в начале файла. Другая неприятная мелочь это отсутствие шагов установки/удаления для всего TestSuit, что могло бы действительно быть полезным.

#include <boost/test/unit_test.hpp>

#include <boost/test/floating_point_comparison.hpp>

using boost::unit_test::test_suite;

struct MyTest

{

void TestCase1()

{

float fnum = 2.00001f;

BOOST_CHECK_CLOSE(fnum, 2.f, 1e-3);

}

void TestCase2()

{}

};

test_suite * GetSuite1()

{

test_suite * suite = BOOST_TEST_SUITE("my_test_suite");

boost::shared_ptr instance( new MyTest() );

suite->add (BOOST_CLASS_TEST_CASE( &MyTest::TestCase1, instance ));

suite->add (BOOST_CLASS_TEST_CASE( &MyTest::TestCase2, instance ));

return suite;

}

#include

using boost::unit_test::test_suite;

extern test_suite * GetSuite1();

boost::unit_test::test_suite* init_unit_test_suite( int /* argc */, char* /* argv */ [] )

{

test_suite * test = BOOST_TEST_SUITE("Master test suite");

test->add( boost::unit_test::ut_detail::auto_unit_test_suite() );

test->add(GetSuite1());

return test;

}

Boost.Test это библиотека с огромным потенциалом. Она имеет огромную поддержку обработки исключений и расширенный набор инструкции ASSERT. Также можно отметить наличие уникальной функциональности по проверке на зацикливание, и различные уровни логгирования. С другой стороны, использование библиотеки очень многословно в случае добавления нового теста в качестве части TestSuit, и это может быть сдерживающим фактором для консольного игрового окружения.

CppUnitLite

У CppUnitLite интересная история возникновения. Михаель Феверс (Michael Feathers), автор CppUnit был несколько расстроен сложностью CppUnit и тем что фреймворк не удовлетворяет все нужды пользователей, и поэтому решил создать ультра легкий фреймворк CppUnitLite. Легкий, как в плане возможностей, так и в плане сложности и размера, но его философия заключалась в том, чтобы позволить пользователем конфигурировать его в соответствии с их потребностями.

В самом деле, CppUnitLite представляет собой только парочку файлов и, возможно, добавляет только 200 строк очень понятного, легкого для понимания и модификации кода. Если быть до конца честным, в этом сравнении в действительности я использовал версию CppUnitLite, которую я доработал несколько лет назад (можете загрузить ее со всеми примерами) с целью добавить функциональность, которая мне была необходима (установщики, обработка исключений, различные методы вывода информации). Я реализовал недостающую функциональность в как раз в таком духе, как это предполагала CppUnitLite, и если чего-то нету, вы можете разобраться, как добавить что-то новенькое буквально за пару минут.

С другой стороны, у CppUnitLite нету никакой документации. Черт возьми, у фреймворка даже нету сайта, что, я уверен, является серьезной проблемой его распространения.

  • Минимальное количество работы, требуемое для создания нового теста. Абсолютно! Из всех рассмотренных фреймворков, CppUnitLite наиболее близко подошел к уровню идеального в этом аспекте. С другой стороны, необходимо учесть факт, что я больше всего использовал для своих целей CppUnitLite и это оказало влияние на субъективизм моей оценки. Так или иначе, фреймворк реализует мою идею о минимально требуемых усилиях для создания простого теста и даже с возможностью использования установщика (хотя все равно, я уверен, что можно сделать лучше).

#include "lib/TestHarness.h"

TEST (Whatever,MyTest)

{

float fnum = 2.00001f;

CHECK_DOUBLES_EQUAL (fnum, 2.0f);

}

  • Простота в модификации и портировании. Без сомнений. Опять таки, Высшая оценка в данной категории. Другие фреймворки не могут соревноваться в этом с CppUnitLite по своей простоте в легкости модификации и портировании, и в то же время отвечающие требованиям разделении функциональности. Оригинальная версия CppUnitLite имеет в своем распоряжении простой и легкий класс для представления строк, избавляя таким образом от зависимости от STL. В моей модифицированной версии, я добавил поддержку std::string, так как я использую его в большинстве своих проектов, но это изменение требует не больше минуты. Также, использование в Linux абсолютно тривиально, несмотря на то, что я ранее использовал фреймворк только под Windows.
  • Поддержка установщиков. Это как раз то место, где у оригинального CppUnitLite есть проблемы. Фреймворк настолько легковесный, что он не поддерживает множество особенностей. Данная функциональность критична для меня, и я, таким образом, добавил ее.

#include "lib/TestHarness.h"

#include "MyTestClass.h"

class MyFixtureSetup : public TestSetup

{

public:

void setup()

{

someValue = 2.0;

str = "Hello";

}

void teardown()

{}

protected:

float someValue;

std::string str;

MyTestClass myObject;

};

TESTWITHSETUP (MyFixture,Test1)

{

CHECK_DOUBLES_EQUAL (someValue, 2.0f);

someValue = 0;

// CppUnitLite doesn't handle system exceptions very well either

//myObject.UseBadPointer();

// A regular exception works nicely though myObject.ThrowException();

}

TESTWITHSETUP (MyFixture,Test2)

{

CHECK_DOUBLES_EQUAL (someValue, 2.0f);

CHECK_STRINGS_EQUAL (str, std::string("Hello"));

}

TESTWITHSETUP (MyFixture,Test3)

{

// Unfortunately, it looks like the framework creates 3 instances of MyTestClass

// right at the beginning instead of doing it on demand for each test. We would

// have to do it dynamically in the setup/teardown steps ourselves.

CHECK_LONGS_EQUAL (1, myObject.s_currentInstances);

CHECK_LONGS_EQUAL (3, myObject.s_instancesCreated);

CHECK_LONGS_EQUAL (1, myObject.s_maxSimultaneousInstances);

}

  • Хорошая обработка ошибок и крешей. Оригинальная версия CppUnitLite не обрабатывает их вовсе. Я добавил минимальную поддержку (только опциональные try|catch). Запуск тестов без поддержки исключений требует перекомпиляции тестов с включенной специальной директивой define, таким образом, это не так удобно, как использование аргументов командной строки, подобной той, которая реализована в Boost.Test.
  • Хорошая функциональность ASSERT. Это то место, где CppUnitLite показывает все свои недостатки. Макрос Assert самый плохой из всех рассмотренных фреймворков. Он не использует поток для вывода содержимого своих переменных, следовательно, необходим самодельный макрос для каждого типа объекта, который вы хотите использовать. Существует поддержка для double, long и строк, но все остальное вы должны реализовывать сами. Также, здесь нету никаких проверок, кроме как на равенство (или на сходимость, в случае чисел с плавающей запятой).
  • Поддержка различных методов вывода информации. Опять таки, имеется только один метод вывода, Но он хорошо изолирован, и достаточно просто добавить прочие методы.
  • Поддержка TestSuit'ов. Пожалуй, единственный фреймворк, который не поддерживает TestSuit. Мне они никогда не были нужны, но иногда, думаю, были бы весьма кстати.

CppUnitLite это базовая система, но с помощью небольших модификаций она способна приобрести свой вес в каждой из категорий. Если бы была поддержка для выражений Assert, она была бы намного ближе к моему идеальному фреймворку. А так, это лишь интересный кандидат на финальную корону.

NanoCppUnit

Я никогда не слышал ничего о NanoCppUnit пока Phlip не принес ее. Просматривая список возможностей, фреймворк казался содержит все что мне было нужно в CppUnitLite, к тому же не требовал никаких доработок и был готов к использованию.

Первый минус NanoCppUnit это ужасная поставка фреймворка. Если вы думаете, что у CppUnitLite дела плохи (как например отсутствие собственного сайта), то вы не совсем правы. По крайней мере, вы можете скачать zip-архив. В случае с NanoCppUnit вы должны вручную загрузить пять файлов по отдельности и поместить их в одну директорию. Я не шучу. Прям таки неимоверная забота о пользователях. Документация, найденая на сайте, скажем, не самая лучшая.

Так или иначе, я продолжил свои поиски простой программы с тестами и взялся за NanoCppUnit. Дистрибутив (согласно информации с веб-сайта) предназначен для использования только на Windows. Я решил что проблему будет просто исправить, но как оказалось изменения требуют, гораздо больше времени, чем я думал сначала (я прекратил это занятие когда получил ошибки использования макросов, где-то в недрах assert). В отличие от CppUnitLite, исходный код не так хорошо структурирован, полон повсюду ужасных макросов, делая совсем нетривиальным добавление новых возможностей, как, скажем, другие методы вывода информации. Пока я окончательно не закопался в ошибках, выглядело даже, что в фреймворке имеются примеры. Но в конце концов, я бросил затею портировать фреймворк по Linux, таким образм мои комментарии здесь базируются на догадках, которые у меня появились в процессе изучения исходного кода фреймворка.

  • Минимальное количество работы, требуемое для создания нового теста. Я думаю да. Я не уверен, что можно создать независимый тест, который является частью TestSuite, но как минимум создание TestSuite не требует ручной регистрации каждого теста. Это (возможно) самый простой из возможных тестов на NanoCppUnit.

struct MySuite: TestCase { };

TEST_(MySuite, MyTest)

{

float fnum = 2.00001f;

CPPUNIT_ASSERT_DOUBLES_EQUAL(fnum, 2.0f, 0.0001);

}

  • Простота в модификации и портировании. Не совсем. Зависимости Windows намного глубже, чем могло бы показаться вначале. Размер исходного кода – небольшой, но он достаточно запутанный, что заставляет чувствовать себя несчастным, работая с ним. Я уверен, что его можно портировать, нужно только приложить достаточно усилий, все же он не настолько большой.
  • Поддержка установщиков. Да, Установка и удаление осуществляются очень похожим образом, как это сделано в CppUnitLite.
  • Хорошая обработка ошибок и крешей. Понятия не имею, поскольку мне не удалось его запустить. В исходном коде встречаются консрукции try/catch, но непонятно, как и включить или выключить. Думаю, здесь ситуация не лучше, чем в CppUnitLite.
  • Поддержка различных методов вывода информации. Не совсем. Везде жестко забито использование потока, который отсылает сожержимое в OutputDebugString() в Windows. Я думаю, вывод текста по-умолчанию, имеет формат ошибок Visual Studio.
  • Хорошая функциональность ASSERT. Да. Хороший перечень конструкций Assert, включая приближение для чисел с плавающей запятой, больше чем, меньше чем и т.д.
  • Поддержка TestSuit'ов. Да. Хотя я не знаю, как это работает. Короче, не очень важно.
Одна из уникальных особенностей NanoCppUnit – это поддержка регулярных выражений, как части выражений Assert. Это очень необычно, но я могу себе представить, насколько это может быть удобным. Некоторое время назад, мне нужно было проверить определенную часть кода на соответствие некоторому формату, поэтому мне пришлось использовать sscanf а потом, проверять содержимое. С регулярными выражениями, там было бы все более элегантно. К сожалению, NanoCppUnit не дотягивает до уровня других фреймворков. Прямо сейчас, складывается такое ощущение, что над фреймворком кипит работа, доделывается много функциональности и еще не до конца структурирован исходный код.

Unit++

Продвигаясь дальше, сейчас мы рассмотрим фреймворк, который меньше всего похож на Xunit. Уникальность особенностей Unit++ заключается в том, что он больше соответствует C++ чем, CppUnit. Минутку, я правильно услышал? Больше соответствует C++? И это преподносится как достоинство? Возвращаясь назад, к моему идеальному фреймворку, можно отметить, что он вообще не имеет чего, что связывало бы его с С++. Однажды я начал размышлять об этом, и пришел к выводу, что нету вовсе никаких оснований полагать, что фреймворки для тестирования должны быть написаны на С++. Тесты которые вы пишете требуют того же языка, на котором написан код, который вы тестируете, но весь «оберточный» код – нет.
Эта точка зрения повлияла на мою оценку данного фреймворка.

Итак, так что же это значит – больше соответствует С++? Для начала – отсутствие макросов. Вы создаете TestSuite путем создания класса, отнаследованного от TestSuite. Это в принципе тот же самый подход, который реализован в большинстве предыдущих фреймворков, но только там это было скрыто.В действительности, это мне никак не помогает узнать, что же я делаю, и я бы не назвал это особенностью. Как результат, тесты более многословны, чем они могли бы быть.

Документация на среднем уровне. Она есть, но там нету детального описания и не перегружена исчерпывающими примерами.

  • Минимальное количество работы, требуемое для создания нового теста. Я боюсь по этому вопросу, оценка фреймворка будет просто удручающей. Фреймворк требует ручной регистрации тестов, и каждый тест должен быть частью TestSuite. Это делает создание нового теста нудным и провоцирует на появление ошибок (к примеру написать тест, но забыть его зарегистрировать). Я не знаю как вам, но для меня такой код не является локаничным и понятным, пока я не затрачу на его изучение некторое время. Как-то не очень оптимистично.

#define __UNITPP #include "unit++.h"

using namespace unitpp;

namespace

{

class Test : public suite

{

void test1()

{

float fnum = 2.00001f;

assert_eq("Checking floats", fnum, 2.0f);

}

public:

Test() : suite("MySuite")

{

add("test1", testcase(this, "Simplest test", &Test::test1));

suite::main().add("demo", this);

}

};

Test* theTest = new Test();

}

  • Простота в модификации и портировании. Средненько. Требуется STL и всякие соответствующие инструменты, как, скажем, iostream (который, к слову, имеет особые проблемы, при работае с STLPort). С другой стороны исходный код небольшой и самодостаточный, таким образом, возможность модификации и портирования существует, было бы время.
  • Поддержка установщиков. Еще один фреймворк, в котором я не увидел, как работать с установщиками. Как и в Boost.Test кажется, что использование конструкторов и деструкторов это все что вам нужно. Быстрый поиск в документации по поводу использования установщиков, их установки и удаления успехом не увенчались. Я не знаю, может я что-то упустил, или просто другие разработчики пишут код совсем по-другому. Я думаю, я мог бы создать класс, для каждого установщика, какого только захотел, поместил бы процедуру установки в конструктор а удаления в деструктор, и наследовал бы каждый новый клас от этого базового (и как то разобрался бы с тем, как создавать экземпляр этого класса и испольовать его для тестирования). Наверняка это возможно, но не совсем тривиально, так ведь? Опять таки, отсутствие установщиков не идет на пользу фреймворку.
  • Хорошая обработка ошибок и крешей. Средне. Фреймворк перехватывает регулярные исключения без крешей, но что с того. Нет поддержки системных исключений Linux. Нету возможности их отключить для отладки.
  • Поддержка различных методов вывода информации. Я не понял, как это реализовать, согласно документации. Возможно, есть способ с использованием поддерживаемой функциональности UI, но это неочевидно (и нету соответствующих примеров). Кроме того, имея в наличии уже проблемы по пунктам 1 и 3, у меня уже не было достаточно мотивации продолжать изучать фреймворк. Кстати, это один из нескольких фреймворков, у которых вывод по умолчанию происходит некорректно для таких IDE как Kdevelop.
  • Хорошая функциональность ASSERT. В фреймворке присутствует минимум возможностей по данному вопросу. Он предоставляет проверки на равенство и проверки по условиям, но это все. Даже нету возможности указать приближение для чисел с плавающей запятой. Хотя бы содержимое переменных он пишет правильно.
  • Поддержка TestSuit'ов. Да, как и большинство конкурентов.

Таким образом, Unit++ не является достойным вариантом. Отчасти, это потому, что у него отсутствуют такие особенности, которые интересуют меня, но он не предлагает ничего нового, чего нету в других фреймворках и у него куча собственных проблем. А то, что нету установщиков, я простить не могу.

CxxTest

После рассмотрения фреймворка, отличного от Xunit (Unit++) я без энтузиазма приступил к изучению самого эксцентричного из них всех, CxxTest. Я никогда до этого не слшал об этом фреймворке, но я знал, что он требует использования перла, для генерации некоторого кода на C++. Внутри меня что-то заскреблось.

Товарищи, я был неправ! Буквально через несколько минут использования CxxTest и беглого просмотра документации (пожалуй, одной из лучших), я точно осознал – это то что мне нужно. Это было для меня неожиданным сюрпризом, так как я уже был готов смириться с ситуацией и поделить победу между CppUnit и CppUnitLite.

Давайте начнем с самого начала. В чем заключается использование Perl и в чем отличие фреймворка от CppUnit? Ерез Волк (Erez Volk), автор CxxTest, обладал уникальной проницательностью, так как мы тестируем C++ программу, но мы не обязаны полагаться на C++ для всего на свете. Другие языки, такие как Java, лучше справляются с тем, что нам необходимо в плане юнит-тестирования, потому что там есть такое поянтие как reflection. С++ проигрывает в этом плане, поэтому мы должны идти на всевозможные трюки в C++, такие как ручная регистрация тестов, ужасные макросы и т.д. CxxTest обходит эти проблемы путем парсинга нашего простого теста и генерации кода, который вызывается в наших тестах. Результат просто превосходный. У нас есть вся гибкость, которая только нужна, при этом нет нужды использовать уродливые макросы, экзотические библиотеки или необычные особенности языка. По сути, требования CxxTest достаточно просты (ну понятно, что должна присутствовать возможность выполнять скрипты Perl).

Этап кодогенерации также просто интегрировать в регулярную build-систему. Замечательная документация описывает в деталях последовательность требуемых дейсвтий, требуемых для интеграции с make-файлами, проектными файлами Visual Studio или Cons. Один раз это проделав, вам больше не понадобится возвращатся к данному этапу еще раз.

Посмотрим, как обстоят дела в плане интересующих нас вопросов.

  • Минимальное количество работы, требуемое для создания нового теста. Очень хорошо. Это практически самый лучший результат. Если бы я придирался к мелочам, я бы сказал, что было бы еще лучше, если бы не было необходимости объявлять класс явно. Поскольку мы используем скрипт Perl, то нет никаких проблем исправить и это и еще больше приблизиться с моему идеальному фреймворку.

class SimplestTestSuite : public CxxTest::TestSuite

{

public: void testMyTest()

{

float fnum = 2.00001f;

TS_ASSERT_DELTA (fnum, 2.0f, 0.0001f);

}

};

  • Простота в модификации и портировании. CxxUnit требует самый простой набор возможностей языка (нету обработки исключений, нету шаблонных функций, и т.д.). Также не требуется никакой сторонней библиотеки. Фреймворк поставляется, как набор заголовочных файлов, таким образом, нет необходимости собирать отдельную библиотеку из исходников, или что-то вроде того. Функциональность хорошо разбита и разделена в оригинальном исходном коде, таким образом сделать модификацию также не составит труда.
  • Поддержка установщиков. CxxUnit получает наивысшую оценку в данной категории. Он не только поддерживает шаги установку/удаление на уровне отдельных тестов, но также поддерживает TestSuite на мировом (глобальном) уровне. Создание установщиков очевидно и требует лишь наследования от класса, и создания ровно столько функций, сколько вам нужно для данного теста. Что бы быть до конца последовательным, я бы влюбился в фреймворк, если бы это было сделано на предыдущем шаге, не говоря об упрощении кода, добавив еще установку/удаление в процессе генерации для каждого теста. Это позволило бы нам работать с объектами непосредственно на стеке, что позволило бы контролировать их время жизни корректно в каждом тесте. Да ладно, Нельзя иметь абсолютно все.

#include "MyTestClass.h"

class FixtureSuite : public CxxTest::TestSuite

{

public:

void setUp()

{

someValue = 2.0;

str = "Hello";

}

void tearDown() {}

void test1()

{

TS_ASSERT_DELTA (someValue, 2.0f, 0.0001f);

someValue = 13.0f;

// A regular exception works nicely though myObject.ThrowException();

}

void test2()

{

TS_ASSERT_DELTA (someValue, 2.0f, 0.0001f);

TS_ASSERT_EQUALS (str, std::string("Hello"));

}

void test3()

{

//myObject.UseBadPointer();

TS_ASSERT_EQUALS (1, myObject.s_currentInstances);

TS_ASSERT_EQUALS (3, myObject.s_instancesCreated);

TS_ASSERT_EQUALS (1, myObject.s_maxSimultaneousInstances);

}

float someValue;

std::string str;

MyTestClass myObject;

};

  • Хорошая обработка ошибок и крешей. Великолепная поддержка. Фреймворк отлавливает все исключения и выводит информацию о них, в таком формате, как и прочие ошибки (хотя поддержки системных ошибок Linux и здесь нету). Вы можете просто перезапустить тест с аргументом командной строки для скрипта Perl, что бы избежать перехватываение исключений и отлавливать их самостоятельно в отладчике. Фреймворк также дает вам возможность конфигурации макроса assert, позволяя вам при необходимости отлавливать исключения самостоятельно.
  • Поддержка различных методов вывода информации. Поддержка различных методов вывода осуществляется путем передачи параметра коммандной строки, который определяет, какой тип вывода использовать. Формат вывод по-умолчанию (error-printer) корректно обрабатывается IDE, и вы можете использовать некоторые другие (включая вывод с GUI, у которого есть индикатор прогресса, поддержка отчетов, или формат в стиле stdio). Добавление нового типа вывода также предусмотрен, что отражено в документации.
  • Хорошая функциональность ASSERT. Опять таки, наивысшая оценка. Здесь в наличие имеется полный набор assert функций, включающий обработку исключений, проверку утверждений и произвольные связи. Также имеется возможность выводить предупреждения, которые могут использоваться для определения разных частей кода, вызывающего один и тот же тест, или вывод TODO сообщений.
  • Поддержка TestSuit'ов. Да. Все тесты являются частью TestSuite.

Еще одной особенностью CxxUnit, с которой я не успел разобраться, это поддержка Mock объектов. Разработчики, использующие TDD знакомы со значением mock объектов, когда возникает необходимость тестировать взаимодействие между набором объектов. Несомненно, CxxUnit позволяет перегружать глобальные функции специфичными mock-функциями (предоставлен пример перегрузки fopen()). Я не думаю, что это полезно при работе с регулярными классами, для которых вы предоставлены сами себе.

А что же сделано не очень хорошо в CxxUnit? Немногое. К примеру, есть желание, что бы синтаксис тестов был более компактным, это касается больших проектов. Если вы будете следовать примерам в документации, то можете создать одиночный исполнитель (runner) для всех ваших тестов. Это может стать проблемой, если у вас сотни тестов, и вы сделаете небольшое изменение, это потребует перекомпиляции всего вашего кода.

После того как я пообщался с Ерезом и перепроверил документацию, я обнаружил, что есть полная поддержка этого в CxxUnit. По умолчанию, когда вы генерируете исполнитель тестов, он добавляет функцию main и несколько глобальных переменных, таким образом, линковка нескольких подобных исполнителей сопряжена с проблемами. Однако, это можно обойти, если вы будете генерировать исполнители с аргументом коммандной строки –part, это позволит не создавать main и глобавльные переменные для них. После этого вы можете слинковать их всех вместе и получить один модуль. Я бы хотел узнать, имеет ли смысл создание исполнителей для каждого TestSuite, или же было бы лучше объеденить их вместе. Стоит исследовать этот момент в будущем.

Выводы

После рассмотрения всех шести фреймворков, осталось четыре потенциальные кандидата: CppUnit, Boost.Test, модифицированный CppUnitLite и CxxTest.

Из всех четырех, мне больше всего понравился CxxTest. Он очень близко подобрался к требованиям моего идеального фреймворка, используя мощь внешнего скриптового языка. Он очень удобен в использовании и обеспечивает изящные дополнительные возможности и отличную функциональность assert'ов. Он требует использование скриптового языка, как часть системы построения проекта, таким образом, этот потенциальный недостаток может способствовать предпочтению оставшихся трех фреймворков.

CppUnit отличный, завершенный фреймворк. Он прошел длительный путь в своем развитии. Главный его недостаток это излишняя многословность при добавлении новых тестов и установщиков, а также зависимость от STL и некоторых других особенностей языка.

Если вам необходима абсолютная простота, вы правильно поступите, если будете использовать CppUnitLite (или модифицированную версию), доработав ее в соответствии с вашими требованиями. Он хорошо структурирован, ультра-компактный, без сторонних зависимостей, таким образом, модификация будет совсем несложной. Главный минус – это минимум возможностей и примитивная функциональность assert.

Если вы собираетесь работать в основном на PC, не планируете делать каких либо модификаций фреймворка и не боитесь зависимостей от библиотек Boost, Boost.Test может оказаться самым удачным выбором.

Нужно ли создавать собственный фреймворк для юнит-тестирования? Я знаю, что Кент Бек рекомендует это в своей книге Test-Driven Development: By Example, и это может послужить хорошим опытом, но я не стану вам это рекомендовать. Возможно, может и есть смысл написать связанный список и организовать стек за короткое время, но я не буду рекомендовать создавать такое в промышленном коде, вместо использования STL. Я настоятельно рекомендую начать использовать один из трех фреймворков для тестирования, которые были упомянуты выше. Если вы готовы немного потрудиться, берите CppUnitLite.

Несмотря на то, что вы выберете, мы можете быть спокойны, используя один из этих трех фреймворков. Самая главная вещь, это то, что вы пишете юнит-тесты, или даже лучше, используете Test-Driven Development. Повторяя слова, Майкла Фивера, код без юнит тестов это legaсy код, вы же не хотите писать legaсy код?