CMake scope: variables and targets in function, macro and subdirectory

Introduction

Understanding scope in CMake is crucial for managing variable visibility and behavior in your build system.

  • A function creates a scope where local variables do not affect parent scope variables.
  • A macro operates in the caller’s scope, allowing variables change there but treats arguments as immutable strings.
  • A subdirectory creates its own scope.
  • Function and subdirectory have access to a copy of their parent scope variables.
  • Function, macro and subdirectory define globally accessible targets.

This guide explores the accessibility of a variable with regards to the scope it is defined with practical examples to clarify their usage.

Prerequisits

I am assuming

  • you had a look at my previous posts on CMake programming
  • you have CMake v3.23+ on your machine,
  • you have a compiler like GCC, Clang, Intel, or MS Visual C++ installed on your operating system.

I call the variables

  • localName: a variable local to function or macro
  • parentName: a variable defined in the parent scope of where function or macro is called.

Run CMake snippets

Create a desired directory and CMakeLists.txt file in it. In a terminal navigate to that directory:

mkdir build
cd build

Then run

cmake ..

You will see the results on screen.

Function Scope

In CMake, function creates a new scope where it is called. Variables defined in a function are private to the function and NOT defined/accessible outside of the function where it is called. However, anything defined in a macro is defined where the macro is called. Let’s show it with examples.

In the example below, variable localName is not defined outside the function where it is called:

function(initFuncLocalDef)
    set(localName "particle.h") 
endfunction()

initFuncLocalDef()  # call function
message("localName=" ${localName}) # prints: localName=

The below example shows that in a function you have access to a copy of parent scope variables, your change to the copies of parent variables is local and has no effect on the parent variables out of the function. Also any variable definitions in a function is private to the function.

function(initFuncParentAccess)
    message("In function, access parent, parentName=" ${parentName})
    set(parentName "particle.h")
    message("In function, after set parent, parentName=" ${parentName})
    set(root_dir "/path/to/myRoot00")
endfunction()

set(parentName "sphere.h")
initFuncParentAccess()
message("outside function, parentName=" ${parentName})
message("outside function, root_dir=" ${root_dir})

It produces the below output:

In function, access parent, parentName=sphere.h
In function, after set parent, parentName=particle.h
outside function, parentName=sphere.h
outside function, root_dir=

In the example below, it is shown that function argument similiar to pass by value in C/C++. A copy of a variable is passed to the function, any change to that copy has no effect on the original variable.

function(initFuncLocalAccess localName)
    message("In function, before change, localName=" ${localName}) 
    set(localName "particle.h") 
    message("In function, after change, localName=" ${localName}) 
endfunction()

set(parentName  sphere.h)

initFuncLocalAccess(${parentName})
message("outside function, localName=" ${localName})
message("outside function, parentName=" ${parentName})

It produces this output:

In function, before change, localName=sphere.h
In function, after change, localName=particle.h
outside function, localName=
outside function, parentName=sphere.h

A CMake function also allows you to define a variable in the caller’s (or parent) scope. In the example below, parentName is changed in the parent scope with PARENT_SCOPE setting:

function(initFuncLocalAccess)
    message("In function, before change, parentName=" ${parentName}) 
    set(parentName "particle.h" PARENT_SCOPE) # sets in parent scope not inside function
    message("In function, before change, parentName=" ${parentName}) 
endfunction()

set(parentName  sphere.h)

initFuncLocalAccess()
message("outside function, parentName=" ${parentName})

The outcome is

In function, before change, parentName=sphere.h
In function, before change, parentName=sphere.h
outside function, parentName=particle.h

To also change parentName locally, you need to write another set without PARENT_SCOPE.

Now we can define a function that sets a variable in its parent scope, similiar to pass by reference in C/C++:

function(getName outFullName first last)
    set(${outFullName} "${first} ${last}" PARENT_SCOPE)
endfunction()

getName(fullName "Chandler" "Bing")

message(${fullName}) # prints Chandler Bing

Macro scope

In contrast to function, a macro doesn’t create a private scope. We can assume that the code in macro is pasted where it is called.

In the example below, parentName is changed inside and outside of macro where it is called. The macro also defined root_dir which is accessbile outside after it is called.

macro(initMacroParentAccess)
    message("In Macro, before set parent, parentName=" ${parentName})
    set(parentName "particle.h")
    message("In Macro, after set parent, parentName=" ${parentName})

    set(root_dir "/path/to/myRoot00")
endmacro()

set(parentName  sphere.h)
initMacroParentAccess()

message("outside function, parentName=" ${parentName})
message("outside function, root_dir=" ${root_dir})

It produces below output

In Macro, before set parent, parentName=sphere.h
In Macro, after set parent, parentName=particle.h
outside function, parentName=particle.h
outside function, root_dir=/path/to/myRoot00

Macro argument is different to function argument. We can think of it as a read-only string that cannot be modified. So if you want to change a variable, don’t pass it to macro as argument, just change it in the macro by refering to its parent name. The example below is just to clear this up, but avoid setting macro argument inside macro.

