1.8 设置编译器选项

NOTE:此示例代码可以在 https://github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-01/recipe-08 中找到,有一个C++示例。该示例在CMake 3.5版(或更高版本)中是有效的,并且已经在GNU/Linux、macOS和Windows上进行过测试。

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

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

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

准备工作

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

  1. #include "geometry_circle.hpp"
  2. #include "geometry_polygon.hpp"
  3. #include "geometry_rhombus.hpp"
  4. #include "geometry_square.hpp"
  5. #include <cstdlib>
  6. #include <iostream>
  7. int main() {
  8. using namespace geometry;
  9. double radius = 2.5293;
  10. double A_circle = area::circle(radius);
  11. std::cout << "A circle of radius " << radius << " has an area of " << A_circle
  12. << std::endl;
  13. int nSides = 19;
  14. double side = 1.29312;
  15. double A_polygon = area::polygon(nSides, side);
  16. std::cout << "A regular polygon of " << nSides << " sides of length " << side
  17. << " has an area of " << A_polygon << std::endl;
  18. double d1 = 5.0;
  19. double d2 = 7.8912;
  20. double A_rhombus = area::rhombus(d1, d2);
  21. std::cout << "A rhombus of major diagonal " << d1 << " and minor diagonal " << d2
  22. << " has an area of " << A_rhombus << std::endl;
  23. double l = 10.0;
  24. double A_square = area::square(l);
  25. std::cout << "A square of side " << l << " has an area of " << A_square
  26. << std::endl;
  27. return EXIT_SUCCESS;
  28. }

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

  1. .
  2. ├─ CMakeLists.txt
  3. ├─ compute-areas.cpp
  4. ├─ geometry_circle.cpp
  5. ├─ geometry_circle.hpp
  6. ├─ geometry_polygon.cpp
  7. ├─ geometry_polygon.hpp
  8. ├─ geometry_rhombus.cpp
  9. ├─ geometry_rhombus.hpp
  10. ├─ geometry_square.cpp
  11. └─ geometry_square.hpp

我们不会为所有文件提供清单,读者可以参考 https://github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-01/recipe-08

具体实施

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

  1. 设置CMake的最低版本:

    1. cmake_minimum_required(VERSION 3.5 FATAL_ERROR)
  2. 声明项目名称和语言:

    1. project(recipe-08 LANGUAGES CXX)
  3. 然后,打印当前编译器标志。CMake将对所有C++目标使用这些:

    1. message("C++ compiler flags: ${CMAKE_CXX_FLAGS}")
  4. 为目标准备了标志列表,其中一些将无法在Windows上使用:

    1. list(APPEND flags "-fPIC" "-Wall")
    2. if(NOT WIN32)
    3. list(APPEND flags "-Wextra" "-Wpedantic")
    4. endif()
  5. 添加了一个新的目标——geometry库,并列出它的源依赖关系:

    1. add_library(geometry
    2. STATIC
    3. geometry_circle.cpp
    4. geometry_circle.hpp
    5. geometry_polygon.cpp
    6. geometry_polygon.hpp
    7. geometry_rhombus.cpp
    8. geometry_rhombus.hpp
    9. geometry_square.cpp
    10. geometry_square.hpp
    11. )
  6. 为这个库目标设置了编译选项:

    1. target_compile_options(geometry
    2. PRIVATE
    3. ${flags}
    4. )
  7. 然后,将生成compute-areas可执行文件作为一个目标:

    1. add_executable(compute-areas compute-areas.cpp)
  8. 还为可执行目标设置了编译选项:

    1. target_compile_options(compute-areas
    2. PRIVATE
    3. "-fPIC"
    4. )
  9. 最后,将可执行文件链接到geometry库:

    1. target_link_libraries(compute-areas geometry)

如何工作

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

可见性的含义如下:

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

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

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

  1. $ mkdir -p build
  2. $ cd build
  3. $ cmake ..
  4. $ cmake --build . -- VERBOSE=1
  5. ... lots of output ...
  6. [ 14%] Building CXX object CMakeFiles/geometry.dir/geometry_circle.cpp.o
  7. /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
  8. [ 28%] Building CXX object CMakeFiles/geometry.dir/geometry_polygon.cpp.o
  9. /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
  10. [ 42%] Building CXX object CMakeFiles/geometry.dir/geometry_rhombus.cpp.o
  11. /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
  12. [ 57%] Building CXX object CMakeFiles/geometry.dir/geometry_square.cpp.o
  13. /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
  14. ... more output ...
  15. [ 85%] Building CXX object CMakeFiles/compute-areas.dir/compute-areas.cpp.o
  16. /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
  17. ... more output ...

输出确认编译标志,确认指令设置正确。

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

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

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

也可以使用全局标志,可以使用CMakeLists.txt运行以下命令:

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

这将使用-fno-rtti - fpic - wall - Wextra - wpedantic配置geometry目标,同时使用-fno exception -fno-rtti - fpic配置compute-areas

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

更多信息

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

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

  1. if(CMAKE_CXX_COMPILER_ID MATCHES GNU)
  2. list(APPEND CMAKE_CXX_FLAGS "-fno-rtti" "-fno-exceptions")
  3. list(APPEND CMAKE_CXX_FLAGS_DEBUG "-Wsuggest-final-types" "-Wsuggest-final-methods" "-Wsuggest-override")
  4. list(APPEND CMAKE_CXX_FLAGS_RELEASE "-O3" "-Wno-unused")
  5. endif()
  6. if(CMAKE_CXX_COMPILER_ID MATCHES Clang)
  7. list(APPEND CMAKE_CXX_FLAGS "-fno-rtti" "-fno-exceptions" "-Qunused-arguments" "-fcolor-diagnostics")
  8. list(APPEND CMAKE_CXX_FLAGS_DEBUG "-Wdocumentation")
  9. list(APPEND CMAKE_CXX_FLAGS_RELEASE "-O3" "-Wno-unused")
  10. endif()

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

  1. set(COMPILER_FLAGS)
  2. set(COMPILER_FLAGS_DEBUG)
  3. set(COMPILER_FLAGS_RELEASE)
  4. if(CMAKE_CXX_COMPILER_ID MATCHES GNU)
  5. list(APPEND CXX_FLAGS "-fno-rtti" "-fno-exceptions")
  6. list(APPEND CXX_FLAGS_DEBUG "-Wsuggest-final-types" "-Wsuggest-final-methods" "-Wsuggest-override")
  7. list(APPEND CXX_FLAGS_RELEASE "-O3" "-Wno-unused")
  8. endif()
  9. if(CMAKE_CXX_COMPILER_ID MATCHES Clang)
  10. list(APPEND CXX_FLAGS "-fno-rtti" "-fno-exceptions" "-Qunused-arguments" "-fcolor-diagnostics")
  11. list(APPEND CXX_FLAGS_DEBUG "-Wdocumentation")
  12. list(APPEND CXX_FLAGS_RELEASE "-O3" "-Wno-unused")
  13. endif()

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

  1. target_compile_option(compute-areas
  2. PRIVATE
  3. ${CXX_FLAGS}
  4. "$<$<CONFIG:Debug>:${CXX_FLAGS_DEBUG}>"
  5. "$<$<CONFIG:Release>:${CXX_FLAGS_RELEASE}>"
  6. )

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

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