Skip to main content

Functions and Macros

Overview

Functions and macros allow you to create reusable CMake code, reducing duplication and improving maintainability. While they appear similar, they have important differences in how they handle variable scope.

Functions

Functions create a new scope for variables. Changes to variables inside the function don't affect the calling scope unless explicitly requested.

Basic Syntax

function(function_name arg1 arg2)
# Function body
# Variables set here are local
endfunction()

# Call it
function_name(value1 value2)

Simple Example

function(print_message msg)
message(STATUS "Message: ${msg}")
endfunction()

print_message("Hello, CMake!")
# Output: -- Message: Hello, CMake!

Arguments

Functions automatically get these variables:

  • ${ARGC}: Argument count
  • ${ARGV}: All arguments as a list
  • ${ARGN}: Arguments beyond named ones
  • ${ARG0}, ${ARG1}, etc.: Individual arguments
function(show_args first second)
message(STATUS "first: ${first}")
message(STATUS "second: ${second}")
message(STATUS "ARGC: ${ARGC}")
message(STATUS "ARGV: ${ARGV}")
message(STATUS "ARGN: ${ARGN}")
endfunction()

show_args(a b c d e)
# Output:
# first: a
# second: b
# ARGC: 5
# ARGV: a;b;c;d;e
# ARGN: c;d;e

ARGN is useful for variadic functions:

function(add_my_executable name)
add_executable(${name} ${ARGN})
target_compile_features(${name} PRIVATE cxx_std_17)
endfunction()

add_my_executable(myapp main.cpp utils.cpp config.cpp)
# Creates executable with all source files

Return Values

Functions don't have explicit return values. Use set(... PARENT_SCOPE) to return data:

function(compute_value result_var)
set(computed "some_value")
set(${result_var} ${computed} PARENT_SCOPE)
endfunction()

compute_value(my_result)
message(STATUS "Result: ${my_result}") # Result: some_value

Important: Setting PARENT_SCOPE doesn't set the variable in the function's scope:

function(example)
set(VAR "value" PARENT_SCOPE)
message(STATUS "In function: ${VAR}") # Empty!
endfunction()

example()
message(STATUS "In parent: ${VAR}") # value

To set both:

function(example)
set(VAR "value" PARENT_SCOPE)
set(VAR "value") # Also set locally
endfunction()

Macros

Macros are like text substitution - they don't create a new scope. Variables set in a macro affect the calling scope.

Basic Syntax

macro(macro_name arg1 arg2)
# Macro body
# Variables set here affect caller
endmacro()

macro_name(value1 value2)

Function vs Macro

Key difference illustrated:

function(my_function)
set(RESULT "from function")
endfunction()

macro(my_macro)
set(RESULT "from macro")
endmacro()

my_function()
message(STATUS "${RESULT}") # Empty - function scope isolated

my_macro()
message(STATUS "${RESULT}") # from macro - macro modifies caller

When to Use Macros

Use macros when you need to:

  • Modify variables in the calling scope
  • Create control flow (loops, conditionals)
  • Want simple text substitution

Most common use - control flow wrappers:

macro(require_package package)
find_package(${package} REQUIRED)
if(NOT ${package}_FOUND)
message(FATAL_ERROR "${package} is required but not found")
endif()
endmacro()

Argument Parsing

For complex functions with optional and named arguments, use cmake_parse_arguments():

function(create_test)
cmake_parse_arguments(
TEST # Prefix for output variables
"WILL_FAIL" # Options (boolean flags)
"NAME;TIMEOUT" # Single-value keywords
"SOURCES;LIBRARIES" # Multi-value keywords
${ARGN} # Arguments to parse
)

if(NOT TEST_NAME)
message(FATAL_ERROR "NAME is required")
endif()

add_executable(${TEST_NAME} ${TEST_SOURCES})
target_link_libraries(${TEST_NAME} PRIVATE ${TEST_LIBRARIES})

add_test(NAME ${TEST_NAME} COMMAND ${TEST_NAME})

if(TEST_WILL_FAIL)
set_tests_properties(${TEST_NAME} PROPERTIES WILL_FAIL TRUE)
endif()

if(TEST_TIMEOUT)
set_tests_properties(${TEST_NAME} PROPERTIES TIMEOUT ${TEST_TIMEOUT})
endif()
endfunction()

