Elegir tecnologías para mi nuevo proyeco C++

Las razones por las que he elegido Meson+Doctest para crear un proyecto en C++. También contiene un ejemplo fácil de ejecutar y de reutilizar como template.

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?

Tip
Si simplemente quieres comenzar un proyecto en C++, fácil de construir, con una librería y pruebas unitarias listas, simplemente visita el repositorio del proyecto de ejemplo https://github.com/carlosvin/uuid-cpp y sigue las instrucciones en el README.md.

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 Cpp 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.

Note
He nombrado solo las cosas que no gustan, pero estos sistemas de construcción tienen otras grandes virtudes, personalmente me encantan Gradle, Autotools y Maven (solo para projectos Java).

CMake vs Meson

Después de descartar los anteriores, estoy considerando Meson y CMake. Los dos son bastante rápidos:

Aunque Meson está hecho en Python, simplemente genera projectos Ninja. La primera vez tenemos que ejecutar Meson para configurar el proyecto, el resto de ejecuciones para compilar o ejecutar pruebas, realmente estaremos ejecutando directamente Ninja.

CMake también puede generar proyectos Ninja entre otros formatos, mira la documentación "CMake generators".

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.

Pasos para compilar y ejectuar tests
$ meson build . (1)
$ cd build
$ ninja build   (2)
$ ninja test    (3)
  1. Primera vez, configuración del proyecto

  2. Cada vez que construyes el projecto

  3. Cada vez que ejecutas tests

Otras comparaciones entre sistemas de construcción

He encontrado un par 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 https://blog.coldflake.com/posts/Testing-C-with-a-new-Catch/[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:

Differencias entre la rama usando doctest y la rama usando Catch
@@ -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.

Note
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.

meson.build
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

include/meson.build
# 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.

include/Uuid.h
namespace ids {

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

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

src/meson.build
libuuid = shared_library(
    'uuid', (1)
    'Uuid.cpp', (2)
    include_directories : inc, (3)
    install : true) (4)

uuidgen = executable(
    'uuidgen', (5)
    'main.cpp', (6)
    include_directories : inc, (7)
    link_with : libuuid, (8)
    install : true) (9)
  1. library name

  2. source files to be compile

  3. previously declared include directories in root meson.build

  4. libuuid will be part of project installation

  5. executable name

  6. source files to compile

  7. previously declared include directories in root meson.build

  8. linking executable with shared previously declared shared library libuuid

  9. uuidgen executable be part of project installation

    main.cpp

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

src/main.cpp
#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.

src/Uuid.cpp
#include "Uuid.h"

Uuid::Uuid()
{ // ...
test
meson.build (test)

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

test/meson.build
testexe = executable(
    'testexe', (1)
    'uuid_test.cpp', (2)
    include_directories : inc, (3)
    link_with : libuuid) (4)

test('Uuid test', testexe) (5)

test('Uuid test with args and env', testexe, args : ['arg1', 'arg2'], env : ['FOO=bar']) (6)
  1. test executable name

  2. tests source files to be compiled

  3. declared include directories in root meson.build

  4. link test executable with previously declared shared library libuuid

  5. test execution

  6. we can specify other test execution passing arguments or environment variables

    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:

Añadir doctest al proyecto
cd test
wget https://raw.githubusercontent.com/onqtam/doctest/master/doctest/doctest.h
uuid_test.cpp

Implementación de las pruebas unitarias.

test/uuid_test.cpp
#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);
            }
        }
    }
}
Tip
Puedes encontrar las instrucciones para construir y ejecutar el proyecto de ejemplo en: https://github.com/carlosvin/uuid-cpp#how-to-build-the-example