2011 年8 月,ISO 委员会发布了 C++11,2017 年 12 月又发布了 C++17 标准,每次编程语言新版本的迭代,会令不少团队也开始着手升级开发环境,例如本文作者。那么从 C++11 升级到 C++17,究竟有哪些特别的变化值得关注?
最近,我们团队正在升级开发环境,尝试使用许多工具和编程语言的新版本。在这个过程中,比较困难的一项工作是将我们的嵌入式应用程序的代码库从 C++11 升级到 C++17。
在本文中,我将展示在嵌入式世界中非常有用的一些C++17 的特性(注意:从 C++11 迁移到 C++17 也涵盖了 C++14,因此我也会提到 C++14 的一些特性)。
查看完整的C++17特性列表,可前往:https://github.com/AnthonyCalandra/modern-cpp-features#c17-language-features。
C++14 的主要变化
当初,我们从 C++03 迁移到了 C++11,与之相比,从 C++11 升级到 C++14 时看到的升级比较小。因此,可以在嵌入式系统中使用的 C++14 特有功能实际上并不多。
二进制字面量
如果你经常需要执行按位运算或修改寄存器,那么一定很喜欢这些字面量。一些编译器具有支持此类字面量的扩展,这些字面量在实际的标准中也有一席之地。
uint8_t a = 0b110; // == 6
uint8_t b = 0b1111'1111; // == 255
constexpr**
在 C++14 中,可以在 constexpr 函数中使用的语法得到了扩展。constexpr 特别适用于嵌入式开发,因为它可以在编译时进行计算并将一些代码简化为常量。请注意,只有当表达式的所有需求都可以在编译期间确定时,才能在编译时计算表达式。
constexpr int factorial(int n) {
if (n <= 1) {
return 1;
} else {
return n * factorial(n - 1);
}
}
factorial(5); // == 120 (Calculated at compile time)
C++ 17 的世界
与 C++14 相比,C++17 标准有了很大的变化,但无需担心,你仍然可以使用已有的功能。除了已有功能之外,你还将拥有更强大的 C++17 语法和库。
(1)属性
首先,我们来介绍三个新属性:[[fallthrough]]、[[nodiscard]] 和 [[maybe_unused]]。因为这些属性只在编译时考虑,所以你根本不需要担心它们的效率。它们的存在就是为了提升代码开发。
[[fallthrough]]
你可以利用这个属性将两个相邻的 case 分支的主体合并到一个 switch 中,而不会收到来自编译器的任何警告。你可以通过这个属性告诉编译器前一个case主体结束是有意为之。
switch (n) {
case 1: [[fallthrough]]
// ...
// no `break;`
case 2:
// ...
break;
}
[[nodiscard]]
你是不是也经常忘记检查函数的返回值?有了这个属性,丢弃返回值就会收到编译器的警告。
[[nodiscard]] bool do_something() {
return is_success; // true for success, false for failure
}
do_something(); /* warning: ignoring the return value of function declared with attribute 'nodiscard' */
[maybe_unused]]
为了避免收到警告,必须将未使用的变量转换为 void,你是不是也感到不耐烦?试试看这个属性,你就可以摆脱那些烦人的警告。
void my_callback(std::string msg, [[maybe_unused]] bool error) {
// Don't care if `msg` is an error message, just log it.
log(msg);
}
(2)编译时的力量
编译时的检查是我最喜欢 C++ 的地方。在 C++17 中,这种能力通过一些新特性得到进一步增强。想一想许多嵌入式系统中繁琐的调试过程,如今甚至不需要部署代码就可以检查结果,是不是觉得是个特大好消息?传输可执行文件、准备环境和测试等一系列工作都非常艰巨,而且很耗时。但使用编译时编程,这部分头疼的工作都可以省略。
没有消息的静态断言
你可能认为,我们已经有了 static_assert(..),可以在编译时进行检查。而如今,断言机制甚至不需要错误消息。这样,代码看上去会更加清晰。
static_assert(false);
if constexpr
我最喜欢的一个语句!我们可以利用 if constexpr 编写一些代码,这些代码可以根据编译时的条件,有选择地进行实例化。
template<typename T>
auto length(const T& value) noexcept {
if constexpr (std::integral::value) { // is number
return value;
}
else {
return value.length();
}
}
int main() noexcept {
int a = 5;
std::string b = "foo";
std::cout << length(a) << ' ' << length(b) << '
'; // Prints "5 3"
}
在 C++17 之前,上面这段代码需要编写两个不同的函数,分别用于字符串和整数输入,如下所示。
int length(const int& value) noexcept {
return value;
}
std::size_t length(const std::string& value) noexcept {
return value.length();
constexpr lambda
如果你也喜欢在代码中使用 lambda 表达式,那么肯定会喜欢这个功能。此外,Lambdas 的调用也可以采用直接声明为 constexpr 的形式。
auto identity = [](int n) constexpr { return n; };
static_assert(identity(123) == 123);
(3)语法糖
在 C++17 中,有一些功能可以帮助你编写更漂亮的代码。即使它们的存在对运行时性能没有明显的影响,但你会很喜欢它们。
折叠表达式
如果你有过使用可变参数模板来编写具有可变输入或迭代次数的递归算法的经历,那么就可能遇到必须为该可变参数模板函数实现终止符的问题。例如,下面的代码是用 C++11 编写的,作用是累加给定的数字。
int sum() { return 0; } // Termination function
template<typename ...Args>
int sum(const int& arg, Args... args) {
return arg + sum(args...);
}
如果我们没有实现不接受任何输入的终止符,这段代码将无法通过编译。但有了折叠表达式,你就不必实现终止符了,而代码看上去也更好,如下所示。
template<typename ...Args>
int sum(Args&&... args) {
return (args + ...);
}
嵌套命名空间
不知道为什么 C++ 委员会以前没有想到这一点。无需多说,分别看下面 C++11 和 C++17 中嵌套命名空间的定义,你就能发现区别。
// C++11
namespace A {
namespace B {
namespace C {
int i;
}
}
}
// C++17
namespace A::B::C {
int i;
}
加强版的条件语句
如果所有条件语句都像 for 语句一样具有初始化,那是不是更强大?在 C++17 中,条件语句也增加了初始化部分。
这是迄今为止我所见过的最强大的功能之一,因为你无需在输入一系列 if-else 语句或 switch-case 之前,编写一堆局部变量。
if (int i = 4; i % 2 == 0) {
cout << i << " is even number" << endl;
}
switch (int i = rand() % 100; i) {
default:
cout << "i = " << i << endl;
break;
}
内联变量
在 C++17 之前,我们必须在源文件中实例化类内静态变量。如今,你可以使用内联变量将声明和初始赋值合并到类定义中,如下所示。
struct BabaMrb {
static const int value = 10;
static inline std::string className = "Hello Class";
}
(4)其他特性
C++17 中还有许多我不知道如何归类的的其他特性。下面,我们来逐一介绍。
复制省略
复制省略(Copy elision),即返回值优化,是大多数编译器为防止在某些情况下出现额外副本而实现的优化。从 C++17 开始,直接返回对象时必然会触发复制省略。在某些情况下,即使只有一次复制操作也会影响系统的性能,例如对实时性有严格要求的系统。遇到这种情况,我们最好确保避免复制,以免降低系统性能。
struct C {
C() { std::cout << "Default constructor" << std::endl; }
C(const C&) { std::cout << "Copy constructor" << std::endl; }
};
C f() {
return C(); // Definitely performs copy elision
}
C g() {
C c;
return c; // May perform copy elision
}
int main() {
C obj = f(); // Copy constructor isn't called
}
共享互斥锁
在使用共享互斥锁后,我们就可以按需读取对象而无需加锁,而写调用可以照往常一样使用常规互斥锁来锁定对象。共享互斥锁可以加快只读访问操作的速度,因为读取操作可以同步进行。
硬件干涉大小
这个新的库功能可以帮助你在编译期间确定 L1 缓存行的大小。有了这个功能,你就可以根据 L1 缓存行的大小调整结构、缓冲区等。我在使用 C++11 为 ARM Cortex-A9 内核实现低级裸机 DMA 驱动程序时就会用到这个功能,因为在编写这些代码时,我需要手动管理高速缓存和主内存之间的一致性。
尽管此功能非常强大,但直到版本 12 才在所有版本的 GCC 中实现,因此很可能你当前的编译器并不支持。如下代码是一个示例,可以帮助你更好地理解这个功能。
using std::hardware_constructive_interference_size;
using std::hardware_destructive_interference_size;
// 64 bytes on x86-64 │ L1_CACHE_BYTES │ L1_CACHE_SHIFT │ __cacheline_aligned │ ...
constexpr std::size_t hardware_constructive_interference_size = 64;
constexpr std::size_t hardware_destructive_interference_size = 64;
struct alignas(hardware_constructive_interference_size) OneCacheLiner { // occupies one cache line
std::atomic_uint64_t x{};
std::atomic_uint64_t y{};
};
总结
与 C++14 不同,C++17 引入了许多新特性。其中一些功能对嵌入式系统开发非常有帮助。
不同产品之间,嵌入式设备的计算能力差异很大。由于 CPU 性能、缺乏编译器支持、验证必要性等多种原因,我选择的某些功能可能不适用于你的固件。总体而言,迁移到 C++17 可能需要花费大量的时间和精力,请认真考虑是否需要迁移。
-
嵌入式系统
+关注
关注
41文章
3590浏览量
129460 -
C++
+关注
关注
22文章
2108浏览量
73636 -
编译器
+关注
关注
1文章
1634浏览量
49122
原文标题:从 C++11 升级至 C++17,它们让嵌入式系统更好了!
文章出处:【微信号:技术让梦想更伟大,微信公众号:技术让梦想更伟大】欢迎添加关注!文章转载请注明出处。
发布评论请先 登录
相关推荐
评论