# Usage
create_test(
NAME test_math
SOURCES test_math.cpp
LIBRARIES mylib
TIMEOUT 30
WILL_FAIL
)

After parsing:

  • TEST_NAME = "test_math"
  • TEST_SOURCES = "test_math.cpp"
  • TEST_LIBRARIES = "mylib"
  • TEST_TIMEOUT = "30"
  • TEST_WILL_FAIL = TRUE

Practical Examples

Compiler Warnings

function(target_set_warnings target_name)
if(MSVC)
target_compile_options(${target_name} PRIVATE
/W4 # Warning level 4
/WX # Warnings as errors
)
else()
target_compile_options(${target_name} PRIVATE
-Wall
-Wextra
-Wpedantic
-Werror
)
endif()
endfunction()

# Use it
add_executable(myapp main.cpp)
target_set_warnings(myapp)

Library Creator

function(add_project_library name)
cmake_parse_arguments(
LIB
"INTERFACE"
"TYPE"
"SOURCES;PUBLIC_HEADERS;PRIVATE_HEADERS;DEPENDENCIES"
${ARGN}
)

# Determine library type
if(LIB_INTERFACE)
set(lib_type INTERFACE)
elseif(LIB_TYPE)
set(lib_type ${LIB_TYPE})
else()
set(lib_type STATIC)
endif()

# Create library
add_library(${name} ${lib_type} ${LIB_SOURCES})
add_library(MyProject::${name} ALIAS ${name})

# Setup includes
if(NOT LIB_INTERFACE)
target_include_directories(${name}
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src
)
else()
target_include_directories(${name} INTERFACE
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
)
endif()

# Link dependencies
if(LIB_DEPENDENCIES)
target_link_libraries(${name} PUBLIC ${LIB_DEPENDENCIES})
endif()

# C++17 requirement
target_compile_features(${name} PUBLIC cxx_std_17)
endfunction()

# Usage
add_project_library(mylib
TYPE STATIC
SOURCES
src/impl.cpp
src/utils.cpp
DEPENDENCIES
MyProject::core
fmt::fmt
)

Executable with Standard Setup

function(add_project_executable name)
cmake_parse_arguments(
EXE
""
""
"SOURCES;LIBRARIES"
${ARGN}
)

add_executable(${name} ${EXE_SOURCES})

target_link_libraries(${name} PRIVATE ${EXE_LIBRARIES})

target_compile_features(${name} PRIVATE cxx_std_17)

# Standard warnings
target_set_warnings(${name})

# Standard output directory
set_target_properties(${name} PROPERTIES
RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin
)
endfunction()

# Usage
add_project_executable(myapp
SOURCES main.cpp app.cpp
LIBRARIES MyProject::core MyProject::ui
)

Install Helper

function(install_project_library target)
install(TARGETS ${target}
EXPORT MyProjectTargets
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib
RUNTIME DESTINATION bin
INCLUDES DESTINATION include
)

# Install headers
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/include)
install(DIRECTORY include/
DESTINATION include
)
endif()
endfunction()

# Usage
install_project_library(mylib)

Variable Scope Comparison

Understanding scope differences:

set(GLOBAL_VAR "global")

function(test_function)
set(FUNC_VAR "function local")
set(GLOBAL_VAR "modified in function")
message(STATUS "In function: ${GLOBAL_VAR}")
endfunction()

macro(test_macro)
set(MACRO_VAR "macro local")
set(GLOBAL_VAR "modified in macro")
message(STATUS "In macro: ${GLOBAL_VAR}")
endmacro()

test_function()
message(STATUS "After function: ${GLOBAL_VAR}") # Still "global"
message(STATUS "FUNC_VAR: ${FUNC_VAR}") # Empty

test_macro()
message(STATUS "After macro: ${GLOBAL_VAR}") # "modified in macro"
message(STATUS "MACRO_VAR: ${MACRO_VAR}") # "macro local"

Control Flow in Macros

Macros can use flow control that affects the caller:

macro(early_return)
if(SOME_CONDITION)
return() # Returns from caller, not just macro!
endif()
endmacro()

This is dangerous and usually unintended. Functions don't have this problem:

