Choosing a Modern C++ stack

I'm starting a new project in C++, but I've run into a pair of questions before start:

  1. Which build system should I use?
  2. Which unit testing framework?

Choosing Build System (Meson)

I have used before Make, Maven, Scons, Gradle and Autotools.

But I have some reasons to try find something else.

Autotools
It is not easy to configure and maintain. There are several configuration files and several configuration steps.
Gradle
C++ feature is still incubating. Not very fast. You can check a similar example project at Build C++ project with Gradle.
Make
I don't love the syntax. Files tends to get messy as project grows.
Scons
It is just slow.
Maven
It is slow and you might end up "Javatizing" your C++ project structure.

Note

I've listed just things I don't like, those projects have other great features.

Now I'm considering Meson or CMake.

CMake has a big advantage over Meson, it is mature and widely used in many projects, which means there are many examples and it will fulfill your C++ project building needs.

Meson is a young project compared with CMake, but it is growing quite fast and it has been adopted in other big projects like Gnome, they have an initiative to port from Autotools to Meson.

I've chosen Meson because:

  • Syntax is really clear to me, when I read meson.build file I can quickly understand what is happening during build process.
  • It is fast. Altought it is written in Python, it generates a Ninja build project. First time you configure the project you has to run Meson, but for building or testing you are actually running Ninja.
$ meson build . # first time you configure the project
$ cd build
$ ninja build   # each time you build it
$ ninja test    # each time you run tests

I've found two interesting comparisons about available C++ build systems, they might be a little be biased because those comparisons come from Meson and Scons.

Unit Testing Framework

I have used some xUnit based libraries like UnitTest++, CppUTest or Google Test which match perfectly with Google Mock. If you want a safe bet that fulfills almost of your testing needs I highly recommend Google Test.

But time ago I found a testing framework with some interesting features, Catch:

  • It is just a header file with no external dependencies, so very easy to start (wget + include downloaded file).
  • You can use normal unit test style or BDD-style

If you want to know more about Catch, I recommend you to give it a try, it is a matter of 2 minutes to have a simple example up and running. You can also read some interesting articles like Why do we need yet another C++ test framework? or Testing C++ With A New Catch.

doctest: A Catch alternative

There is another testing framework named doctest, with same benefits as Catch, but it promises to be faster and lighter (benchmark results) than Catch.

doctest is modeled after Catch and some parts of the code have been taken directly, but there are differences.

It hasn't been easy to decide, both are really similar, you can check here differences between project using doctest and project using Catch.

I've finally chosen doctest because it promises to be faster: benchmark results.

Note

I've created project using both frameworks you can find them in corresponding branches: doctest branch or catch branch.

Hint

You can see diferencies between projects at: https://github.com/carlosvin/uuid-cpp/pull/1

Example

I've created an example to illustrate this article: https://github.com/carlosvin/uuid-cpp.

It is a basic implementation of UUID pseudo-random generator based on mt19937 which is not cryptographically secure.

Project output artifacts

  • Shared library: libuuid.
  • Header library for developers who want to use the shared library: include/Uuid.h.
  • Executable uuidgen (UUID generator).
  • Test executable (not installed). It tests shared library.

For example, if you execute ninja install on Linux, you will get something like:

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

Project structure (Fork project)

  • meson.build

    Root project file configuration. It defines project properties and subdirectories.

    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

      Subdirectory build configuration file.

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

      Header file, it is the library interface definition which will be included from projects using that library

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

      It declares 2 output artifacts libuuid and 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

      Entry point for main executable uuidgen

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

      Implementation of declared class in header file.

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

      File to configure tests build process.

      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

      doctest library in a single header file. You can try to automate library installation as part of your build process, but I haven't figure out yet a way to do it with Meson. For now I've installed it manually:

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

      Tests implementation.

       // This tells doctest to provide a main() - only do this in one cpp file
      #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()};
      
              MESSAGE(uuid_str);
              CHECK(uuid.most > 0);
              CHECK(uuid.least > 0);
              CHECK(uuid_str.size() == 36);
          }
      }
      
      // BDD style
      
      SCENARIO( "UUID creation" ) {
      
          GIVEN( "A random UUID " ) {
              ids::Uuid uuid;
              std::string uuid_str {uuid.to_str()};
      
              CHECK(uuid_str.size() == 36);
      
              WHEN( "get the most and least" ) {
                  THEN( "should be more than 0" ) {
                      CHECK( uuid.most > 0);
                      CHECK( uuid.least > 0);
                  }
              }
          }
      }
      

Comments

Comments powered by Disqus