看到一个介绍 C++17 的系列博文(原文),有十来篇的样子,觉得挺好,看看有时间能不能都简单翻译一下,这是第三篇~
在之前的文章中我介绍了一些C++17语言核心层的变化,这次我会介绍更多的相关细节,涉及的主题有:内联变量(inline variables),模板,auto相关的自动类型推导以及属性(attributes).
C++标准整体的(特性)时间线
上图中列出的是C++17的主要特性,这篇文章介绍的则是另一些不那么为人熟知的特性.
过去我们不将C++代码打包为仅含头文件的程序库(header-only libraries)的一个主要原因,就是为了正确处理相同的变量引用,C++17引入的内联变量解决了这个问题!现在你可以声明内联的全局变量和静态变量了,相关的规则限制和内联函数是一致的.
这意味着:
你可以重复定义一个内联变量,但是该内联变量必须在使用到他的编译单元中可见.一个全局内联变量(即非静态内联变量)必须在每一个编译单元中进行声明并且该全局内联变量在每一个编译单元中都有相同的内存地址.现在你能直接在头文件中声明(内联)变量并且多次包含他们(包含对应的头文件)了!
class Widget
{
public:
Widget() = default;
Widget(int w): width(w), height(getHeight(w)){}
Widget(int w, int h): width(w), height(h){}
private:
int getHeight(int w){ return w*3/4; }
static inline int width= 640;
static inline int height= 480;
static inline bool frame= false;
static inline bool visible= true;
...
};
inline Widget wVGA;
auto 可以根据其初始化表达式自动推导变量类型, 在C++17中, auto 的这种自动类型推导能力又进一步增强了,借助auto,函数模板和(类模板的)构造函数的模板参数可以根据其参数自动进行类型推导(细节介绍),非类型模板参数的类型也可以从参数中自动推导出来.下面我就来介绍一下非类型模板参数的自动类型推导.
首先要说明一下哪些属于非类型模板参数:他们是 nullptr, 整型, 左值引用, 指针 以及 枚举类型.下面的讲解主要以整型为主.
说了这么多理论,是时候看个示例了:
template <auto N>
class MyClass
{
...
};
template <int N>
class MyClass<N>
{
...
};
MyClass<'x'> myClass2; // Primary template for char
MyClass<2017> myClass1; // Partial specialisation for int
第1行代码中,通过将模板参数声明为 auto, 编译器便可以自动推导非类型模板参数(第1行代码中的 N)的类型了,你甚至可以像示例代码中那样(第7和第8行)偏特化该模板(示例代码中为int类型进行了偏特化).第13行代码的模板会依据原始模板(示例中的第一个模板)进行实例化,而第14行代码的实例化依据的则是偏特化模板版本(示例中的第二个模板).
一般的类型修饰符也可以用在非类型模板参数上,所以很多时候,你不必非得使用模板偏特化来限制非类型模板参数的类型.
template <const auto* p>
struct S;
上述代码中, p 被限制成必须是常量指针类型.
即便在可变参数模板中,非类型模板参数也可以进行自动类型推导.
template <auto... ns>
class VariadicTemplate
{
...
};
template <auto n1, decltype(n1)... ns>
class TypedVariadicTemplate
{
...
};
示例代码中,模板 VariadicTemplate(第1行至第5行) 可以对任意数量的非类型模板参数进行自动类型推导,而 TypeVariadicTemplate 模板(第7行至第11行)则仅会自动推导第一个非类型模板参数的类型,其余非类型模板参数的类型都与第一个非类型模板参数的类型相同.
C++17 更改了 auto 结合使用 列表初始化 的规则.
C++17之前,如果你结合使用 auto 和 列表初始化,你会得到一个 std::initializer_list.
auto initA{1}; // std::initializer_list<int>
auto initB = {2}; // std::initializer_list<int>
auto initC{1, 2}; // std::initializer_list<int>
auto initD = {1, 2}; // std::initializer_list<int>
这个规则很容易记忆,也很容易教授,但是在C++17中,这个规则变复杂了(个人感觉这个改动并不好)
auto initA{1}; // int
auto initB = {2}; // std::initializer_list<int>
auto initC{1, 2}; // error, no single element
auto initD = {1, 2}; // std::initializer_list<int>
现在,使用初始化列表进行赋值依然会得到类型 std::initializer_list ,但使用初始化列表进行复制构造却只支持单个数值了,得到的类型也不再是std::initializer_list,而是对应的初始化数值类型.
接下来让我们来看一些小而美的特性.
在C++17中,你可以非常简便的定义嵌套的命名空间.
相比之前的写法:
namespace A
{
namespace B
{
namespace C
{
...
}
}
}
C++17中的写法要简明很多:
namespace A::B::C
{
...
}
C++17 新增了三个属性 [[fallthrough]], [[nodiscard]], 和 [[maybe_unused]].
这三个属性都是为了处理编译器警告,下面的例子来自于cppreference.com.
[[fallthrough]]可以在 switch 语句中使用,他必须单独占据一行代码,并且后面需要跟随一个 case标签(或者default标签) 语句,以此来说明代码从[[fallthrough]]的前一个标签"落下"(继续执行后面标签的逻辑,而不break)是有意为之的,编译器不应该诊断其为警告.
这里有个例子:
void f(int n)
{
void g(), h(), i();
switch (n)
{
case 1:
case 2:
g();
[[fallthrough]];
case 3: // no warning on fallthrough
h();
case 4: // compiler may warn on fallthrough
i();
[[fallthrough]]; // illformed, not before a case label
}
}
代码第9行的[[fallthrough]]属性抑制了编译器的编译警告,但是代码第12行由于缺少[[fallthrough]]属性,编译器便有可能产生告警.第14行代码的[[fallthrough]]声明是病态的,因为其后没有跟随case标签(或者default标签).
[[nodiscard]]属性可以用于函数声明,枚举声明以及类声明中.如果你丢弃了一个声明为[[nodiscard]]的函数的返回值,编译器就会产生一个编译警告.同样的,如果你丢弃了函数中返回的(声明为)[[nodiscard]]枚举或者(声明为)[[nodiscard]]类,编译器同样会给出警告,抑制该类警告的一种方法就是对返回值进行一次void转型操作.
下面的示例中,第6行代码会产生一个编译警告,但在第12行代码中,由于 foo 函数返回的是引用类型(虽然引用类型本身是[[nodiscard]]属性),所以不会产生编译警告.
struct [[nodiscard]] error_info {};
error_info enable_missile_safety_mode();
void launch_missiles();
void test_missiles()
{
enable_missile_safety_mode(); // compiler may warn on discarding a nodiscard value
launch_missiles();
}
error_info& foo();
void f1()
{
foo(); // nodiscard type is not returned by value, no warning
}
可以使用[[maybe_unused]]的地方很多:类,typedef,变量,非静态成员变量,函数,枚举类型或者枚举值.[[maybe_unused]]可以抑制编译器对于代码中未使用实体的编译警告.
void f([[maybe_unused]] bool thing1,
[[maybe_unused]] bool thing2)
{
[[maybe_unused]] bool b = thing1;
assert(b); // in release mode, assert is compiled out
}
release模式下,上面第5行代码在编译时会被移除,但是由于我们之前为 b 声明了[[maybe_unused]]属性,所以编译器不会产生警告,同样的,虽然代码中也没有使用参数 thing2, 但是由于 thing2 也声明了[[maybe_unused]]属性,所以也不会产生编译警告.