Elegir tecnologías para mi nuevo proyeco C++

Estoy empezando un pequeño proyecto en C++ y antes de nada me han surgido un par de preguntas:

  1. ¿Cómo voy a construirlo?
  2. ¿Qué framework para pruebas unitarias utilizar?

Elegir un Sistema de Construcción (Meson)

Ya he utilizado antes Make, Maven, Scons, Gradle y Autotools, pero tengo algunas razones para probar algo diferente, hay algunas cosas que no me gustan:

Autotools
No es fácil de configurar y mantener: hay distintos ficheros de configuración y distintos pasos de configuración.
Gradle
La construcción de proyectos C++ está todavía en desarrollo, los modelos y APIs están cambiando. No es muy rápido. Puedes ver un ejemplo en este artículo Construir un proyecto C++ con Gradle.
Make
A medida que el proyecto crece los archivos de configuración se van complicando y volviendo poco manejables. La sintáxis no me parece clara (esto es una custión de gustos).
Scons
Es más lento y un poco más difícil de comprender que Meson.
Maven
Es lento y puedes terminar "Javatizando" la estructura del proyecto.

Nota

He nombrado solo las cosas que no gustan, pero estos sistemas de construcción tienen otras grandes virtudes, personalmente me encantan Gradle, Autotools y Scons.

CMake vs Meson

Después de descartar los anteriores, estoy considerando Meson y CMake.

CMake
Tiene una gran ventaja sobre Meson, es mucho más maduro y es mucho más usado, lo que significa que podrás encontrar muy fácilmente ejemplos, documentación y ayuda en Internet. No importa el tipo de proyecto que estés empezando, lo más seguro es que CMake sea una buena elección.
Meson
Es un proyecto jóven comparado con CMake, pero está creciendo rápido y ya ha sido adoptado por algunos proyectos importantes como Gnome, donde han comenzado una iniciativa para migrar desde Autotools a Meson.

Finalmente he elegido Meson porque:

  • La sintáxis es muy clara para mí, cuando leo un archivo meson.build entiendo rápidamente lo ue está pasando durante el proceso de construcción.
  • Es rápido, aunque está escrito en Python utiliza Ninja para construir el proyecto. La primera vez tienes que utilizar Meson para configurar el proyecto, pero para construir y probar el proyecto relmente estamos ejecutando Ninja.
$ meson build . # Primera vez, configuración del proyecto
$ cd build
$ ninja build   # cada vez que construyes el projecto
$ ninja test    # cada vez que ejecutas tests

He encontrado un para de comparaciones interesantes entre algunos de los sistemas de construcción en C++, aunque puede que no sean del todo imparciales porque han sido realizadas por Meson y Scons.

Framework the Pruebas Unitarias

Anteriorment he utilizado algunas librerías del tipo xUnit como UnitTest++, CppUTest o Google Test que encaja perfectamente con Google Mock.

Si quires una apuesta segura que cumpla tus expectativas, te recomiendo Google Test.

Pero hace algún tiempo encontré un framework de pruebas con algunas características no tan comunes en librerías de pruebas C++ y que resultaba realmente fácil de utilizar, estoy hablando de Catch:

  • Es simplemente un fichero de cabeceras C++ sin dependencias adicionales, por lo que resulta realmente rápido comenzar (wget y utilizar el fichero descargado desde tus pruebas).
  • Puedes utilizar el estilo normal de pruebas unitarias o el estilo BDD.

Si quieres saber más sobre Catch, te recomiendo que directamente lo pruebes, el siguiente ejemplo, es cuestión de dos minutos simple example up and running. Puedes también leer algunos artículos como Why do we need yet another C++ test framework? o Testing C++ With A New Catch.

doctest: Una alternativa a Catch

Hay otro framework de pruebas llamado doctest, con los mismos principios que Catch, pero promete ser más rápido y ligero (resultados de las comparaciones de rendimiento) que Catch.

doctest fue diseñado basándose en los puntos fuertes de Catch, pero hay algunas diferencias.

No es fácil decidirse por uno, los dos son muy parecidos, puedes comprobar las diferencias a continuación:

@@ -1,12 +1,12 @@
-#define CATCH_CONFIG_MAIN // It tells Catch to provide a main() - only do this in one cpp file
+#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN

-#include "catch.hpp"
+#include "doctest.h"
#include "Uuid.h"
#include <string>

constexpr int MAX_ITERS = 100;

