CMake 系列(七):设置编译器选项

前面的示例展示了如何探测 CMake,从而获得关于编译器的信息,以及如何切换项目中的编译器。本篇的任务是控制项目的编译器标志。CMake 为调整或扩展编译器标志提供了很大的灵活性,可以选择下面两种方法:

  • CMake 将编译选项视为目标属性。因此,可以根据每个目标设置编译选项,而不需要覆盖 CMake 默认值;
  • 可以使用 -D CLI 标志直接修改 CMAKE_<LANG>_FLAGS_<CONFIG> 变量。这将影响项目中的所有目标,并覆盖或扩展 CMake 默认值。

本示例中,我们将展示这两种方法。

提示:本示例包含一个 C++ 示例,在 CMake 3.5 或更高版本中有效,并已在 GNU/Linux、macOS 和 Windows 上测试通过。

准备工作

编写一个示例程序,计算不同几何形状的面积,compute-areas.cpp:

#include "geometry_circle.hpp"
#include "geometry_polygon.hpp"
#include "geometry_rhombus.hpp"
#include "geometry_square.hpp"
#include <cstdlib>
#include <iostream>

int main() {
  using namespace geometry;
  double radius = 2.5293;
  double A_circle = area::circle(radius);
  std::cout << "A circle of radius " << radius << " has an area of " << A_circle
            << std::endl;
  int nSides = 19;
  double side = 1.29312;
  double A_polygon = area::polygon(nSides, side);
  std::cout << "A regular polygon of " << nSides << " sides of length " << side
            << " has an area of " << A_polygon << std::endl;
  double d1 = 5.0;
  double d2 = 7.8912;
  double A_rhombus = area::rhombus(d1, d2);
  std::cout << "A rhombus of major diagonal " << d1 << " and minor diagonal " << d2
            << " has an area of " << A_rhombus << std::endl;
  double l = 10.0;
  double A_square = area::square(l);
  std::cout << "A square of side " << l << " has an area of " << A_square
            << std::endl;
  return EXIT_SUCCESS;
}

函数的各种实现分布在不同的文件中,每个几何形状都有一个头文件和源文件。总共有 4 个头文件和 5 个源文件要编译:

├─ CMakeLists.txt
├─ compute-areas.cpp
├─ geometry_circle.cpp
├─ geometry_circle.hpp
├─ geometry_polygon.cpp
├─ geometry_polygon.hpp
├─ geometry_rhombus.cpp
├─ geometry_rhombus.hpp
├─ geometry_square.cpp
└─ geometry_square.hpp

具体实施

现在已经有了源代码,我们的目标是配置项目,并使用编译器标志进行实验。

首先,设置 CMake 的最低版本:

cmake_minimum_required(VERSION 3.5 FATAL_ERROR)

声明项目名称和语言:

project(recipe-08 LANGUAGES CXX)

然后,打印当前编译器标志,CMake 将对所有 C++ 目标使用这些标志:

message("C++ compiler flags: ${CMAKE_CXX_FLAGS}")

为目标准备了一个标志列表,其中一些将无法在 Windows 上使用:

list(APPEND flags "-fPIC" "-Wall")

if(NOT WIN32)
  list(APPEND flags "-Wextra" "-Wpedantic")
endif()

添加一个新的目标——geometry 库,并列出它的源依赖关系:

add_library(geometry
  STATIC
  geometry_circle.cpp
  geometry_circle.hpp
  geometry_polygon.cpp
  geometry_polygon.hpp
  geometry_rhombus.cpp
  geometry_rhombus.hpp
  geometry_square.cpp
  geometry_square.hpp
)

为这个库目标设置编译选项:

target_compile_options(geometry
  PRIVATE
  ${flags}
)

然后,将生成 compute-areas 可执行文件作为一个目标:

add_executable(compute-areas compute-areas.cpp)

还为可执行目标设置编译选项:

target_compile_options(compute-areas
  PRIVATE
  "-fPIC"
)

最后,将可执行文件链接到 geometry 库:

target_link_libraries(compute-areas geometry)

工作原理

本例中,警告标志有 -Wall、-Wextra 和 -Wpedantic,将这些标志添加到 geometry 目标的编译选项中;compute-areas 和 geometry 目标都将使用 -fPIC 标志。编译选项可以添加三个级别的可见性:INTERFACE、PUBLIC 和 PRIVATE。

