第15章:语言要求

随着C和C++语言的不断发展,开发人员越来越需要理解支持使用C和/或C++的编译器和链接器标志。不同的编译器使用不同的标志,但即使使用相同的编译器和链接器,也可以使用标志来选择不同的标准库实现。

C++11支持相对较新的特性,CMake还没有直接支持选择使用哪种标准,所以项目只能自己添加所需的标志。CMake 3.1中,引入了一些特性,允许以一致和方便的方式选择C和C++标准,并抽象出各种编译器和链接器的差异。这种支持在后续版本中得到了扩展,从CMake 3.6开始,涵盖了最常见的编译器(CMake 3.2添加了大部分编译器支持,3.6添加了Intel编译器)。

CMake为指定语言提供了两种主要方法。第一个是直接设置语言标准,第二个是允许项目指定它们需要的语言特性,并让CMake选择适当的语言标准。虽然功能主要是由C和C++语言驱动的,但其他语言和伪语言(如CUDA)也可以支持。

15.1. 直接设定语言标准

项目控制构建所使用的语言标准的最简单方法是直接设置它们,使用这种方法,开发人员不需要知道或指定代码使用的语言特性,只需要设置一个数字,表示代码假设支持的标准。这不仅容易理解和使用,还相对简单,可以确保在整个项目中使用相同的标准。这在链接阶段变得很重要,在这个阶段,应该在所有链接库和目标文件之间使用一致的标准库。

与CMake的常见模式一样,目标属性用于控制构建目标源以及连接可执行或共享库时使用哪一种标准。对于给定的语言,有三个与指定标准相关的目标属性(必须是C或CXX中的一个,对于新的CMake版本,CUDA也是一个选项):

<LANG>_STANDARD

将项目指定为目标使用的语言标准。支持此特性的第一个CMake版本中,CSTANDARD的有效值为90、99或11,而CXX_STANDARD的有效值为98、11或14。CMake 3.8中,也支持17;CMake 3.12中,可以使用20。人们有理由认为,以后的CMake版本会随着时间的推移增加对其他语言标准的支持。CMake 3.8还支持值为98或11的CUDA_STANDARD,CXX_STANDARD通常会控制的CUDA特定版本。当创建一个目标时,该属性的初始值可从`CMAKE_STANDARD`变量中获取。

<LANG>_STANDARD_REQUIRED

<LANG>_STANDARD属性指定了项目语言标准时,<LANG>_STANDARD_REQUIRED决定了该语言标准是作为最低要求,还是仅仅作为“可用时使用”的标准。人们可能会直观地认为<LANG>_STANDARD默认是必需的,而<LANG>_STANDARD_REQUIRED属性默认是关闭的。当关闭时,如果编译器不支持请求的标准,CMake将把请求回退到更早的标准,而不是报错终止。这种回退的行为通常是新开发人员无法预估的,可能会造成混乱。因此,对于大多数项目来说,当指定<LANG>_STANDARD属性时,需要将其对应的<LANG>_STANDARD_REQUIRED属性设置为true,以确保特定要求的标准会视为一个严格的要求。当创建一个目标时,该属性的初始值会从CMAKE_<LANG>_STANDARD_REQUIRED变量中获取。

<LANG>_EXTENSIONS

许多编译器都支持对语言标准的扩展,通常会提供一个编译器和/或链接器标志来启用或禁用这些扩展。<LANG>_EXTENSIONS目标属性就是控制是否启用这些特定的扩展。对于一些编译器/连接器,该设置可以改变目标与标准库(见下面的例子)。请注意,对于许多编译器/链接器,可以使用相同的标志来控制语言标准和是否启用扩展。如果一个项目设置了<LANG>_EXTENSIONS属性,也应该设置<LANG>_STANDARD属性,否则<LANG>_EXTENSIONS可能会忽略。创建目标时,<LANG>_EXTENSIONS属性的初始值从CMAKE_<LANG>_EXTENSIONS变量中获取。

在实践中,项目通常会设置为上述目标属性提供的默认值,而不是直接设置目标属性。这确保了项目中的所有目标都以一致的方式生成。此外,强烈建议项目设置所有这三个属性/变量。对于开发人员来说,<LANG>_STANDARD_REQUIRED<LANG>_EXTENSIONS的默认值相对来说不是很直观,因此通过显式地设置它们,项目可以清楚地知道期望的标准行为是什么。几个示例可以帮助演示其用法:

# Require C++11 and disable extensions for all targets
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

当使用GCC或Clang时,上面的代码通常会添加-std=c++11标志。它还可以根据平台添加链接器标记,如-stdlib=libc++。针对Visual Studio编译器VS2015 Update 3之前,如果编译器不支持C++11则不会有标示添加。还要注意的是,在Visual Studio 15 Update 3中,编译器支持指定C++标准,但仅适用于C++14及以后版本,C++14是默认设置。

