Cpp Notes

6.programming

Programming in CMake

Control Flow

  • CMake provides a set of all-caps keywords for control flow, particularly in if() statements. Variables can be referenced directly or with ${}.
  • Historically, if() statements predate variable expansion, but as of CMake 3.1+, quoting variable references avoids potential re-expansions (policy CMP0054).

Example of if statement:

if(variable)
    # True if variable is `ON`, `YES`, `TRUE`, `Y`, or any non-zero number
else()
    # True if variable is `0`, `OFF`, `NO`, `FALSE`, `N`, `IGNORE`, `NOTFOUND`, `""`, or ends in `-NOTFOUND`
endif()

Quoted Variables in CMake 3.1+:

With CMake 3.1+ and CMP0054 enabled, quoted variables will not be re-expanded:

if("${variable}")
    # True if variable is not "false-like"
else()
    # False if variable is undefined, which expands to `""`
endif()

Common Control Flow Keywords:

  • Unary Keywords:

    • NOT: Negates the result.
    • TARGET: Checks if a given name is a target.
    • EXISTS (file): Checks if a file exists.
    • DEFINED: Checks if a variable is defined.
  • Binary Keywords:

    • STREQUAL: Compares two strings for equality.
    • AND, OR: Logical operators.
    • MATCHES: Evaluates a regular expression match.
    • VERSION_LESS, VERSION_LESS_EQUAL: Compares version numbers (CMake 3.7+ for VERSION_LESS_EQUAL).
  • Parentheses: You can use parentheses to group expressions for clarity and precedence:

    if((variable1 AND variable2) OR NOT variable3)
        # Logic with grouped conditions
    endif()

Generator Expressions

Most CMake logic (like if statements) is executed at configure time. However, when you need logic to be evaluated at build or install time, you use generator expressions. These expressions are evaluated within target properties.

Basic Forms of Generator Expressions:

  1. Informational Expressions:

    • Form: $<KEYWORD>
    • These provide configuration-specific information.
  2. Conditional Expressions:

    • Form: $<KEYWORD:value>
    • KEYWORD controls the evaluation, and value is what gets evaluated.
    • If KEYWORD evaluates to 1, the value is substituted; if 0, it is not.
    • Example of nesting expressions: $<$<CONFIG:Debug>:--my-flag>

Example: Debug-Specific Compile Option

To apply a compile flag only for the Debug configuration:

target_compile_options(MyTarget PRIVATE "$<$<CONFIG:Debug>:--my-flag>")
  • This approach is better than using specialized *_DEBUG variables and works across configurations.

Why Avoid Configure-Time Values for Configurations?

Never use configure-time values for the current configuration, as multi-configuration generators (like IDEs) don't have a "current" configuration at configure time. Generator expressions should handle this dynamically at build time.

Common Uses for Generator Expressions

  1. Limiting to a Specific Language:

    • Use generator expressions to limit an option or property to certain languages, such as CXX, and avoid mixing with others like CUDA.
  2. Accessing Configuration-Dependent Properties:

    • Generator expressions allow access to properties like target file locations that change based on the configuration.
  3. Handling Build and Install Directories:

    • A common pattern for including directories during both build and install:
      target_include_directories(
          MyTarget
        PUBLIC
          $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
          $<INSTALL_INTERFACE:include>
      )
    • This sets different include paths depending on whether the project is being built or installed.

Key Considerations

  • You can nest generator expressions for more complex logic.
  • Variables can help simplify nested generator expressions.
  • Some generator expressions accept multiple values, separated by commas.

Generator expressions are a powerful tool for making CMake logic dynamic and configuration-specific, particularly for multi-configuration builds like those in IDEs.

Macros and Functions

The primary difference between macros and functions in CMake is scope:

  • Functions: Have their own scope. Variables set inside a function are local to that function unless explicitly passed to the parent scope using PARENT_SCOPE.
  • Macros: Do not have a scope. Variables set in a macro are visible globally, which can lead to "leakage" of variables into other parts of the code.

Functions and Scope

When you define and call functions in CMake, variables declared or modified inside the function are local to that function by default. This means the changes made to a variable inside a function won't be reflected outside of it, unless you explicitly use PARENT_SCOPE to tell CMake that you want the variable to be modified in the parent scope (the scope where the function was called from).

  • PARENT_SCOPE: When you use the set() command with PARENT_SCOPE, the variable is updated in the scope of the caller (the parent function or global scope), not within the local scope of the function.

Important Notes:

  • CMake functions don't return values like traditional programming languages. Instead, you manipulate variables directly by modifying their scope.
  • When nesting functions, if you want a variable to be passed up multiple levels of scope, you'll need to propagate it using PARENT_SCOPE in each function.
# Define a function that sets a variable in the parent scope
function(set_variable var_name var_value)
    message(STATUS "Inside function: setting ${var_name} to ${var_value}")
    set(${var_name} "${var_value}" PARENT_SCOPE)
endfunction()

# Usage
set_variable(MyVar "Hello from function!")
message(STATUS "Outside function: MyVar is now '${MyVar}'")
-- Inside function: setting MyVar to Hello from function!
-- Outside function: MyVar is now 'Hello from function!'
  • Inside the Function: The variable MyVar is set to "Hello from function!" using PARENT_SCOPE, meaning the value is updated in the calling scope, not just inside the function.
  • Outside the Function: When message(STATUS ...) is called after the function, the value of MyVar is printed as it was set by the function.
# Define an inner function that sets a variable in the parent scope
function(inner_function var_name)
    set(${var_name} "Inner Value" PARENT_SCOPE)