可见性的含义如下:

  • PRIVATE:编译选项会应用于给定的目标,不会传递给与目标相关的目标。本示例中,即使 compute-areas 将链接到 geometry 库,compute-areas 也不会继承 geometry 目标上设置的编译器选项;
  • INTERFACE:给定的编译选项将只应用于指定目标,并传递给与目标相关的目标;
  • PUBLIC:编译选项将应用于指定目标和使用它的目标。

目标属性的可见性是 CMake 的核心,本系列后面的示例中会经常讨论这个话题。以这种方式添加编译选项,不会影响全局 CMake 变量 CMAKE_<LANG>_FLAGS_<CONFIG>,并能更细粒度地控制在哪些目标上使用哪些选项。

我们如何验证这些标志是否按照我们的意图正确使用呢?或者换句话说,如何确定项目在 CMake 构建时实际使用了哪些编译标志?一种方法是,使用 CMake 将额外的参数传递给本地构建工具,本例中会设置环境变量 VERBOSE=1:

$ mkdir -p build
$ cd build
$ cmake ..
$ cmake --build . -- VERBOSE=1

输出确认了编译标志,说明指令设置正确:

... lots of output ...
[ 14%] Building CXX object CMakeFiles/geometry.dir/geometry_circle.cpp.o
/usr/bin/c++ -fPIC -Wall -Wextra -Wpedantic -o CMakeFiles/geometry.dir/geometry_circle.cpp.o -c /home/bast/tmp/cmake-cookbook/chapter-01/recipe-08/cxx-example/geometry_circle.cpp
[ 28%] Building CXX object CMakeFiles/geometry.dir/geometry_polygon.cpp.o
/usr/bin/c++ -fPIC -Wall -Wextra -Wpedantic -o CMakeFiles/geometry.dir/geometry_polygon.cpp.o -c /home/bast/tmp/cmake-cookbook/chapter-01/recipe-08/cxx-example/geometry_polygon.cpp
[ 42%] Building CXX object CMakeFiles/geometry.dir/geometry_rhombus.cpp.o
/usr/bin/c++ -fPIC -Wall -Wextra -Wpedantic -o CMakeFiles/geometry.dir/geometry_rhombus.cpp.o -c /home/bast/tmp/cmake-cookbook/chapter-01/recipe-08/cxx-example/geometry_rhombus.cpp
[ 57%] Building CXX object CMakeFiles/geometry.dir/geometry_square.cpp.o
/usr/bin/c++ -fPIC -Wall -Wextra -Wpedantic -o CMakeFiles/geometry.dir/geometry_square.cpp.o -c /home/bast/tmp/cmake-cookbook/chapter-01/recipe-08/cxx-example/geometry_square.cpp

... more output ...

[ 85%] Building CXX object CMakeFiles/compute-areas.dir/compute-areas.cpp.o
/usr/bin/c++ -fPIC -o CMakeFiles/compute-areas.dir/compute-areas.cpp.o -c /home/bast/tmp/cmake-cookbook/chapter-01/recipe-08/cxx-example/compute-areas.cpp

... more output ...

控制编译器标志的第二种方法,不需要对 CMakeLists.txt 进行修改。如果想在这个项目中修改 geometry 和 compute-areas 目标的编译器选项,可以使用 CMake 参数进行配置:

$ cmake -D CMAKE_CXX_FLAGS="-fno-exceptions -fno-rtti" ..

这个命令将编译项目,禁用异常和运行时类型标识(RTTI)。

也可以使用全局标志,运行以下命令进行配置:

$ cmake -D CMAKE_CXX_FLAGS="-fno-exceptions -fno-rtti -fpic -Wall -Wextra -Wpedantic" ..

这将使用 -fno-rtti -fpic -Wall -Wextra -Wpedantic 配置 geometry 目标,同时使用 -fno-exceptions -fno-rtti -fpic 配置 compute-areas 目标。

注意:本系列中,我们推荐为每个目标设置编译器标志。使用 target_compile_options() 不仅允许对编译选项进行细粒度控制,而且还可以更好地与 CMake 的更高级特性进行集成。

更多信息

大多数时候,编译器有特性标志。当前的例子只适用于 GCC 和 Clang;其他供应商的编译器不确定是否会理解(如果不是全部)这些标志。如果项目是真正跨平台的,那么这个问题就必须得到解决,有三种方法可以解决这个问题。