-TEST_CASE("Uuid", "[uuid]")
+TEST_CASE("Uuid")
{
for (int i = 0; i < MAX_ITERS; i++)
{
@@ -26,7 +26,7 @@ TEST_CASE("Uuid", "[uuid]")

// BDD style

-SCENARIO("UUID creation", "[Uuid]")
+SCENARIO("UUID creation")
{

GIVEN("A random UUID ")

Finalmente he elegido doctest simplemente porque es más rápido: resultados de las comparaciones de rendimiento.

Nota

He creado el proyecto de ejemplo utilizando ambos frameworks, puedes encontrarlos en diferentes ramas del repositorio: rama doctest or rama catch.

Ejemplo

He creado un ejemplo para ilustrar este artículo: https://github.com/carlosvin/uuid-cpp.

Consiste en una implementación básica de un generador pseudo-aleatorio de UUID, está basado en mt19937 que no es criptográficamente seguro.

Artefactos del Proyecto

Cuando instalemos el proyecto, Meson (Ninja realmente) generará una serie de artefactos en nuestro sistema.

  • Librería compartida: libuuid.
  • Fichero de cabeceras para que los desarrolladores puedan usar la librería: include/Uuid.h.
  • Fichero ejecutable uuidgen (Generador de UUID).
  • Ejecutable de las pruebas unitarias (no será instalado).

Si ejecutamos ninja install en Linux obtendremos los siguientes ficheros:

/usr/local/lib/libuuid.so
/usr/local/include/Uuid.h
/usr/local/bin/uuidgen

Estructura del Proyecto (Fork project)

  • meson.build

    Fichero principal de configuración para construir el proyecto. Lo utilizamos para especificar las propiedades y subdirectorios del proyecto.

    project(
        'cpp-meson-example', # project name
        'cpp', # C++ project, e.g: for C project
        version : '1.0.0',
        license : 'MIT',
        default_options : ['cpp_std=c++11']) # compile for C++
    
    # it will be referred from subdir projects
    inc = include_directories('include')
    
    # meson will try to find a meson.build file inside following directories
    subdir('include')
    subdir('src')
    subdir('test')
    
  • include
    • meson.build

      Archivo de configuración para construir este directorio, no hay mucho que hacer aquí, simplemente indicamos qué ficheros de cabeceras han de ser instalados

      # Select header files to be installed
      install_headers('Uuid.h')
      
    • Uuid.h

      Archivos de cabeceras, es el interfaz que expone la librería y que será incluido por los usuarios de la misma.

      namespace ids {
      
      class Uuid {
          private:
          // ...
      
  • src
    • meson.build (src)

      Declara 2 artefactos de salida: La librería libuuid y el ejecutable uuidgen.

      libuuid = shared_library(
          'uuid', # library name
          'Uuid.cpp', # source files to be compile
          include_directories : inc, # previously declared include directories in root :code:`meson.build`
          install : true) # :code:`libuuid` will be part of project installation
      
      uuidgen = executable(
          'uuidgen', # executable name
          'main.cpp', # source files to compile
          include_directories : inc, # previously declared include directories in root :code:`meson.build`
          link_with : libuuid, # linking executable with shared previously declared shared library :code:`libuuid`
          install : true) # :code:`uuidgen` executable be part of project installation
      
    • main.cpp

      Código fuente del ejecutable de la aplicación: uuidgen

      #include "Uuid.h"
      #include <iostream>
      
      int main()
      {
          ids::Uuid uuid;
          std::cout << uuid.to_str() << std::endl;
          return 0;
      }
      
    • Uuid.cpp

      Implementación de la clase declarada en el fichero de cabeceras Uuid.h.

      #include "Uuid.h"
      
      Uuid::Uuid()
      { // ...
      
  • test
    • meson.build (test)

      Archivo de configuración para construir y ejecutar las pruebas unitarias.

      testexe = executable(
          'testexe', # test executable name
          'uuid_test.cpp', # tests source files to be compiled
          include_directories : inc,  # declared include directories in root :code:`meson.build`
          link_with : libuuid) # link test executable with previously declared shared library :code:`libuuid`
      
      # test execution
      test('Uuid test', testexe)
      
      # we can specify other test execution passing arguments or environment variables
      test('Uuid test with args and env', testexe, args : ['arg1', 'arg2'], env : ['FOO=bar'])
      
    • doctest.h

      Librería doctest en un único fichero de cabeceras. Puedes tratar de automatizar el proceso de instalación de la librería, yo por el momento la he instalado manualmente, ya que es un proceso muy sencillo:

      cd test
      wget https://raw.githubusercontent.com/onqtam/doctest/master/doctest/doctest.h
      
    • uuid_test.cpp

      Implementación de las pruebas unitarias.

      #define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
      
      #include "doctest.h"
      #include "Uuid.h"
      #include <string>
      
      constexpr int MAX_ITERS = 100;
      
      TEST_CASE("Uuid")
      {
          for (int i = 0; i < MAX_ITERS; i++)
          {
              ids::Uuid uuid;
              std::string uuid_str{uuid.to_str()};
      
              INFO(uuid_str);
      
              // If assertion fails test execution is stopped
              REQUIRE(uuid_str.size() == 36);
      
              // If assertion fails test execution continues
              CHECK(uuid.most > 0);
              CHECK(uuid.least > 0);
          }
      }
      
      // BDD style
      
      SCENARIO("UUID creation")
      {
      
          GIVEN("A random UUID ")
          {
      
              ids::Uuid uuid;
              std::string uuid_str{uuid.to_str()};
      
              REQUIRE(uuid_str.size() == 36);
      
              WHEN("get the most and least")
              {
                  THEN("should be more than 0")
                  {
                      CHECK(uuid.most > 0);
                      CHECK(uuid.least > 0);
                  }
              }
          }
      }
      

Sugerencia

Puedes encontrar las instrucciones para construir y ejecutar el proyecto de ejemplo en: https://github.com/carlosvin/uuid-cpp#how-to-build-the-example

Comentarios

Comments powered by Disqus