function(safe_early_return)
if(SOME_CONDITION)
return() # Returns from function only
endif()
endfunction()

Organizing Functions

Create a utilities file:

cmake/ProjectUtils.cmake
function(target_set_warnings target)
# ... implementation
endfunction()

function(add_project_library name)
# ... implementation
endfunction()

function(add_project_executable name)
# ... implementation
endfunction()

Include in root CMakeLists.txt:

list(APPEND CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake)
include(ProjectUtils)

# Now functions are available
add_project_library(mylib SOURCES src/lib.cpp)

Advanced Patterns

Function Returning Multiple Values

function(get_version major minor patch)
set(${major} 1 PARENT_SCOPE)
set(${minor} 2 PARENT_SCOPE)
set(${patch} 3 PARENT_SCOPE)
endfunction()

get_version(MAJ MIN PAT)
message(STATUS "Version: ${MAJ}.${MIN}.${PAT}") # 1.2.3

Optional Arguments

function(add_my_library name)
cmake_parse_arguments(
LIB
"HEADER_ONLY" # Optional flag
"VERSION" # Optional value
"SOURCES" # Required value
${ARGN}
)

if(LIB_HEADER_ONLY)
add_library(${name} INTERFACE)
else()
if(NOT LIB_SOURCES)
message(FATAL_ERROR "SOURCES required for non-header-only library")
endif()
add_library(${name} ${LIB_SOURCES})
endif()

if(LIB_VERSION)
set_target_properties(${name} PROPERTIES VERSION ${LIB_VERSION})
endif()
endfunction()

# Header-only usage
add_my_library(utils HEADER_ONLY)

# Regular library usage
add_my_library(core SOURCES core.cpp VERSION 1.0.0)

Conditional Compilation Helper

function(target_add_feature target feature)
string(TOUPPER ${feature} FEATURE_UPPER)

option(ENABLE_${FEATURE_UPPER} "Enable ${feature}" ON)

if(ENABLE_${FEATURE_UPPER})
target_compile_definitions(${target} PRIVATE HAS_${FEATURE_UPPER})
message(STATUS "${target}: ${feature} enabled")
else()
message(STATUS "${target}: ${feature} disabled")
endif()
endfunction()

# Usage
add_executable(myapp main.cpp)
target_add_feature(myapp networking)
target_add_feature(myapp graphics)

Best Practices

Function/Macro Guidelines

Prefer functions over macros:

  • Functions provide proper scoping
  • Less surprising behavior
  • Easier to debug

Use macros only for:

  • Control flow wrappers (rare)
  • Simple text substitution
  • When you specifically need caller scope modification

Naming conventions:

  • Lowercase with underscores: add_my_library
  • Prefix with project: myproject_add_library
  • Descriptive names: target_set_warnings not set_warn

Documentation:

# Add a library with standard project configuration
#
# Arguments:
# name - Library name
# TYPE - STATIC, SHARED, or INTERFACE (default: STATIC)
# SOURCES - Source files
# DEPENDENCIES - Libraries to link
function(add_project_library name)
# ...
endfunction()

Common Pitfalls

Avoid These Mistakes

❌ Forgetting PARENT_SCOPE in functions:

function(get_value result)
set(${result} "value") # Wrong - only local
endfunction()

✅ Correct:

function(get_value result)
set(${result} "value" PARENT_SCOPE)
endfunction()

❌ Using return() in macros:

macro(bad_macro)
return() # Returns from caller!
endmacro()

✅ Use function:

function(good_function)
return() # Returns from function only
endfunction()

Quick Reference

# Function
function(name arg1 arg2)
# New scope
set(var "value" PARENT_SCOPE) # Return value
endfunction()

# Macro
macro(name arg1 arg2)
# Caller's scope
# Changes affect caller
endmacro()

# Argument parsing
cmake_parse_arguments(
PREFIX
"OPTIONS"
"SINGLE_VALUES"
"MULTI_VALUES"
${ARGN}
)

# Built-in variables
${ARGC} # Argument count
${ARGV} # All arguments
${ARGN} # Extra arguments
${ARG0} # First argument

Functions and macros are essential for creating maintainable, reusable CMake code. Master them to build clean, DRY (Don't Repeat Yourself) build systems.