最典型的方法是将所需编译器标志列表附加到每个配置类型的 CMake 变量 CMAKE_<LANG>_FLAGS_<CONFIG> 中。标志被确定为对给定编译器有效的标志,因此将包含在 if-endif 子句中,用于检查 CMAKE_<LANG>_COMPILER_ID 变量,例如:

if(CMAKE_CXX_COMPILER_ID MATCHES GNU)
  list(APPEND CMAKE_CXX_FLAGS "-fno-rtti" "-fno-exceptions")
  list(APPEND CMAKE_CXX_FLAGS_DEBUG "-Wsuggest-final-types" "-Wsuggest-final-methods" "-Wsuggest-override")
  list(APPEND CMAKE_CXX_FLAGS_RELEASE "-O3" "-Wno-unused")
endif()

if(CMAKE_CXX_COMPILER_ID MATCHES Clang)
  list(APPEND CMAKE_CXX_FLAGS "-fno-rtti" "-fno-exceptions" "-Qunused-arguments" "-fcolor-diagnostics")
  list(APPEND CMAKE_CXX_FLAGS_DEBUG "-Wdocumentation")
  list(APPEND CMAKE_CXX_FLAGS_RELEASE "-O3" "-Wno-unused")
endif()

更细粒度的方法是,不修改 CMAKE_<LANG>_FLAGS_<CONFIG> 变量,而是定义特定的标志列表:

set(CXX_FLAGS)
set(CXX_FLAGS_DEBUG)
set(CXX_FLAGS_RELEASE)

if(CMAKE_CXX_COMPILER_ID MATCHES GNU)
  list(APPEND CXX_FLAGS "-fno-rtti" "-fno-exceptions")
  list(APPEND CXX_FLAGS_DEBUG "-Wsuggest-final-types" "-Wsuggest-final-methods" "-Wsuggest-override")
  list(APPEND CXX_FLAGS_RELEASE "-O3" "-Wno-unused")
endif()

if(CMAKE_CXX_COMPILER_ID MATCHES Clang)
  list(APPEND CXX_FLAGS "-fno-rtti" "-fno-exceptions" "-Qunused-arguments" "-fcolor-diagnostics")
  list(APPEND CXX_FLAGS_DEBUG "-Wdocumentation")
  list(APPEND CXX_FLAGS_RELEASE "-O3" "-Wno-unused")
endif()

稍后,使用生成器表达式来设置编译器标志,在此基础上为每个配置和每个目标生成构建系统:

target_compile_options(compute-areas
  PRIVATE
  ${CXX_FLAGS}
  "$<$<CONFIG:Debug>:${CXX_FLAGS_DEBUG}>"
  "$<$<CONFIG:Release>:${CXX_FLAGS_RELEASE}>"
)

当前示例中展示了这两种方法,我们推荐后者(特定于项目的变量和 target_compile_options())。

两种方法都有效,并在许多项目中得到广泛应用。不过,每种方式都有缺点。CMAKE_<LANG>_COMPILER_ID 不能保证为所有编译器都定义。此外,一些标志可能会被弃用,或者在编译器的较晚版本中引入。与 CMAKE_<LANG>_COMPILER_ID 类似,CMAKE_<LANG>_COMPILER_VERSION 变量不能保证为所有语言和供应商都提供定义。尽管检查这些变量的方式非常流行,但我们认为更健壮的替代方法是检查所需的标志集是否与给定的编译器一起工作,这样项目中实际上只使用有效的标志。结合特定于项目的变量、target_compile_options() 和生成器表达式,会让解决方案变得非常强大。后面的示例中,我们将展示如何使用 check-and-set 模式。

系列导航


   转载规则


《CMake 系列(七):设置编译器选项》 由 吴杭沉 采用 知识共享署名 4.0 国际许可协议 进行许可。
 上一篇
CMake 系列(八):脚本控制流 CMake 系列(八):脚本控制流
本章前面的示例中,已经使用过 if-else-endif。CMake 还提供了创建循环的语言工具:foreach-endforeach 和 while-endwhile。两者都可以与 break() 结合使用,以便尽早从循环中跳出。本示例将
2022-05-03
下一篇 
CMake 系列(六):切换构建类型 CMake 系列(六):切换构建类型
CMake 可以配置构建类型,例如 Debug、Release 等。配置时,可以为 Debug 或 Release 构建设置相关的选项或属性,例如编译器和链接器标志。控制生成构建系统使用的配置变量是 CMAKE_BUILD_TYPE。该变量
2022-04-19
  目录