macro(initMacroLocalAccess localName)
    message("In Macro, before set arg, localName=" ${localName}) # macro arg string replacement
    set(localName "particle.h") # defines localName in the outerscope
    message("In Macro, after set arg, localName=" ${localName}) # still macro arg string replacement, doesn't use defined var
endmacro()

set(name sphere.h)
initMacroLocalAccess(${name})
message("outside Macro, localName=" ${localName})

The output is

In Macro, before set arg, localName=sphere.h
In Macro, after set arg, localName=sphere.h
outside Macro, localName=particle.h

Subdirectory scope

Each subdirectory has its own scope. It receives a copy of variables of parent directory, grandparent directory, and so on. However, they don’t have access to sibiling directories.

To show this, I created an example with directory tree of

root/folderA/folderB
root/folderC

each directory, root, folderA, folderB, and folderC, contains file CMakeLists.txt.

root/CMakeLists.txt is:

cmake_minimum_required(VERSION 3.20)
project(example LANGUAGES CXX)

set(varRoot "varRootIsSet")
add_executable(rootTarget main.cpp)

function(printVars dir)
    # some code ....
endfunction()


add_subdirectory(folderA)
add_subdirectory(folderC)
printVars("root")

For the sake of readability, I put the code for function printVars at the end of this section.

folderA/CMakeLists.txt is:

add_executable(targetA ../main.cpp)
set(varA "VarAIsSet") # order is important, to have folderAVar to go into B, should be defined before
add_subdirectory("folderB")
printVars("folderA")

folderB/CMakeLists.txt is:

set(varB "VarBIsSet")
add_executable(targetB ../../main.cpp)
printVars("folderB")

folderC/CMakeLists.txt is:

add_executable(targetC ../main.cpp)
set(varC "VarCIsSet")
printVars("folderC")

And finally the definition of printVars is:

function(printVars dir)
    message("-------")
    if(TARGET rootTarget)
        message(${dir}": root target is defined.")
    endif()
    if(TARGET targetA)
        message(${dir}": targetA is defined.")    
    endif()
    if(TARGET targetB)
        message(${dir}": targetB is defined.")       
    endif()
    if(TARGET targetC)
        message(${dir}": targetC is defined.")
    endif()

    message(${dir}":varRoot:"${varRoot})
    message(${dir}":varA:"${varA})
    message(${dir}":varB:"${varB})
    message(${dir}":varC:"${varC})
endfunction()

By running CMake, we get below terminal output:

-------
folderB": root target is defined."
folderB": targetA is defined."
folderB": targetB is defined."
folderB":varRoot:"varRootIsSet
folderB":varA:"VarAIsSet
folderB":varB:"VarBIsSet
folderB":varC:"
-------
folderA": root target is defined."
folderA": targetA is defined."
folderA": targetB is defined."
folderA":varRoot:"varRootIsSet
folderA":varA:"VarAIsSet
folderA":varB:"
folderA":varC:"
-------
folderC": root target is defined."
folderC": targetA is defined."
folderC": targetB is defined."
folderC": targetC is defined."
folderC":varRoot:"varRootIsSet
folderC":varA:"
folderC":varB:"
folderC":varC:"VarCIsSet
-------
root": root target is defined."
root": targetA is defined."
root": targetB is defined."
root": targetC is defined."
root":varRoot:"varRootIsSet
root":varA:"
root":varB:"
root":varC:"

The summary is:

  • Variables defined in root are readable in subdirectories
  • Variables in folderA are readable in folderB, but NOT vice versa.
  • Variable in folderA and folderB are not readable in folderC.
  • Variables in FolderC are not readable in folderA and folderB.
  • The targets are accessible everywhere, however, the code flow must define them first.

If you want to set a variable in a subdirectory and the change is seen in the parent directory you can use set with PARENT_SCOPE like we did with function. Another option is to make it internal cache variable and it will be globally accessible/editable.

Accessibility of Targets

  • A target defined in a function or in macro is globally accessible after they are called.
  • A target defined in a subdirectory is globally accessible after the CMake configuration run passes through the subdirectory.

In the example below, defSpaceTargetFunc() and defSpaceTargetMacro() can interchangeably be called to define space target.

function(defSpaceTargetFunc)
    add_executable(space)
    target_sources(space PUBLIC main.cpp)
endfunction()

macro(defSpaceTargetMacro)
    add_executable(space)
    target_sources(space PUBLIC main.cpp)
endmacro()

defSpaceTargetFunc() # comment this, and uncomment next line
#defSpaceTargetMacro()
get_target_property(spaceSources space SOURCES)
message("sources are:" ${spaceSources})

Function VS Macro

Function is generally safer and cleaner choice as it doesn’t pollute the caller scope with its private variables.

Tags ➡ C++

Subscribe

I notify you of my new posts

Latest Posts

Comments

0 comment