相比之下,下面的示例请求一个更新的C++版本,并启用了编译器扩展,结果生成了一个GCC/Clang编译器标记,如-std=gnu++14。Visual Studio编译器在默认情况下也支持所要求的标准,这取决于编译器的版本。如果正在使用的编译器不支持所要求的C++标准,CMake将配置编译器使用它所支持的最新C++标准。

# Use C++14 if available and allow compiler extensions for all targets
set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_STANDARD_REQUIRED OFF)
set(CMAKE_CXX_EXTENSIONS ON)

C的情况非常类似。下面的例子展示了如何设置C标准的详细信息,这次只针对特定的目标:

# Build target foo with C99, no compiler extensions
set_target_properties(foo PROPERTIES
 C_STANDARD 99
 C_STANDARD_REQUIRED ON
 C_EXTENSIONS OFF
)

应该注意的是,<LANG>_STANDARD指定了一个最低标准,不一定是一个确切的要求。某些情况下,CMake可能会选择一个近期标准来满足编译特性的需求(下面将讨论)。

15.2. 根据特性需求设置语言标准

直接为一个目标或整个项目设置语言标准是管理标准需求的最简单方法,当项目开发人员知道哪个语言版本提供项目代码所使用的特性时,这是最合适的方法。当使用大量语言特性时,这种方式特别方便,因为每个特性都不必显式地指定。然而,在某些情况下,开发人员可能更喜欢声明代码使用的语言特性,而让CMake选择适当的语言标准。与直接指定标准不同,这样做还有一个额外的好处,即编译特性可以成为目标接口的一部分,因此可以在链接到它的其他目标上强制执行。

编译特性需求由目标属性COMPILEFEATURES和INTERFACE_COMPILE_FEATURES控制,但是这些属性通常使用target_compile_features()命令填充,而不是直接操作。这个命令的形式与CMake提供的target…()命令非常相似:

target_compile_features(targetName
 <PRIVATE|PUBLIC|INTERFACE> feature1 [feature2 ...]
 [<PRIVATE|PUBLIC|INTERFACE> feature3 [feature4 ...]]
 ...
)

PRIVATE、PUBLIC和INTERFACE关键字有它们通常的含义,控制着特性应该如何应用。PRIVATE特性填充COMPILE_FEATURES属性,就仅应用于目标本身。使用INTERFACE关键字指定的那些特性填充INTERFACE_COMPILE_FEATURES属性,该属性会应用于链接到targetName的目标。

指定为PUBLIC的特性将添加到这两个属性中,因此将应用到目标本身,以及链接到它的其他目标中。

每个特性必须是底层编译器支持的特性。CMake提供了两个已知特性列表:CMAKE_<LANG>_KNOWN_FEATURES,包含该语言所有已知特性;CMAKE_<LANG>_COMPILE_FEATURES,包含编译器支持的特性。如果编译器不支持所请求的特性,CMake将报告错误。开发人员可以在CMake文档中找到CMAKE_<LANG>_KNOWN_FEATURES变量 ,它不仅列出了特定版本的CMake所理解的特性,还包含了对每个特性相关的标准文档的引用。请注意,并不是所有的功能都提供了特定的语言版本可以显式地指定使用编译功能。例如,新的C++ STL类型、函数等没有相关的特性。

CMake 3.8中,每种语言的元特性可用来表示特定的语言标准,而不是特定的编译特性。这些元特性采用<lang>_std_<value>的形式,当列为必需的编译特性时,CMake将确保使用支持该语言标准的编译器标志。例如:添加一个编译特性,确保目标和任何链接到它的支持C++14,可以使用以下功能:

target_compile_features(targetName PUBLIC cxx_std_14)

如果项目需要支持比3.8更早的CMake版本,那么上面的元特性将不可用。这种,每个编译特性都必须单独列出,这是不实际的,也是不完整的。这通常会限制编译特性的有用性,因为项目通常选择通过前一节中描述的目标属性来设置语言标准。

当一个目标同时设置了它的<LANG>_STANDARD属性并指定了编译特性(直接或传递接口特性的结果)时,CMake将强制执行更强的标准要求。在下面的例子中,foo用C++14构建,bar用C++17构建,guff用C++14构建:

set_target_properties(foo PROPERTIES CXX_STANDARD 11)
target_compile_features(foo PUBLIC cxx_std_14)

set_target_properties(bar PROPERTIES CXX_STANDARD 17)
target_compile_features(bar PRIVATE cxx_std_11)

set_target_properties(guff PROPERTIES CXX_STANDARD 11)
target_link_libraries(guff PRIVATE foo)

