こんにちは。田原です。

C++の変数はグローバル変数やローカル変数、メンバ変数等様々な変数があり厳密にスコープ(有効範囲)が定義されていますね。CMake変数にも同様にスコープがあります。CMakeはインタプリタということもあり、C++変数とは考え方がかなり異なります。今回はこのCMake変数のスコープについて解説します。

1.スコープの種類

C++の変数のスコープは寿命とアクセス範囲の組み合わせがあるので実に多種類あります。
{ 静的変数、スタック上の変数、ヒープ上の変数 } × { プログラム全体、同じソースファイル内、1つのクラス内、1つの関数内 }、更にスレッド・スコープもあります。単純な組み合わせではなく、なかなか複雑です。

それに比べるとCMakeは簡単です。

  1. CMakeはインタプリタですので、基本は設定コマンドが実行された時点でメモリ領域が確保されます。
  2. しかし、一定のケースでは呼び出し元で確保されたメモリ領域とは異なる領域が確保されます。
  3. 更に、その内の一部のケースで呼び出し元のメモリ領域へ設定することができます。

2.基本

先に述べたように基本は設定した時点以降でCMake変数は有効になります。

また、未設定の時は 空文字列 に展開されます。

message(STATUS "NORMAL_VAR=${NORMAL_VAR}")
set(NORMAL_VAR "parent")
message(STATUS "NORMAL_VAR=${NORMAL_VAR}")
set(NORMAL_VAR "normal")
message(STATUS "NORMAL_VAR=${NORMAL_VAR}")
実行結果
-- NORMAL_VAR=
-- NORMAL_VAR=parent
-- NORMAL_VAR=normal
マクロの内部でsetした場合、マクロを呼び出した時に設定されます。
macro(macro_sample)
    message(STATUS "MACRO_VAR=${MACRO_VAR}")
    set(MACRO_VAR "macro")
endmacro()

message(STATUS "MACRO_VAR=${MACRO_VAR}")
set(MACRO_VAR "parent")
message(STATUS "MACRO_VAR=${MACRO_VAR}")
macro_sample()
message(STATUS "MACRO_VAR=${MACRO_VAR}")
実行結果
-- MACRO_VAR=
-- MACRO_VAR=parent
-- MACRO_VAR=parent
-- MACRO_VAR=macro
include の例です。

include はC/C++の#includeと同じように振る舞いますのでスコープは形成しません。
微妙にadd_subdirectoryと混乱するかもしれませんので、念の為サンプルを載せておきます。

message(STATUS "INCLUDED_VAR=${INCLUDED_VAR}")
set(INCLUDED_VAR "included")
message(STATUS "INCLUDED_VAR=${INCLUDED_VAR}")
set(INCLUDED_VAR "parent")
message(STATUS "INCLUDED_VAR=${INCLUDED_VAR}")
include(included.cmake)
message(STATUS "INCLUDED_VAR=${INCLUDED_VAR}")
実行結果
-- INCLUDED_VAR=
-- INCLUDED_VAR=parent
-- INCLUDED_VAR=parent
-- INCLUDED_VAR=included

3.スコープが形成される(呼び出し先で別メモリ領域が確保される)ケース

冒頭に書いたように「基本」は特定のケースで無効になります。
function()~endfunction()の間、add_subdirectory()で読み込んだCMakeLists.txtの中、foreach()~endforeach()のループ変数です。
これらのスコープに入る前に定義された変数の値を見ることができますが、設定しても呼び出し元には反映されません。呼び出し先の変数用の領域が別途確保され、呼び出し元の値が「コピー」されているようです。

まずは、function()の例です。

(以降、未設定状態の変数確認は省略しています。)

function(function_sample)
    message(STATUS "FUNCTION_VAR=${FUNCTION_VAR}")
    set(FUNCTION_VAR "function")
    message(STATUS "FUNCTION_VAR=${FUNCTION_VAR}")
endfunction()

