第十七章:语言要求

Posted by lili on

随着C和C++语言的不断演进,开发人员越来越需要了解编译器和链接器标志,以启用其代码所使用的C和/或C++版本的支持。不同的编译器使用不同的标志,即使在使用相同的编译器和链接器时,标志也可以用于选择标准库的不同实现。

在C++11支持相对较新的日子里,CMake并没有直接支持选择要使用的标准,因此项目被迫自行解决所需的标志。在CMake 3.1中,引入了一些功能,允许以一种一致且方便的方式选择C和C++标准,将各种编译器和链接器的差异抽象出来。此支持在后续版本中得到了扩展,从CMake 3.6开始涵盖了大多数常见的编译器(CMake 3.2添加了大部分编译器支持,3.6添加了英特尔编译器)。

CMake提供了两种指定语言要求的主要方法。第一种是直接设置语言标准,第二种是允许项目指定其需要的语言特性,然后让CMake选择相应的语言标准。虽然这个功能主要是由C和C++语言推动的,但其他语言和伪语言如CUDA和Objective-C/C++也得到了支持。

17.1. 直接设置语言标准

项目控制构建使用的语言标准的最简单方法是直接设置它们。使用这种方法,开发人员无需了解或指定代码使用的单个语言特性,只需要设置一个表示代码所支持的标准的单个数字。这不仅易于理解和使用,而且有一个优势,即相对容易确保在整个项目中使用相同的标准。这在链接阶段变得重要,因为应该在所有链接的库和目标文件中使用一致的标准库。如果头文件根据使用的语言标准不同而定义不同的东西,这在编译时也可能很重要。

与CMake的通常模式一样,目标属性控制在构建目标的源文件和链接最终可执行文件或共享库时将使用哪个标准。对于给定的语言,有三个与指定标准相关的目标属性。在下文中,<LANG>最常见的可能是C或CXX,但CUDA、OBJC和OBJCXX也在最近的CMake版本中得到了支持。

<LANG>_STANDARD

指定项目希望为指定目标使用的语言标准。

  • C_STANDARD支持值90、99和11,CMake 3.21添加了17和23。
  • CXX_STANDARD支持值98、11和14。自CMake 3.8以来,也支持值17,自CMake 3.12以来支持值20,自CMake 3.20以来支持值23。自CMake 3.25开始,值26被认为是有效的,但目前没有编译器支持它。
  • CUDA_STANDARD是CXX_STANDARD在CUDA上的特定版本。如果没有定义CUDA_STANDARD,它实际上会回退到CXX_STANDARD。自CMake 3.8以来,CUDA_STANDARD支持值98和11,自CMake 3.9以来支持值14,自CMake 3.18以来支持值03、17和20,自CMake 3.22以来支持值23。这些值中的一些在比上述版本早的CMake版本中被认为是有效的,但在先前的最低版本中实际上没有编译器支持。
  • 自CMake 3.16以来,OBJC_STANDARD和OBJCXX_STANDARD遵循类似的模式。如果它们没有定义,则会回退到C_STANDARD和CXX_STANDARD。实际上,设置OBJC_STANDARD或OBJCXX_STANDARD可能是不寻常的,因为通常会希望回退到C_STANDARD和CXX_STANDARD,以确保C/C++代码使用与Objective-C/C++代码一致的语言标准。

合理地假设随着时间的推移,后续的CMake版本将支持其他语言标准。当创建目标时,此<LANG>STANDARD属性的初始值取自CMAKE<LANG>_STANDARD变量。

<LANG>_STANDARD_REQUIRED

虽然<LANG>_STANDARD属性指定项目希望使用的语言标准,<LANG>_STANDARD_REQUIRED确定该语言标准是作为最低要求还是作为“如果可用则使用”的指导方针对待。人们可能直观地期望<LANG>_STANDARD默认情况下是一个要求,但不管好坏,<LANG>_STANDARD_REQUIRED属性默认情况下为OFF。当OFF时,如果编译器不支持请求的标准,CMake会将请求降级到较早的标准,而不是以错误停止。这种降级行为对新开发人员来说通常是意外的,在实践中可能导致混淆的原因。因此,对于大多数项目,在指定<LANG>_STANDARD属性时,其相应的<LANG>_STANDARD_REQUIRED属性几乎总是需要设置为true,以确保特定请求的标准被视为强制要求。在创建目标时,此属性的初始值取自CMAKE_<LANG>_STANDARD_REQUIRED变量。

<LANG>_EXTENSIONS

许多编译器支持其对语言标准的扩展。通常提供了一个编译器和/或链接器标志来启用或禁用这些扩展。<LANG>_EXTENSIONS目标属性控制是否为该特定目标启用这些扩展。请参阅下文以了解此属性的初始化方式。