请注意,这意味着可以使用比项目预期的更最新的语言标准,在某些情况下可能会导致编译错误。例如,C++17删除了std::auto_ptr,所以如果代码希望用较旧的语言标准进行编译,仍然使用std::auto_ptr时,如果工具链严格执行删除操作,则无法进行编译。

15.2.1. 检测和使用可选语言特性

有些项目能够处理支持或不支持的语言特性。例如:如果编译器支持它们,可能提供一个示例,或者只定义某些函数重载。项目可能支持一些可选的编译器特性,例如:用于指导开发人员的关键字,或为编译器提供捕获常见错误的增强性关键字。像final和override这样的C++关键字就是常见的例子。

CMake提供了许多处理上述场景的方法。一种方法是使用生成器表达式,根据特定编译器特性的可用性,有条件地设置编译器定义或包含目录。这些可能有点冗长,但提供了极大的灵活性,并基于特性的功能支持非常精确地处理。考虑下面的例子:

add_library(foo ...)

# Make override a feature requirement only if available
target_compile_features(foo PUBLIC
 $<$<COMPILE_FEATURES:cxx_override>:cxx_override>
)

# Define the foo_OVERRIDE symbol so it provides the
# override keyword if available or empty otherwise
target_compile_definitions(foo PUBLIC
 $<$<COMPILE_FEATURES:cxx_override>:-Dfoo_OVERRIDE=override>
 $<$<NOT:$<COMPILE_FEATURES:cxx_override>>:-Dfoo_OVERRIDE>
)

上面的代码将允许如下代码为任何C++编译器编译,不管它是否支持override关键字:

class MyClass : public Base
{
public:
 void func() foo_OVERRIDE;
 ...
};

除了override关键字之外,许多其他特性也可以以相同的方式使用条件定义符。C++关键字,如final, constexpr, noexcept等能都可以使用,从而产生有效和正确的代码。其他关键字,如nullptr和static_assert,如果编译器不支持,则可以使用其他实现。为每个特性指定生成器表达式来覆盖受支持和不受支持的情况将是冗长乏味的,但CMake通过模块系统提供了一种更方便的机制。WriteCompilerDetectionHeader模块定义了一个名为write_compiler_detection_header()的函数,该函数可以自动进行此类处理。它生成一个头文件,项目的源代码可以#include该头文件,以获取适当指定的编译器定义。该函数的简化版本如下所示,只显示强制选项。

write_compiler_detection_header(
 FILE fileName
 PREFIX prefix
 COMPILERS compiler1 [compiler2 ...]
 FEATURES feature1 [feature2 ...]
)

该函数将向指定的文件名输出一个C/C++头文件,其中的内容将列出的每个特性定义适当的宏。每个特性都有一个宏,其形式为prefix_COMPILER_UPPERCASEFEATURE,其值将为1或0,具体取决于所使用的编译器是否支持该特性。一些特性还可能具有prefix_UPPERCASEFEATURE形式的宏,它为每个编译器提供该特性最合适的实现,包括编译器的不同版本。

考虑一个C++项目,它可以使用override、final和nullptr关键字(如果可用的话),目标是支持GNU、Clang、Visual Studio和Intel编译器在这些编译器支持的任何平台上。如果编译器支持右值引用,项目还将定义move构造函数。下面将在构建目录中写出一个名为foocompiler_detectionh的头文件,并以字符串foo作为每个宏的前缀:

include(WriteCompilerDetectionHeader)
write_compiler_detection_header(
 FILE foo_compiler_detection.h
 PREFIX foo
 COMPILERS GNU Clang MSVC Intel
 FEATURES cxx_override
 cxx_final
 cxx_nullptr
 cxx_rvalue_references
)

C++代码使用上面的宏定义可能看起来像这样:

#include "foo_compiler_detection.h"

class MyClass foo_FINAL : public Base
{
public:
#if foo_COMPILER_CXX_RVALUE_REFERENCES
 MyClass(MyClass&& c);
#endif
 void func1() foo_OVERRIDE;
 void func2(int* p = foo_NULLPTR);
};

使用上述源文件的目标仍需要选择适当的语言标准,这种情况下,由于后退实现可用,所需的标准可以指定,但不是必需的:

set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED OFF)
set(CMAKE_CXX_EXTENSIONS OFF)

add_library(foo MyClass.cpp)

# The header is written to the build directory
# so ensure we add that to the header search path
target_include_directories(foo
 PUBLIC "${CMAKE_CURRENT_BINARY_DIR}"
)

CMake为相当多的特性提供了后退实现,所有这些在WriteCompilerDetectionHeader模块文档中都有描述。write_compiler_detection_header()命令还接受许多没有提到的可选参数,这些参数可以控制生成的头文件的结构和位置,并在生成的头文件的开始和结束处添加任意内容。感兴趣的读者可以参考CMake模块文档,以获得完整的细节。