set(FUNCTION_VAR "parent")
function_sample()
message(STATUS "FUNCTION_VAR=${FUNCTION_VAR}")
実行結果
-- FUNCTION_VAR=parent
-- FUNCTION_VAR=function
-- FUNCTION_VAR=parent
次に、add_subdirectory()の例です。
add_executable(cpp_exe cpp_exe.cpp)

message(STATUS "SUBDIR_VAR=${SUBDIR_VAR}")
set(SUBDIR_VAR "subdir")
message(STATUS "SUBDIR_VAR=${SUBDIR_VAR}")
exe/cpp_exe.cpp
#include <iostream>

int main()
{
    std::cout << "Hello, world!!\n";
}
cmake_minimum_required(VERSION 3.13.4)
project(sample)

set(SUBDIR_VAR "parent")
add_subdirectory(exe)
message(STATUS "SUBDIR_VAR=${SUBDIR_VAR}")
実行結果(不要部分は省略)
-- SUBDIR_VAR=parent
-- SUBDIR_VAR=subdir
-- SUBDIR_VAR=parent
最後に、foreach()の例です。

foreach()のループ変数だけは、呼び出し先用に領域が設けられますが、それ以外のforeach()内部で設定した変数は「基本」と同じ振る舞いをします。

set(FOREACH_VAR0 "parent")
set(FOREACH_VAR1 "parent")
foreach(FOREACH_VAR0 RANGE 0 2)
    message(STATUS "FOREACH_VAR0=${FOREACH_VAR0}")
    message(STATUS "FOREACH_VAR1=${FOREACH_VAR1}")
    set(FOREACH_VAR0 "foreach")
    set(FOREACH_VAR1 "foreach")
endforeach()
message(STATUS "FOREACH_VAR0=${FOREACH_VAR0}")
message(STATUS "FOREACH_VAR1=${FOREACH_VAR1}")
実行結果
-- FOREACH_VAR0=0
-- FOREACH_VAR1=parent
-- FOREACH_VAR0=1
-- FOREACH_VAR1=foreach
-- FOREACH_VAR0=2
-- FOREACH_VAR1=foreach
-- FOREACH_VAR0=parent
-- FOREACH_VAR1=foreach

4.そして、第二の例外(PARENT_SCOPE)

更に、呼び出されて方でのみ有効な変数に対して、PARENT_SCOPE指定してsetすることで呼び出し元へ反映できるものがあります。
それは、function()~endfunction()とadd_subdirectory()で読み込んだCMakeLists.txtの中です。

振る舞いとしては、呼び出し元の変数が呼び出し先へコピーされ、PARENT_SCOPEを指定しない時はコピーされた変数へ設定され、PARENT_SCOPEを指定するとコピー元(呼び出し元)の変数が設定されるようです。

まずは、function()の例です。
function(function_sample)
    message(STATUS "FUNCTION_VAR0=${FUNCTION_VAR0}") # 呼び出し先(呼び出し元のコピー)
    set(FUNCTION_VAR0 "function" PARENT_SCOPE)       # 呼び出し元へ設定
    message(STATUS "FUNCTION_VAR0=${FUNCTION_VAR0}") # 呼び出し先
    set(FUNCTION_VAR0 "function XXX")                # 呼び出し先へ設定
    message(STATUS "FUNCTION_VAR0=${FUNCTION_VAR0}") # 呼び出し先

    message(STATUS "FUNCTION_VAR1=${FUNCTION_VAR1}")
    set(FUNCTION_VAR1 "function")
endfunction()

set(FUNCTION_VAR0 "parent")
set(FUNCTION_VAR1 "parent")
function_sample()
message(STATUS "FUNCTION_VAR0=${FUNCTION_VAR0}")
message(STATUS "FUNCTION_VAR1=${FUNCTION_VAR1}")
実行結果
-- FUNCTION_VAR0=parent
-- FUNCTION_VAR0=parent
-- FUNCTION_VAR0=function XXX
-- FUNCTION_VAR1=parent
-- FUNCTION_VAR0=function
-- FUNCTION_VAR1=parent
次に、add_subdirectory()の例です。
add_executable(cpp_exe cpp_exe.cpp)

