Scientific Computing

C++ std::string with char*

C++ std::string is a dynamic, contiguous container for character strings. String data is easily and efficiently passed between std::string to / from a C or Fortran function that expects a char* pointer.

The basic algorithm is:

  1. allocate std::string with desired size and fill with \0.
  2. use std::string::data() to get a char* pointer to the string data that is read/write for the C or Fortran function (or C++).
  3. use std::string::c_str() to get a const char* pointer to the string data that is read-only for the C or Fortran function (or C++). This trims the string to the first \0 character. Otherwise, the std::string::length() will include all the unwanted trailing \0 characters.

example

CMake install via Snap

The CMake Snap package allows easy install of the latest CMake version. Scroll down to the “Install CMake on your Linux distribution” section and click on the distribution closest to the computer being used to ensure Snap is setup correctly.

After CMake install, add to PATH in ~/.profile or similar like:

export PATH=/snap/cmake/current/bin/:$PATH

C++ and C warning preprocessor directive

C++ #warning and C #warning are preprocessor directives that emit a warning message during compilation.

These trivial examples assume that a warning should be omitted if symbol “MYFEATURE” is not defined.

#ifndef MYFEATURE
#if __cplusplus >= 202302L
#  warning "C++ compiler lacks feature"
#endif
#endif
#ifndef MYFEATURE
#if __STDC_VERSION__ >= 202311L
#  warning "C compiler lacks feature"
#endif
#endif

The #if strictly check that the compiler language support is at least the specified version. Most compilers have long-supported the #warning directive without the #if check needed. That is, the following is sufficient for most compilers:

#ifndef MYFEATURE
#warning "C++ compiler lacks feature"
#endif

Related: MVSC __cplusplus macro flag

C++ std::make_unique instead of new delete

C++ std::make_unique() is a C++14 feature that creates std::unique_ptr without using new and delete. std::make_unique() is a safer and more convenient way to manage memory in C++.

GCC 4.9 circa 2014 added support for std::make_unique(), along with virtually all modern C++ compilers for several years already.

__cpp_lib_make_unique feature test macro can fence non-essential code if supporting ancient compilers is required.

CTest TEST_LAUNCHER target property

CMake target property TEST_LAUNCHER allows specifying a test launcher program. For example, MPI programs can use mpiexec to run tests with parameters. This allows deduplicating or making more programmatic test runner scripts.

Typically we create a small CMake function to configure the test launcher for each target and the associated tests.

Example

Detect CI via environment variable

CI systems typically set the environment variable CI as a de facto standard for easy CI detection. Here are details of several popular CI services:

In general, across programming languages, test frameworks allow behavior changes based on the environment variables set by the CI system.

Python Pytest

Pytest handles conditional tests well. This allows skipping a subset of tests on CI by detecting the CI environment variable:

import os
import pytest

CI = os.environ.get('CI') in ('True', 'true')


@pytest.mark.skipif(CI, reason="not a test for CI")
def test_myfun():
    ...

CMake CTest

CTest can also use CI environment variables to adjust test behavior.

if("$ENV{CI}")
  set(NO_GFX on)
endif()

add_test(NAME menu COMMAND menu)
set_tests_properties(menu PROPERTIES DISABLED $<BOOL:${NO_GFX}>)

Enclose "$ENV{}" in quotes in case the environment variable is empty or not defined, or the configuration step may syntax error.

Fortran logical boolean C_BOOL

Fortran compilers typically use 4 bytes for logical while C compilers usually use 1 byte for bool.

oneAPI flag -fpscomp logicals ensures integer values 0,1 corresponding to .false., .true. are used. Otherwise by default unexpected values may cause programs to fail at runtime with impossible boolean values like 255.

In CMake this flag is applied like:

if(CMAKE_Fortran_COMPILER_ID MATCHES "^Intel")
  add_compile_options("$<$<COMPILE_LANGUAGE:Fortran>:-fpscomp;logicals>")