对于许多编译器,用于控制语言标准和是否启用扩展的标志相同。在CMake 3.21或更早的版本中,如果项目设置了<LANG>_EXTENSIONS属性而没有设置<LANG>_STANDARD属性,<LANG>_EXTENSIONS可能会被忽略。CMake 3.22引入了政策CMP0128,影响此行为。当CMP0128设置为NEW时,<LANG>_EXTENSIONS将受到尊重,而不管是否设置了<LANG>_STANDARD。

CMP0128还影响在创建目标时<LANG>_EXTENSIONS的初始化方式。当CMP0128设置为OLD或未设置时,<LANG>_EXTENSIONS属性的初始值取自CMAKE_<LANG>_EXTENSIONS变量。如果未设置该变量,则默认值为true。当CMP0128设置为NEW时,<LANG>_EXTENSIONS的初始值还将取自CMAKE_<LANG>_EXTENSIONS变量(如果已定义),否则将回退到一个单独的只读CMAKE_<LANG>_EXTENSIONS_DEFAULT变量的值。CMake在其编译器检查期间确定编译器的默认值,并将相应地设置CMAKE_<LANG>_EXTENSIONS_DEFAULT以反映该行为。

CMP0128还有一个潜在的影响。当此策略设置为NEW时,与设置语言标准或扩展相关的标志仅在编译器的默认行为尚未提供这些设置时才会添加。这可能会改变在编译器命令行上看到的标志,但除非项目直接添加自己的语言标准标志(这是强烈不推荐的),否则不应改变实际行为。

在实践中,项目更倾向于设置提供上述目标属性默认值的变量,而不是直接设置目标属性。这确保项目中的所有目标都以一致的方式使用兼容的设置构建。还建议项目设置所有三个变量,而不仅仅是其中的一些。对于许多开发人员来说,_STANDARD_REQUIRED和_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 标志。对于Visual Studio 2015 Update 3之前的编译器,不会添加任何标志,因为编译器默认支持C++11,或者根本不支持C++11。从Visual Studio 15 Update 3开始,编译器支持指定C++标准为C++14或更高版本,如果未设置,则默认为C++14。

相比之下,以下示例请求更高版本的C++并启用编译器扩展,从而生成类似于 -std=gnu++14 的GCC/Clang编译器标志。同样,取决于编译器版本,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标准的详细信息:

# 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可能会选择更高版本的标准。

17.2. 通过功能要求设置语言标准

直接为目标或整个项目设置语言标准是管理标准要求的最简单方法。当项目的开发人员知道哪个语言版本提供了项目代码使用的功能时,这是最合适的方法。特别是在使用大量语言特性时,因为不需要显式指定每个特性。然而,在某些情况下,开发人员可能更愿意说明其代码使用哪些语言特性,然后由CMake选择适当的语言标准。这有一个优势,即与直接指定标准不同,编译特性要求可以成为目标接口的一部分,因此可以强制应用于链接到它的其他目标。

编译特性要求由目标属性COMPILE_FEATURES和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属性又指定了编译特性(直接或通过链接到其链接的东西的INTERFACE特性的结果)的情况下,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,如果工具链严格执行此删除,可能无法编译。CUDA、OBJC和OBJCXX语言在某种程度上有点不同,因为它们是基于C或C++的。OBJC和OBJCXX语言目前还没有它们自己单独的一组编译特性,但可以使用相应基础语言的编译特性。在CMake 3.16或更早的版本中,CUDA语言也没有自己独立的一组编译特性,并依赖于C++编译特性。CMake 3.17添加了专用于CUDA的编译特性支持。

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

一些项目具有处理特定语言特性是否被支持的能力。例如,它们可能提供备用实现,或仅在编译器支持时定义某些函数重载。项目可能支持某些编译特性是可选的,例如旨在指导开发人员或提供编译器捕获常见错误的能力的关键字。C++中的关键字,如final和override,就是这种情况的常见示例。CMake提供了多种处理上述情景的方法。一种方法是使用生成器表达式根据特定编译特性的可用性有条件地设置编译器定义或包含目录。这可能有点冗长,但它们提供了很大的灵活性,并支持对基于特性的功能进行非常精确的处理。考虑以下示例:

add_library(Foo ...)
# 仅在可用时使override成为特性要求
target_compile_features(Foo PUBLIC
    $<$<COMPILE_FEATURES:cxx_override>:cxx_override>
)
# 定义foo_OVERRIDE符号,因此如果可用,则提供override关键字,否则为空
target_compile_definitions(Foo PUBLIC
    $<$<COMPILE_FEATURES:cxx_override>:foo_OVERRIDE=override>
    $<$<NOT:$<COMPILE_FEATURES:cxx_override>>:foo_OVERRIDE>
)

