CMake 秘籍(一)(5)

简介: CMake 秘籍(一)

CMake 秘籍(一)(4)https://developer.aliyun.com/article/1524608

还有更多

大多数情况下,标志是编译器特定的。我们当前的示例仅适用于 GCC 和 Clang;其他供应商的编译器将不理解许多,如果不是全部,这些标志。显然,如果一个项目旨在真正跨平台,这个问题必须解决。有三种方法可以解决这个问题。

最典型的方法是将所需的一组编译器标志附加到每个配置类型的 CMake 变量,即 CMAKE__FLAGS_。这些标志设置为已知适用于给定编译器供应商的内容,因此将包含在

if-endif 子句检查 CMAKE__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__FLAGS_ 变量,而是定义项目特定的标志列表:

set(COMPILER_FLAGS)
set(COMPILER_FLAGS_DEBUG)
set(COMPILER_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_option(compute-areas
  PRIVATE
    ${CXX_FLAGS}
    "$<$<CONFIG:Debug>:${CXX_FLAGS_DEBUG}>"
    "$<$<CONFIG:Release>:${CXX_FLAGS_RELEASE}>"
  )

我们在当前的配方中展示了这两种方法,并明确推荐后者(项目特定变量和 target_compile_options())而不是前者(CMake 变量)。

这两种方法都有效,并在许多项目中广泛使用。然而,它们也有缺点。正如我们已经提到的,CMAKE__COMPILER_ID并不保证为所有编译器供应商定义。此外,某些标志可能会被弃用,或者可能在编译器的较新版本中引入。与CMAKE__COMPILER_ID类似,CMAKE__COMPILER_VERSION变量并不保证为所有语言和供应商定义。尽管检查这些变量非常流行,但我们认为更稳健的替代方案是检查给定编译器是否支持所需的标志集,以便仅在项目中实际使用有效的标志。结合使用项目特定变量、target_compile_options和生成器表达式,这种方法非常强大。我们将在第 3 个示例中展示如何使用这种检查和设置模式,即第七章中的“编写一个函数来测试和设置编译器标志”。

设置语言标准

本示例代码可在github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-01/recipe-09获取,包含 C++和 Fortran 示例。本示例适用于 CMake 版本 3.5(及更高版本),并在 GNU/Linux、macOS 和 Windows 上进行了测试。

编程语言有不同的标准可供选择,即提供新改进语言结构的不同版本。启用新标准是通过设置适当的编译器标志来实现的。我们在前面的示例中展示了如何做到这一点,无论是针对特定目标还是全局设置。CMake 3.1 版本引入了针对 C++和 C 语言标准的平台和编译器无关机制:为目标设置_STANDARD属性。

准备工作

对于以下示例,我们将要求 C++编译器符合 C++14 标准或更高版本。本示例代码定义了一个动物的多态层次结构。我们在层次结构的基类中使用std::unique_ptr

std::unique_ptr<Animal> cat = Cat("Simon");
std::unique_ptr<Animal> dog = Dog("Marlowe);

我们没有明确使用各种子类型的构造函数,而是使用工厂方法的实现。工厂使用 C++11 的可变参数模板实现。它保存了继承层次结构中每个对象的创建函数映射:

typedef std::function<std::unique_ptr<Animal>(const std::string &)> CreateAnimal;

它根据预先分配的标签进行分派,以便对象的创建将如下所示:

std::unique_ptr<Animal> simon = farm.create("CAT", "Simon");
std::unique_ptr<Animal> marlowe = farm.create("DOG", "Marlowe");

在工厂使用之前,将标签和创建函数注册到工厂:

Factory<CreateAnimal> farm;
farm.subscribe("CAT", [](const std::string & n) { return std::make_unique<Cat>(n); });
farm.subscribe("DOG", [](const std::string & n) { return std::make_unique<Dog>(n); });

我们使用 C++11 的lambda函数定义创建函数。注意使用std::make_unique来避免引入裸new操作符。这个辅助函数是在 C++14 中引入的。

此 CMake 功能是在版本 3.1 中添加的,并且一直在不断发展。CMake 的后续版本为 C++标准的后续版本和不同的编译器提供了越来越好的支持。我们建议您检查您的首选编译器是否受支持,请访问文档网页:cmake.org/cmake/help/latest/manual/cmake-compile-features.7.html#supported-compilers

如何做到这一点

我们将逐步构建 CMakeLists.txt 并展示如何要求特定的标准(在本例中为 C++14):

  1. 我们声明了所需的最低 CMake 版本、项目名称和语言:
cmake_minimum_required(VERSION 3.5 FATAL_ERROR)
project(recipe-09 LANGUAGES CXX)
  1. 我们要求在 Windows 上导出所有库符号:
set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)
  1. 我们需要为库添加一个目标。这将编译源代码成一个共享库:
add_library(animals
  SHARED
    Animal.cpp
    Animal.hpp
    Cat.cpp
    Cat.hpp
    Dog.cpp
    Dog.hpp
    Factory.hpp
  )
  1. 现在我们为目标设置 CXX_STANDARDCXX_EXTENSIONSCXX_STANDARD_REQUIRED 属性。我们还设置了 POSITION_INDEPENDENT_CODE 属性,以避免在某些编译器上构建 DSO 时出现问题:
set_target_properties(animals
  PROPERTIES
    CXX_STANDARD 14
    CXX_EXTENSIONS OFF
    CXX_STANDARD_REQUIRED ON
    POSITION_INDEPENDENT_CODE 1
  )
  1. 然后,我们为 animal-farm 可执行文件添加一个新的目标并设置其属性:
add_executable(animal-farm animal-farm.cpp)
set_target_properties(animal-farm
  PROPERTIES
    CXX_STANDARD 14
    CXX_EXTENSIONS OFF
    CXX_STANDARD_REQUIRED ON
  )
  1. 最后,我们将可执行文件链接到库:
target_link_libraries(animal-farm animals)
  1. 让我们也检查一下我们的例子中的猫和狗有什么要说的:
$ mkdir -p build
$ cd build
$ cmake ..
$ cmake --build .
$ ./animal-farm
I'm Simon the cat!
I'm Marlowe the dog!

它是如何工作的

在步骤 4 和 5 中,我们为 animalsanimal-farm 目标设置了一系列属性:

  • CXX_STANDARD 规定了我们希望采用的标准。
  • CXX_EXTENSIONS 告诉 CMake 只使用将启用 ISO C++标准的编译器标志,而不使用编译器特定的扩展。
  • CXX_STANDARD_REQUIRED 指定所选标准版本是必需的。如果该版本不可用,CMake 将以错误停止配置。当此属性设置为 OFF 时,CMake 将查找下一个最新的标准版本,直到设置了适当的标志。这意味着首先查找 C++14,然后是 C++11,然后是 C++98。

在撰写本文时,还没有 Fortran_STANDARD 属性可用,但可以使用 target_compile_options 设置标准;请参阅 github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-01/recipe-09

如果语言标准是所有目标共享的全局属性,您可以将 CMAKE__STANDARDCMAKE__EXTENSIONSCMAKE__STANDARD_REQUIRED 变量设置为所需值。所有目标上的相应属性将使用这些值进行设置。

还有更多

CMake 通过引入编译特性的概念,提供了对语言标准的更精细控制。这些特性是由语言标准引入的,例如 C++11 中的可变参数模板和 lambda,以及 C++14 中的自动返回类型推导。您可以通过target_compile_features()命令要求特定目标支持某些特性,CMake 会自动为该标准设置正确的编译器标志。CMake 还可以为可选的编译器特性生成兼容性头文件。

我们建议阅读cmake-compile-features的在线文档,以全面了解 CMake 如何处理编译特性和语言标准:cmake.org/cmake/help/latest/manual/cmake-compile-features.7.html

使用控制流结构

本食谱的代码可在github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-01/recipe-10找到,并附有一个 C++示例。该食谱适用于 CMake 版本 3.5(及以上),并在 GNU/Linux、macOS 和 Windows 上进行了测试。

在本章之前的食谱中,我们已经使用了if-elseif-endif结构。CMake 还提供了创建循环的语言设施:foreach-endforeachwhile-endwhile。两者都可以与break结合使用,以提前从封闭循环中跳出。本食谱将向您展示如何使用foreach遍历源文件列表。我们将对一组源文件应用这样的循环,以降低编译器优化,而不引入新的目标。

准备就绪

我们将重用本章第 8 个食谱中引入的geometry示例,控制编译器标志。我们的目标是通过将它们收集到一个列表中,对一些源文件的编译器优化进行微调。

如何操作

以下是在CMakeLists.txt中需要遵循的详细步骤:

  1. 与第 8 个食谱,控制编译器标志一样,我们指定了所需的最低 CMake 版本、项目名称和语言,并声明了geometry库目标:
cmake_minimum_required(VERSION 3.5 FATAL_ERROR)
project(recipe-10 LANGUAGES CXX)
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
  )
  1. 我们决定以-O3编译器优化级别编译库。这作为目标的PRIVATE编译选项设置:
target_compile_options(geometry
  PRIVATE
    -O3
  )
  1. 然后,我们生成一份需要以较低优化级别编译的源文件列表:
list(
  APPEND sources_with_lower_optimization
    geometry_circle.cpp
    geometry_rhombus.cpp
  )
  1. 我们遍历这些源文件,将它们的优化级别调整至-O2。这是通过使用它们的源文件属性来完成的:
message(STATUS "Setting source properties using IN LISTS syntax:")
foreach(_source IN LISTS sources_with_lower_optimization)
  set_source_files_properties(${_source} PROPERTIES COMPILE_FLAGS -O2)
  message(STATUS "Appending -O2 flag for ${_source}")
endforeach()
  1. 为了确保设置了源属性,我们再次遍历并打印每个源的COMPILE_FLAGS属性:
message(STATUS "Querying sources properties using plain syntax:")
foreach(_source ${sources_with_lower_optimization})
  get_source_file_property(_flags ${_source} COMPILE_FLAGS)
  message(STATUS "Source ${_source} has the following extra COMPILE_FLAGS: ${_flags}")
endforeach()
  1. 最后,我们添加了compute-areas可执行目标,并将其与geometry库链接:
add_executable(compute-areas compute-areas.cpp)
target_link_libraries(compute-areas geometry)
  1. 让我们验证在配置步骤中标志是否正确设置:
$ mkdir -p build
$ cd build
$ cmake ..
...
-- Setting source properties using IN LISTS syntax:
-- Appending -O2 flag for geometry_circle.cpp
-- Appending -O2 flag for geometry_rhombus.cpp
-- Querying sources properties using plain syntax:
-- Source geometry_circle.cpp has the following extra COMPILE_FLAGS: -O2
-- Source geometry_rhombus.cpp has the following extra COMPILE_FLAGS: -O2
  1. 最后,使用VERBOSE=1检查构建步骤。您将看到-O2标志被附加到-O3标志上,但最终的优化级别标志(在这种情况下是-O2)“胜出”:
$ cmake --build . -- VERBOSE=1

它是如何工作的

foreach-endforeach语法可以用来表达对一组变量的重复任务。在我们的例子中,我们使用它来操作、设置和获取项目中特定文件的编译器标志。这个 CMake 代码片段引入了两个额外的新的命令:

  • set_source_files_properties(file PROPERTIES property value),它为给定文件设置属性到传递的值。与目标类似,文件在 CMake 中也有属性。这允许对构建系统生成进行极其精细的控制。源文件可用属性的列表可以在这里找到:cmake.org/cmake/help/v3.5/manual/cmake-properties.7.html#source-file-properties
  • get_source_file_property(VAR file property),它检索给定文件的所需属性的值,并将其存储在 CMake 的VAR变量中。

在 CMake 中,列表是由分号分隔的字符串组。列表可以通过list命令或set命令创建。例如,set(var a b c d e)list(APPEND a b c d e)都创建了列表a;b;c;d;e

为了降低一组文件的优化级别,将它们收集到一个单独的目标(库)中,并为该目标显式设置优化级别,而不是附加一个标志,这可能更清晰。但在本例中,我们的重点是foreach-endforeach

还有更多

foreach()构造可以以四种不同的方式使用:

  • foreach(loop_var arg1 arg2 ...):提供了一个循环变量和一个明确的项列表。当打印sources_with_lower_optimization中项的编译器标志集时,使用了这种形式。请注意,如果项列表在一个变量中,它必须被显式展开;也就是说,必须将${sources_with_lower_optimization}作为参数传递。
  • 作为对整数的循环,通过指定一个范围,例如foreach(loop_var RANGE total),或者替代地
    foreach(loop_var RANGE start stop [step])
  • 作为对列表值变量的循环,例如foreach(loop_var IN LISTS [list1 [...]])。参数被解释为列表,并且它们的内含物会自动相应地展开。
  • 作为对项的循环,例如foreach(loop_var IN ITEMS [item1 [...]])。参数的内容不会展开。
相关文章
|
15天前
|
消息中间件 Unix C语言
CMake 秘籍(二)(5)
CMake 秘籍(二)
18 1
|
15天前
|
编译器 测试技术 开发工具
CMake 秘籍(八)(4)
CMake 秘籍(八)
15 0
|
15天前
|
XML 监控 Linux
CMake 秘籍(七)(4)
CMake 秘籍(七)
28 0
|
15天前
|
编译器 开发工具
CMake 秘籍(八)(2)
CMake 秘籍(八)
15 0
|
15天前
|
Linux C++ iOS开发
CMake 秘籍(四)(1)
CMake 秘籍(四)
11 0
|
15天前
|
Linux iOS开发 C++
CMake 秘籍(六)(3)
CMake 秘籍(六)
17 1
|
15天前
|
并行计算 Unix 编译器
CMake 秘籍(七)(5)
CMake 秘籍(七)
33 0
|
15天前
|
编译器 Linux C++
CMake 秘籍(二)(1)
CMake 秘籍(二)
18 0
|
15天前
|
并行计算 关系型数据库 编译器
CMake 秘籍(七)(3)
CMake 秘籍(七)
29 0
|
15天前
|
Linux 编译器 C++
CMake 秘籍(七)(2)
CMake 秘籍(七)
24 1

热门文章

最新文章