elseif(CMAKE_Fortran_COMPILER_ID STREQUAL "NVHPC")
  add_compile_options("$<$<COMPILE_LANGUAGE:Fortran>:-Munixlogical>")
endif()

For C interoperability, Fortran can use:

use, intrinsic :: iso_c_binding

logical(kind=C_BOOL) :: L
logical :: Q

c_sizeof(L) == 1
c_sizeof(Q) == 4
! typically

while C uses:

#include <stdbool.h>
#include <stdio.h>

int main(void) {
bool L;
printf("%zu\n", sizeof(L));
}

and likewise C++ bool is typically 1 byte:

#include <iostream>

int main(){
  bool L;
  std::cout << sizeof(L) << "\n";
}

Always use iso_c_binding when using C or C++ with Fortran modules to produce cross-platform compatible projects.

See “bool” examples for interfacing between C, C++ and Fortran.

Do not use

The Intel oneAPI compiler -standard-semantics flag to use C_BOOL values correctly between C, C++ and Fortran has a BIG DOWNSIDE in that it breaks linking with system libraries–including IntelMPI! All projects and libraries linking to a Fortran library that used oneAPI -standard-semantics must also use -standard-semantics.

C++ / C __has_include macro

GCC 5 enabled the __has_include macro that checks if a source file exists. __has_include() was made language standard syntax in C++17 and C23. __has_include("file.h") checks if “file.h” exists as a file on the compiler includes paths, but not if “file.h” can be included with the specified compiler language standard.

Watch out for buggy __has_include() in GCC < 10. We found that __has_include("linux/magic.h") with quotes "" instead of the usual carets __has_include(<linux/magic.h>) was needed specifically for header linux/magic.h. Other systems libraries with carets worked as expected even with GCC < 10.

#if __has_include("linux/magic.h")  // quotes needed for GCC < 10
#  include <linux/magic.h>
#endif

Examples

Compile and run the example number.cpp code.

% c++ -std=c++20 numbers.cpp

% ./a.out

3.14159

numbers.cpp:

#if __has_include(<numbers>)
#  define HAVE_NUMBERS
#  include <numbers>
#endif

#include <iostream>
#include <cstdlib>

int main(){
    // numbers example:
#ifdef HAVE_NUMBERS
    std::cout << std::numbers::pi << std::endl;
#else
    std::cout << "std::numbers::pi not available" << std::endl;
#endif
    return EXIT_SUCCESS;
}

CMake default install prefix in subprojects

CMake FetchContent is useful to incorporate subprojects at configure time. FetchContent subproject cache variables can override the top-level project cache, which can be confusing.

Projects may wish to use a default install prefix when cmake –install-prefix is not specified. Environment variable CMAKE_INSTALL_PREFIX can set a default install prefix across projects.

Some projects force a default install prefix if not specified by the top level project:

if(PROJECT_IS_TOP_LEVEL AND CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
  set_property(CACHE CMAKE_INSTALL_PREFIX PROPERTY VALUE ${CMAKE_BINARY_DIR}/local)
endif()

However, it is more a canonical approach to check the install prefix write permissions.

message(STATUS "CMAKE_INSTALL_PREFIX: ${CMAKE_INSTALL_PREFIX}")
file(MAKE_DIRECTORY ${CMAKE_INSTALL_PREFIX})
if(CMAKE_VERSION VERSION_GREATER_EQUAL 3.29)
  if(NOT IS_WRITABLE ${CMAKE_INSTALL_PREFIX})
    message(FATAL_ERROR "CMAKE_INSTALL_PREFIX is not writable: ${CMAKE_INSTALL_PREFIX}")
  endif()
else()
  file(TOUCH ${CMAKE_INSTALL_PREFIX}/.cmake_writable "")
endif()

CMake Presets install prefix

CMakePresets.json can set a default install prefix:

{
  "version": 3,

"configurePresets": [
{
  "name": "default",
  "binaryDir": "${sourceDir}/build",
  "installDir": "${sourceDir}/build/local"
}
]
}