endfunction()

# Define an outer function that calls the inner function
function(outer_function_not_propagate var_name)
    inner_function(${var_name})
endfunction()


function(outer_function_propagate_again var_name)
    inner_function(${var_name})
    set(${var_name} ${var_name} PARENT_SCOPE)
endfunction()

# Usage
outer_function_not_propagate(OuterVar)
message(STATUS "OuterVar is now '${OuterVar}'")
# OuterVar is now ''

outer_function_propagate_again(OuterVar)
message(STATUS "OuterVar is now '${OuterVar}'")
# OuterVar is now 'OuterVar'

Handling Named Arguments in CMake

  • CMake supports named arguments, similar to the named parameters in built-in CMake functions. You can implement this feature in your own functions using the cmake_parse_arguments command.
  • For versions of CMake older than 3.5, you need to include the CMakeParseArguments module.
function(COMPLEX)
    cmake_parse_arguments(
        COMPLEX_PREFIX       # Prefix for parsed arguments
        "FLAG1;FLAG2"     # Boolean flags (no value expected)
        "ONE_VALUE;ONE_VALUE2"  # Single-value arguments
        "MULTI_VALUES"       # Multi-value arguments
        ${ARGN}              # Arguments passed to the function
    )
    message("COMPLEX_PREFIX_FLAG1=${COMPLEX_PREFIX_FLAG1}")
    message("COMPLEX_PREFIX_FLAG2=${COMPLEX_PREFIX_FLAG2}")
    message("COMPLEX_PREFIX_ONE_VALUE=${COMPLEX_PREFIX_ONE_VALUE}")
    message("COMPLEX_PREFIX_ONE_VALUE2=${COMPLEX_PREFIX_ONE_VALUE2}")
    message("COMPLEX_PREFIX_MULTI_VALUES=${COMPLEX_PREFIX_MULTI_VALUES}")
    message("COMPLEX_PREFIX_UNPARSED_ARGUMENTS=${COMPLEX_PREFIX_UNPARSED_ARGUMENTS}")
endfunction()

complex(FLAG1
        MULTI_VALUES some other values
        ONE_VALUE value
        expected_not_parsed1 expected_not_parsed2)

# COMPLEX_PREFIX_FLAG1=TRUE
# COMPLEX_PREFIX_FLAG2=FALSE
# COMPLEX_PREFIX_ONE_VALUE=value
# COMPLEX_PREFIX_ONE_VALUE2=
# COMPLEX_PREFIX_MULTI_VALUES=some;other;values
# COMPLEX_PREFIX_UNPARSED_ARGUMENTS=expected_not_parsed1;expected_not_parsed2

Additional Notes

  • Positional arguments: You can combine named arguments with positional arguments. Any remaining unparsed arguments are captured in COMPLEX_PREFIX_UNPARSED_ARGUMENTS.

  • Alternative method: The official documentation shows a method using set() to handle lists without explicitly writing semicolons. Choose the style that best suits your needs.

configure_file

  • The configure_file command in CMake is used to generate files by substituting CMake variables within an input template file (usually ending with .in).
  • This is frequently used for configuration headers or other generated files where project-specific information (such as version numbers) needs to be included.

Example: Version Header File

Suppose you have a file Version.h.in:

// Version.h.in
#pragma once

#define MY_VERSION_MAJOR @PROJECT_VERSION_MAJOR@
#define MY_VERSION_MINOR @PROJECT_VERSION_MINOR@
#define MY_VERSION_PATCH @PROJECT_VERSION_PATCH@
#define MY_VERSION_TWEAK @PROJECT_VERSION_TWEAK@
#define MY_VERSION "@PROJECT_VERSION@"

This file contains placeholders (@PROJECT_VERSION_MAJOR@, etc.) that will be replaced with actual values.

To generate the file during the build process:

configure_file(
    "${PROJECT_SOURCE_DIR}/include/My/Version.h.in"
    "${PROJECT_BINARY_DIR}/include/My/Version.h"
)
  • The configure_file command copies Version.h.in to the target location and substitutes all CMake variables.
  • After this, include the binary directory in your build system, as this file will be generated there.

Keywords for configure_file:

  • @ONLY: Prevents substitution of ${} syntax (useful when both ${} and @ syntax are present in the input file).
  • COPY_ONLY: Skips variable substitution and acts like a simple file copy.

Example of Use with Boolean Variables

If you need to handle boolean flags in a header, CMake provides:

  • #cmakedefine: For #define if the variable is true.
  • #cmakedefine01: For #define 1 if true and #define 0 if false.

Reading Values from Files

CMake can also read values from a file. For example, you might want to read a version number from a header file.

Example: Reading a Version from a Header

# Regular expression to extract version from the header
set(VERSION_REGEX "#define MY_VERSION[ \t]+\"(.+)\"")

# Read the line containing the version
file(STRINGS "${CMAKE_CURRENT_SOURCE_DIR}/include/My/Version.hpp" 
    VERSION_STRING REGEX ${VERSION_REGEX})

# Extract the version string
string(REGEX REPLACE ${VERSION_REGEX} "\\1" VERSION_STRING "${VERSION_STRING}")

# Use the extracted version in the project definition
project(My LANGUAGES CXX VERSION ${VERSION_STRING})
  • file(STRINGS): Reads lines from a file that match a given regular expression.
  • string(REGEX REPLACE): Extracts the matched version string using a capture group (\\1).

This method is useful when you need to synchronize version numbers between CMake and a manually maintained source file (such as a header-only library).