上述示例允许像下面这样的代码在任何C++编译器上编译,而不管它是否支持override关键字:

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

其他特性也可以使用类似的方式有条件地定义符号。例如,如果编译器支持,则可以使用C++关键字final、constexpr和noexcept,否则可以省略它们但仍产生有效的代码。其他关键字,如nullptr和static_assert,具有替代实现,如果不支持该关键字,则可以使用。为了覆盖支持和不支持的情况,为每个特性指定生成器表达式可能既繁琐又容易出错。使用WriteCompilerDetectionHeader模块(截至CMake 3.20已弃用)等技术可以帮助减少这些缺点,但从长远来看,将行为切换到标准级别而不是在个别特性上可能是更好的方法。

17.3. 推荐实践

项目应避免直接设置编译器和链接器标志以控制所使用的语言标准。所需的标志因编译器而异,因此使用CMake提供的功能并允许其适当地填充标志更加健壮、可维护和方便。CMakeLists.txt文件将更清晰地表达意图,因为使用人类可读的变量和属性,而不是通常晦涩的原始编译器和链接器标志。

控制语言标准要求的最简单方法是使用CMAKE_<LANG>_STANDARD、CMAKE_<LANG>_STANDARD_REQUIRED和CMAKE_<LANG>_EXTENSIONS变量。这些变量可用于设置整个项目的语言标准行为,确保在所有目标中保持一致的使用。理想情况下,应在顶层CMakeLists.txt文件中的第一个project()命令之后设置这三个变量。项目应始终一起设置所有三个变量,以清楚地表明语言标准要求应如何执行以及是否允许编译器扩展。省略CMAKE_<LANG>_STANDARD_REQUIRED或CMAKE_<LANG>_EXTENSIONS可能导致意外行为,因为默认值可能不符合某些开发人员的直观期望。

如果只需要强制执行语言标准以供某些目标,而不是其他目标,则可以在单个目标上设置<LANG>_STANDARD、<LANG>_STANDARD_REQUIRED和<LANG>_EXTENSIONS目标属性,而不是在整个项目上设置。这些属性的行为就像它们是PRIVATE一样,意味着它们只对该目标指定要求,而不对任何链接到它的内容指定要求。这会使项目更难以确保所有目标都正确指定了语言标准的详细信息。实际上,使用变量在整个项目范围内设置语言要求通常更容易且更健壮,而不是使用每个目标的属性。除非项目需要为不同的目标设置不同的语言标准行为,否则建议使用变量。

如果使用CMake 3.8或更高版本,可以使用编译特性在每个目标上指定所需的语言标准。target_compile_features()命令使此操作变得简单明了,清楚指定这些要求是PRIVATE、PUBLIC还是INTERFACE。以这种方式指定语言要求的主要优势是,通过PUBLIC和INTERFACE关系,可以在其他目标上具有传递性地强制执行这些要求。这些要求在导出和安装目标时也会保留(请参阅第27章,安装)。但请注意,仅提供<LANG>_STANDARD和<LANG>_STANDARD_REQUIRED目标属性行为的等效部分,因此仍应使用<LANG>_EXTENSIONS目标属性或CMAKE_<LANG>_EXTENSIONS变量来控制是否允许编译器扩展。

如果策略CMP0128未设置为NEW,则<LANG>_EXTENSIONS属性及其相关的变量通常仅在相应的<LANG>_STANDARD也被设置时才生效。这是因为编译器通常将两者合并为单个标志。因此,除非使用CMake 3.22或更高版本且将CMP0128策略设置为NEW,否则很难避免不得不指定<LANG>_STANDARD,即使使用编译特性时也是如此。

指定单个编译特性提供了在每个目标级别对语言要求进行精细控制的能力。在实践中,开发人员难以确保所有目标使用的特性都得到了明确指定,因此始终存在语言要求是否被正确定义的问题。随着时间的推移,它们也可能很容易变得过时,因为代码开发持续进行。大多数项目可能会发现以这种方式指定语言要求很繁琐而脆弱,因此只有在情况明显需要时才应使用它们。

在C++11和C++14的早期阶段,使用编译特性可能是有用的,因为编译器支持通常滞后。对于后来的语言版本,主流编译器的支持速度更快。因此,CMake在C++14之后停止提供语言标准的细粒度特性。对于C++17及更高版本,仅提供了高级元特性,如cxx_std_17。

对于仍然需要支持旧编译器的项目,它们可以检测可用的编译特性并为特性是否可用提供实现。由CMake提供的WriteCompilerDetectionHeader模块有时对帮助项目过渡到更现代的编译器是有用的,但该模块在CMake 3.20中已弃用。因此,不应再使用。