深入使用WriteCompilerDetectionHeader模块之前,项目应该仔细考虑使用编译器检测头文件是否值得。它可以扩展项目,是支持的编译器范围的优秀工具。特别是,长期存在的项目可能会发现,在某些平台上仍然需要支持较老的编译器时,它是一个有用的垫脚石,当然也可以将代码库更新为最新的语言特性。使用该模块的主要缺点是可能会降低源代码的可读性。强制所有源文件使用替代(生成的)符号名而不是标准语言关键字也很困难,因为这对一些开发人员来说不大能想得到。

15.3. 推荐

项目应该避免直接设置编译器和链接器标志,来控制所使用的语言标准。所需的标志因编译器的不同而不同,因此使用CMake提供的特性更健壮、更可维护、更方便,并允许适当地填充。CMakeLists.txt文件也将更清楚地表达意图,而不是通常晦涩难懂的编译器和链接器标志。

控制语言标准最简单的方法是使用CMAKE_<LANG>_STANDARDCMAKE_<LANG>_STANDARD_REQUIREDCMAKE_<LANG>_EXTENSIONS变量。这些可用于为整个项目设置语言标准,确保所有目标的标准一致。这可以帮助避免链接不一致的标准库和其他链接问题。理想情况下,应该在顶级CMakeLists.txt文件的第一个project()命令后设置这些变量。项目应该始终将这三个变量放在一起,以明确应该如何执行语言标准要求,以及是否允许编译器进行扩展。忽略CMAKE_<LANG>_STANDARD_REQUIREDCMAKE_<LANG>_EXTENSIONS通常会导致意想不到的行为,因为默认值可能不是一些开发人员所期望的。

如果语言标准只需要对某些目标执行,而不需要对其他目标执行,那么<LANG>_STANDARD<LANG>_STANDARD_REQUIRED<LANG>_EXTENSIONS目标属性可以在单个目标上设置,而不是在整个项目中设置。这些属性的行为就好像是私有的一样,只指定目标的需求,而不指定任何链接到它的目标。因此,这给项目增加了更多的负担,以确保所有目标都正确指定了语言标准细节。实际中,使用变量在项目范围内设置语言需求通常比使用每个目标属性更容易、更健壮。最好使用这些变量,除非项目需要针对不同的目标使用不同的语言标准。

如果使用CMake 3.8或更高版本,可以使用编译特性为每个目标指定所需的语言标准。targetcompile_features()命令简化了这一过程,并清楚地指定了这些需求是PRIVATE、PUBLIC的还是INTERFACE。以这种方式指定语言需求的主要优点是,可以通过PUBLIC和INTERFACE关系将其传递到其他目标上。当目标在导出和安装时,这些要求也保留了下来。但请注意,只提供<LANG>_STANDARD<LANG>_STANDARD_REQUIRED的目标属性行为,所以仍然应该使用<LANG>_EXTENSIONS目标属性或`CMAKE_EXTENSIONS变量来控制是否允许编译器扩展。这些 …EXTENSIONS属性/变量通常只与对应的_STANDARD生效,由于设置编译器和链接器经常将这两个结合到一个标志,所以最终还要指定_STANDARD`,即使使用编译特性。因此,使用项目范围的变量会让项目更容易、更健壮。

指定单独的编译特性,可以在每个目标级别上对语言需求进行细粒度控制。在实践中,开发人员很难保证目标使用的所有特性都明确指定,因此始终存在语言需求是否正确定义的问题。随着代码开发的持续进行,也很容易过时。大多数项目会发现以这种方式指定语言需求既繁琐又脆弱,所以只有在情况明确需要时才应该使用它们。对一种语言添加时,比如:为即将发布的语言使用建议的特性时,编译特性可能是一种有用的方法,CMake支持这些特性。一般来说,项目应该更喜欢使用变量或属性来在更高的级别上设置语言需求,以获得更好的可维护性和健壮性。另外,通过编译设置标准元特性(像cxx_std_11)还可以避免许多设置单独特性的问题。对于语言标准,CMake不再定义单个的特性,只提供元特性。

项目可以检测可用的编译功能,并提供功能可用的实现。CMake甚至通过WriteCompilerDetectionHeader模块提供了一些方便的宏,使这个任务更容易完成。项目通常只考虑使用这些特性作为过渡路径更新旧代码库时,使用新的语言特性,因为它们往往会让开发者感觉不那么自然(因为降低了代码的可读性)。一个明显的例外是项目用于多种编译器语言标准的支持,可以有所不同。对于这个场景,在使用现代编译器时,对特定语言特性的可选支持可能有助于减少编译器警告和编码错误。对于大多数开发人员来说,这些好处应该与增加代码的复杂性和可读性在降低,以及不太自然的代码样式间进行权衡。

Last updated