message(STATUS "SUBDIR_VAR0=${SUBDIR_VAR0}")
set(SUBDIR_VAR0 "subdir" PARENT_SCOPE)
message(STATUS "SUBDIR_VAR0=${SUBDIR_VAR0}")
set(SUBDIR_VAR0 "subdir XXX")
message(STATUS "SUBDIR_VAR0=${SUBDIR_VAR0}")

message(STATUS "SUBDIR_VAR1=${SUBDIR_VAR1}")
set(SUBDIR_VAR1 "subdir")
exe/cpp_exe.cpp
#include <iostream>

int main()
{
    std::cout << "Hello, world!!\n";
}
cmake_minimum_required(VERSION 3.13.4)
project(sample)

set(SUBDIR_VAR0 "parent")
set(SUBDIR_VAR1 "parent")
add_subdirectory(exe)
message(STATUS "SUBDIR_VAR0=${SUBDIR_VAR0}")
message(STATUS "SUBDIR_VAR1=${SUBDIR_VAR1}")
実行結果(不要部分は省略)
-- SUBDIR_VAR0=parent
-- SUBDIR_VAR0=parent
-- SUBDIR_VAR0=subdir XXX
-- SUBDIR_VAR1=parent
-- SUBDIR_VAR0=subdir
-- SUBDIR_VAR1=parent
最後に、foreach()のループ変数について補足(PARENT_SCOPE非対応です)

ところで、foreach()は少なくもCMake 3.13.4ではPARENT_SCOPEを指定するとエラーになります。

set(FOREACH_VAR0 "parent")
set(FOREACH_VAR1 "parent")
foreach(FOREACH_VAR0 RANGE 0 2)
    message(STATUS "FOREACH_VAR0=${FOREACH_VAR0}")
    message(STATUS "FOREACH_VAR1=${FOREACH_VAR1}")
    set(FOREACH_VAR0 "foreach" PARENT_SCOPE)
    set(FOREACH_VAR1 "foreach")
endforeach()
message(STATUS "FOREACH_VAR0=${FOREACH_VAR0}")
message(STATUS "FOREACH_VAR1=${FOREACH_VAR1}")
実行結果
-- FOREACH_VAR0=0
-- FOREACH_VAR1=parent
CMake Warning (dev) at sample.cmake:6 (set):
  Cannot set "FOREACH_VAR0": current scope has no parent.
This warning is for project developers.  Use -Wno-dev to suppress it.

-- FOREACH_VAR0=1
-- FOREACH_VAR1=foreach
CMake Warning (dev) at sample.cmake:6 (set):
  Cannot set "FOREACH_VAR0": current scope has no parent.
This warning is for project developers.  Use -Wno-dev to suppress it.

-- FOREACH_VAR0=2
-- FOREACH_VAR1=foreach
CMake Warning (dev) at sample.cmake:6 (set):
  Cannot set "FOREACH_VAR0": current scope has no parent.
This warning is for project developers.  Use -Wno-dev to suppress it.

-- FOREACH_VAR0=parent
-- FOREACH_VAR1=foreach

5.まとめ

CMakeで複雑な変数操作を行うことは比較的少ないとは思います。
実際、呼び出し元に変数の値を反映したい時、私はついついmacroを使っていました。当時は今回のような深い調査をしないままCMakeを使っていたので手抜きですね。
しかし、CMakeはスクリプト(簡易言語)とはいえ、なるべくfunctionを使ったほうが思わぬ落とし穴にハマりにくいのでfunctionを使った方が良さそうです。

年をとるにつれ最近はどうも集中できなくていけません。これはきっと椅子のせいだ!!と(椅子のせいにして)、大塚家具でハーマンミラーのエンボディ・チェアを買いました。有名なアーロンチェアと迷ったのだけど、長~い時間背もたれに頼る私にはこちらの方が良さそうでした。座面もゆったりしているから疲れなさそう。届くのが楽しみです。

それでは今回はこのへんで。お疲れ様でした!