C++17整装待发

本文有 13693 字,大约需要 34 分钟可以读完, 创建于 2017-10-04

9月份对于C++社区而言绝对是个令人振奋的收获季节。

先是在9月上旬的时候,语言标准化委员会完成了新的语言标准的草案投票并取得了全票通过(虽然有一些小的语法、修辞上的评论待修改); Herb Sutter第一时间在其博客上发布了这一激动人心的消息:C++2017已经获得了实质性的通过静待最终发布。 值得注意的是,这是C++这门大象一般行动缓慢的语言在变更为更敏捷的火车发布模型之后,第一次按照时间点准时发布大的语言版本。 之前可是有10多年才发布一个版本(说好的C++0x最终变成了十六进制年份的玩笑)导致开发者等到心凉自己造轮子的事情;而C++14是一个修复设计问题的小版本。

另外一个大事件则是CppCon2017社区大会的成功举办,与往常一样有一大堆的新料演讲(已经放在这里)放了出来供社区消化、学习。

C++17

作为一个按照敏捷发布模型出来的第一个大的语言版本,由于需要照顾到质量、时间节点(毕竟需要按时发布)和参考实现成熟度等因素,本来新特性不会很多。 然而作为一个语言的大版本来说,没有一定的新料自然也有滥竽充数的嫌疑;在敏捷的大背景下,谁也不愿意多等都希望马上出来一些质量够好、实现就绪马上可用的新特性;而且要不止一个,因为可供选择的编程语言实在是太多了。

值得庆幸的是,WG21标准化小组早在3月份就完成了新语言特性的草稿准备工作。

语言特性

很多语法糖方面的更新,可以让程序员生产率更高,包括以下这些主要的更新

嵌套的命名空间声明

这是一个小小的改进,但足以节省大家很多敲击键盘和格式化缩进的烦恼。因为在比较大的项目中,经常用嵌套的命名空间来隔离API和类,之前我们不得不这样写

namespace outer{
    namespace inner {
        namesapce module {
            //my module definitions...
        }
    }
}

或者为了节省空间(采用微软的换行风格有强迫症的更要哭了),直接缩略为

namespace outer{ namespace inner{ namespace module {
    //my module definitions...
}}}

新标准直接允许我们写为

namespace outer::inner::module {
    //my module definitions...
}

是否清爽了很多,或者有本来就应该如此的感觉?为什么没有更早支持啊。

static_assert的消息提醒参数变为可选

这个是针对之前引入的用于与编译期间静态检查机制static_assert的一点小修正,允许其携带的消息提醒参数被忽略,以减少编译噪音。 本来这个静态断言机制可以很好地服务于编译器多态检查,如果没问题就一路继续编译下去,如果出错,则会打印一条错误信息包含在错误诊断中,并终止编译。 新的改动允许不提供该诊断信息。

auto可以用于初始化列表的类型推导

C++11通过重用auto关键字来做自动类型推断,而C++17将其扩充到了初始化列表类型,并要求它符合特定的规则。即

auto x = {1}; //decltype(x) = initializer_list<int>:
auto y = {2, 3}; //declytpe(y) = initializer_list<int, int>;
auto x1 {1}; //same as x
auto y1 {2, 3}; //ill-formed! Not a single element!

字面量类型扩展

字面量类型具有很好的声明式风格和良好的可读性,之前的语言标准支持的类型比较有限(比如字符常量、整数、浮点常量),而C++17引入了对其他字符集的支持

  • u8'c-char'用以支持UTF-8编码的字符常量,其类型依然是char类型,而其值则是用ISO10646编码的单个字符。
  • u'c-char'用以支持UCS2编码可以支持2个字节,内部类型是char16_t,如 u'\U0001f34c'
  • U'c-char'支持4个字节的UCS4,内部类型是char32_t,内部用Unicode编码,比如U'猫'

浮点类型的常量声明支持了更多可读性更好的格式,下边的例子

#include <iostream>
int main()
{
  std::cout << 123.456e-67 << '\n'
            << .1E4f       << '\n'
            << 58.         << '\n'
            << 4e2         << '\n';
}

会输出四种方式所声明的浮点数字如下

1.23456e-65
1000
58
400

可变长模板参数中的折叠表达式

C++11 引入了可变长的模板参数,大大简化了模板元编程 (为什么要改进这个 ? 因为语言的设计哲学就是追求零开销的抽象,这一切都需要强大的模板元编程能力才能二者兼得) 。 C++17 则支持在变长模板参数中使用折叠 (fold ) 或归约 (reduce) , 这两者是函数式编程中最基本的函数单元 ; 最基础的 Haskell 或者 Javascript 教程都会介绍两者 , 可惜 C++ 才刚刚加入支持 ; 喜欢函数式编程语言的早就改换门庭了吧 :-)

这一新特性可以支持从一堆模板参数中,仅仅指明第一个而省略其他,也可以指明最后一个省略前边的。 这些参数中间可以用操作符来连接,编译器需要自己负责将他们按照既定的规则串联起来。

比如这个例子完成左折叠运算 (如注释所写)

template<typename... Args>
bool all(Args... args) { 
    //op is &&, pack parameters as ..., specified last parameter
    return (... && args); 
}

//expanded as left fold, as 
// ((true && true) && true) && false = false
bool b = all(true, true, true, false);

fold 操作的详细语法定义如下

(pack op ...) //unary right fold
(... op pack) //unary left fold
(pack op ... op init) //binary right fold
(init op ... op pack) //binary left fold 

运算符可以是所有语法已经定义的运算符或者调用操作 , 然而当我们使用特殊的运算符去带入表达式的时候,代码可能并不是那么直观 , 下面更复杂一点的例子分别演示了使用标准输出运算符和逗号运算符的情形

template<typename ...Args>
void printer(Args&&... args) {
    //left fold print
    (std::cout << ... << args) << '\n';
}
 
template<typename T, typename... Args>
void push_back_vec(std::vector<T>& v, Args&&... args)
{
    //binary left fold, push back from left to right, init=v
    (v.push_back(args), ...);
}

第一个例子中,操作绑定于标准输出流的 << 运算符 ,这里使用 universial reference 可以自动转发避免拷贝。 第二个例子中,对应的操作符为成员函数调用 (push_back 调用) 。

如下一个例子则用编译器元编程技术实现常量的按bit字节序转换 :

// compile-time endianness swap based on http://stackoverflow.com/a/36937049 
template<class T, std::size_t... N>
constexpr T bswap_impl(T i, std::index_sequence<N...>) {
  return (((i >> N*CHAR_BIT & std::uint8_t(-1)) << (sizeof(T)-1-N)*CHAR_BIT) | ...);
}

template<class T, class U = std::make_unsigned_t<T>>
constexpr U bswap(T i) {
  return bswap_impl<U>(i, std::make_index_sequence<sizeof(T)>{});
}
 
int main()
{
    static_assert(bswap<std::uint16_t>(0x1234u)==0x3412u);
    static_assert(bswap<std::uint64_t>(0x0123456789abcdefULL)==0xefcdab8967452301ULL);
}

constexpr 可以作用于 if 判断

自从在 C++11 标准中引入以来 , constexpr 一直有诸多限制和不便,新的语言标准则在一直放宽对其的限制 , 以便提供更高的优化效率 。 简单来说 , constexpr 用于修饰可能潜在地被编译期计算赋值的函数或者变量 。

C++17 主要放松了之前常量表达式不可用于 if 判断的限制。它可以达到的效果是 , 对于声明为 constexpr 的条件表达式 , 如果传入的参数在编译期没有被命中 , 则其对应的代码块甚至可以被编译器优化掉 。

考虑如下的 C++14 代码

template<unsigned n>
struct Arg {
    template<class X, class...Xs>
    constexpr auto operator()(X x, Xs...xs) {
        return Arg<n - 1>{}(xs...);
    }
};

template<>
struct Arg<0> {
    template<class X, class...Xs>
    constexpr auto operator()(X x, Xs...) {
        return x;
    }
};

//type alias
template<unsigned n>
constexpr auto arg = Arg<n>{};

// arg<2>(0, 1, 2, 3, 4, 5) == 2;

为了实现提取第 N 个特定的参数,我们需要大量的模板元代码以实现目的,可读性非常不友好。新的语言特性则允许我们写为

template <unsigned n>
struct Get{
    template <class X, class...Xs>
    if constexpr (n > sizeof...(xs) ) {
        return n;
    } else if constexpr (n > 0) {
        return Get<n-1>{}(xs...);
    } else {
        return x;
    }

这里如果对应的分支没有被命中,那么对应的代码可以不用编译,一个明显的好处是可以减少目标代码的体积, 并减少和复杂的模板元技巧斗争的痛苦。 在 Linux GCC7.1 编译上述代码,同样的编译开关和优化选项下,新的版本目标代码体积更小一些,执行速度也略快。

标准程序库

标准库同时更新了一些新的基础设施,包括用于函数式编程范式的 optional 类型和类型安全的 variant 类。

std::optional<T>

类似于 Haskell 中的 Maybe Monadoptional类用于表述一个封装类型,它要么是有初始化好的一个值,要么什么也没有。 该类型最合适的应用场景就是用于返回可能失败也可能成功的情形,这种情况下如果操作成功,那么返回的结果是一个封装过的类型安全的对象, 如果操作失败,则返回空。

其好处是明显提高了代码的可读性,并且提供了方便的构造函数去生成封装好的对象。同时为了便于使用,它还支持隐式转换为 bool 类型

  • 如果其有值,则返回true
  • 否则就返回false

出于强类型保证考虑,空值的情况下,其封装类型本质上是 std::nullopt_t 类型,并定义了一个对象 nullopt, 标准要求其必须是一个 constexpr 修饰的编译器常量,实现可以根据具体编译器实现来自主决定。

optional类型支持类型的值提取,用 value_or 来封装一个取值或者返回一个给定的值的操作。其用法和 Java8 的Optional类有些相似, 只是它没有提供现成的链式操作封装,API不如Java的丰富。

std::variant<...Types>

variant 类型用于表述多个不同的可能值的联合类型,任何一个时刻其值可能是给定的模板类型中的一个;初始化完成一个对象后,标准不允许动态 的申请内存。它封装的类型可以任意的对象类型,然而不允许是

  • 引用类型
  • 数组
  • 空类型 void

一个简单的例子如下

//initialize variant and hold a value of double type
std::variant<int, float, std::string> var{12.0};

//default construct from first type, contains 0 of int type
decltype(var) another; 
内部参数值的访问

需要获取其封装的值的时候,可以用std::get来获取,由于任意时刻其值可能是给定类型的一种, 当对应的值的当前类型和期望获取的类型不匹配时,get 操作使用 std::bad_variant_access 异常来传递错误。 get操作提供两种类型的提取方法,一种是使用索引位置获取,此时可以用auto类型完成自动类型推导;另外一种方式是使用类型作为模板参数。

标准库也提供了查询操作可以让调用者检查给定的variant是否当前持有一个给定的类型的值而不抛出异常。 variant 类型支持hash操作,因此它可以被放置在 unordered_map 中作为索引。

赋值

variant对象可以被赋值为新的varant 对象,然而其赋值语义却略微复杂一些,规则是首先判断两者是否同是variant类型,如果是则做如下判断

  • 如果传入的对象和自身都没有持有任何具体值,则什么也不做
  • 否则如果传入的对象没有持有值但是自身有值,则销毁内部持有的对象并标记自身为不持有值
  • 否则,判断传入的对象的可能的值类型是否和自身可能持有的值类型完全一致,如果一致则取出传入的对象的值,放入本对象中
  • 否则对传入的值的可能类型进行检查,如果其类型满足可以被不抛异常的拷贝或者不抛异常的移动,则调用对应类型的emplace操作
  • 否则等价于对给定类型加一层 variant 封装之后的赋值操作

variant 对象也支持重新赋值为一个非variant类型的新值;其赋值逻辑是先从所有的可能的类型中使用类型重载解析来决定可以被转换的类型, 然后判断当前的对象中是否已经持有和选出的类型相同的值 (因为任意时刻其持有的值可能是动态可变的) ,如果是则调用转发赋值; 否则判断是否能用传入参数类型构造出选中的类型的值(可以不抛异常地构造新值或者选中的类型没有不抛异常的移动构造函数), 如果有则相当于对给定类型调用emplace操作; 否则就用variant类型封装传入的参数,再调用前述的两个variant类型的赋值处理逻辑尝试赋值。

考虑如下的例子

variant<string> v1; //holds nothing
v1 = "abc"; //construct string and save it

variant<string, string> v2;
v2 = "abc"; //Error! Resolution fails!

variant<string, bool> v3;
v3 = "abc"; //choose bool, v3 contains true

第二个赋值中,由于选不出一个唯一的重载版本赋值,这里最终会编译出错。第三个赋值中,bool 类型被选中作为 const char* 的接收类型。

特殊的 monostate

由于variant类型要求第一个值类型必须可以默认构造,对于不满足默认构造的类型值,如果想将其放入variant中,则需要下一番功夫。 标准库提供了一个特殊的 monostate 类型用来处理情况,从而我们可以将其放在第一个类型参数处应对上述情况。 即给定如下定义

struct MyData {
    MyData(int value) : v_(value) {};
    int value;
};

//std::varinat<MyData, OtherType> v; wouldn't compile!

我们可以采用变通的方法构造期望的variantstd::variant<std::monostate, MyData, OtherType>; 这样默认构造出来的对象其实包含了一个空的monostate对象。

std::visit函数

该函数其实封装了visitor模式,用于调用一个给定的函数到一系列variant对象上,其签名如下

template <class Visitor, class... Variants>
constexpr /*result of vis call*/ visit(Visitor&& vis, Variants&&... vars);

如下的例子可以打印所有的对象

using var_t = std::variant<int, long, double, std::string>;
std::vector<var_t> vec = {1, 7l, 0.5, "hello!"};
std::visit([](auto&& arg) {
    std::cout << arg << std::end;
}, v);

结合上面的constexpr if 我们可以模拟出类似 Haskell 的类型匹配代码

//helper 
template <class T>
struct AlwaysFalse : std::false_type{};

std::visit([](auto&& arg) {
    using T = std::decay<declytype(arg)>;
    if constexpr (std::is_same_v<T, int>)
        std::cout << "It's a int with value: " << arg << "\n";
    else if constexpr (std::is_same_v<T, long>)
        std::cout << "It's a long with value: " << arg << "\n";
    else if constexpr (std::is_same_v<T, double>)
        std::cout << "It's a double with value: " << arg << "\n";
    else if constexpr (std::is_same_v<T, std::string>)
        std::cout << "It's a string with value: " << arg << "\n";
    else
        static_assert(AlwaysFalse<T>::value, "non-exhaustive value!" << "\n";
}, v);

字符串及工具函数

C++17 将之前放在 experimental 空间中的 string_view 正式标准化,放入了 std 命名空间中。 顾名思义,string_view 类提供了对某个 string (或者 C 的数组) 的一个只读的视图,程序员自己必须保证所引用的 string 对象是有效的。

std::to_charsstd::from_chars 则提供了对字符串和数值类型的相互转换,转换结果存放在一个结构体中,此结构体包含一个指针位置和一个错误码, 如果发生错误,则可以检查转换进行到哪个位置发生了错误。出于性能的考虑,这些转换函数会忽略本地化设置也不会抛出任何异常。 典型的应用场景是诸如XML或者JSON的转换等。

CppCon2017

作为一个紧凑的技术社区大会,很多精彩的演讲材料放出来,各行各业的大牛纷纷登台分享;一个明显的趋势是现代的C++社区更加关注泛型编程和模板元编程, 并更加探索语言所能达到的极限。这里仅挑两个个人感兴趣的琢磨一下。

constexpr 的威力

Ben Deane 和 Jason Turner 提出了一个大胆的想法,期望用编译期计算的方法自动地从一个JSON常量字符串生成一个C++的对象出来,也就是如下的伪代码

constexpr auto jsv
    = R"({
    "feature-x-enabled": true,
    "value-of-y": 1729,
    "z-options": {"a": null,
    "b": "220 and 284",
    "c": [6, 28, 496]}
    })"_json;

if constexpr (jsv["feature-x-enabled"]) {
// code for feature x
} else {
// code when feature x turned off
}

目标

显然他们想到了constexpr给出的更高的执行效率、更清晰的代码、更少跨平台折磨的承诺;进而他们回顾了C++历史上重要版本对 constexpr 从诞生到逐步完善过程中 的特性和限制,进而提出了2个需要解决上述问题的重要挑战

  • 如何采用一致的数据结构来表示 JSON 数据
  • 如何解析json串

从Json的数据要素上来看,无非是空值、bool类型、字符串、数组和字典类型的组合,而字典类型则本身又包含了值类型,因而我们需要递归处理, 并且需要设法将constexpr 应用到所有这些结构上,才能保证编译期和运行期可以采用一致的算法。

原型

最简单的地方显然是从字符串类型入手,而且 string_view 看起来是个不错的选项,可惜某些成员函数不支持 constexpr, 于是他们自己动手。。。 造了一个类似的 static_string 并且对所有的函数加上了constexpr, 看起来还是一切顺利。 接下来一个数据结构是数组,最自然的选择当然是vector 了, 内部可以用 C++11 中引入的 std::array 来做,非常清晰自然。 vector的挑战首先来自于 push_back,一个疑似的 std::next() 的bug被挖出来了,在GCC7.2的实现中, 它的一个内部成员是无法用 constexpr 的方式来构造。

接下来的部分是关于 map 这一类似的数据结构的,否则我们就没法处理字典类型了。 有人可能问为什么不能用 std::pair 类型呢?原因仍然是 既有实现的限制 导致没法用了,因为它内部有个赋值操作不支持 constexpr

搞定了数据结构的部分,剩下的就是算法了;你重写了一些基本容器,当然也需要搞定算法了。 初始版本的自定义容器虽然支持各种constexpr 操作,但他们还不支持运行时缩放(注意编译器运行也是一个特殊的运行时)并且大小是固定的。 他们想到的可能的改进之道是,封装对象到std::optional 中去,和改进 allocator 使其也可以被模板化运算。

解决了这些,剩下的就是解析给定的JSON字符串了。最容易的思路还是从简单的解析器开始,而最简单的解析器莫过于匹配一个给定的字符了。 这一项准备工作完成了对 lambda 表达式的optional封装和 constexpr 化处理。

有了这个解析器,那么其它复杂一点的对字符串处理的解析就不难完成,譬如匹配某些字符中的一个,可以组合字符串函数和字符匹配解析器完成。 不匹配任何一个字符的解析,或者解析匹配给定字符串都可以迎刃而解。不难看出,这一思路是函数式编程的组合式思路。 进而他们将这些基本的函数式编程特性借用过来,使用组装得到更为强大的解析器,即使用 fmap / bind / | /combine, 余下的部分就如砍瓜切菜,上面贴出一些 Haskell 函数,下面就比葫芦画瓢写 C++ 代码出来。

改进和完善

很快一个可以工作的基本原型就有了,基本的概念验证宣告结束;当然也有一些问题出来。 第一个技术性的问题,JSON的数字其实不是一个 int 类型(详情可以翻看JSON的语法定义),乃至JSON的字符串也不能很好的表示为 string_view , 第三个问题则是漫长的编译时间(模板元编程的通病)了;幸好一些优化的手段被发掘出来缓解这些问题。

总结

最后作者们给出了一些结论

  • 所有的标准容器和算法其实都有潜力被改写为 constexpr 友好的
  • 标准库的设施需要通过 constexpr 的测试来发现问题,这里已经发现了几个
  • 许多迭代器算法和结构需要改写为 constexpr
  • lambda 表达式的 constexpr 释放了很多未来的扩展空间

自动假设系统架构中的现代C++规范

Jan Babst探索了自动驾驶领域(AutoSAR)对新的C++编程语言的使用并给出了他们的编程规范和使用建议,非常值得思考和借鉴。

背景

现代的自动驾驶对软件的需求显著有别于传统的汽车控制软件;传统上来说汽车上的软件系统更像是相互隔离的“孤岛”式系统;各个软件模块各司其职即可。 自动驾驶则需要更多的互操作和中心控制,其软件架构更接近于一个小型的分布式系统,同时对性能和时延有极高的要求,稍有延迟则可能引起严重的后果, 这是一个C++占有极大优势的领域,毕竟C++的设计哲学是零成本的抽象。 更多信息可以查看开放自动驾驶系统架构的官方网站

Jan大概简述了这一领域为什么期望现代的C++语言:尽管很多嵌入式开发软件仍然在使用老旧的C++版本,ISO的安全标准建议最好使用更为现代的技术; 新的开发者当然更喜欢一门现代一点的语言而不是20年前的语言(Bjarne也说过需要像学习一门新语言一样学习现代C++), 更不必说新的C++规范提供了很多标准的简化特性诸如自动类型推导、一致的初始化列表、可变长参数模板、并发和并行支持等。 简而言之,不管是从吸引开发者的角度还是提高代码生产率的角度看,使用新标准都好处多多,嵌入式领域也不例外

编程规范

接下来的部分就是对编程规范的选择和思考了,详细的文档在这里, 这里Jan介绍了一些重要的规则及为什么这样选择。

是否坚持单一返回

函数里面是否允许只有一个地方返回,是一个C/C++里很有争议的话题;C++语言的自动析构和异常机制使得这一问题更加突出。 保守的策略是坚持函数只能在一个地方返回以避免多返回带来的不确定行为和维护难题,原因是多处返回和GOTO是一样的, 而伟大的Dijkstra几十年前就提出了有名的论断给GOTO判了死刑,不是吗?

该标准的前一个版本(2008版,在C++11引入之前)是明确建议单一返回原则的,并且将可能抛异常的情况视作例外规则, 认为异常处理过程不属于返回点;这本质上是自相矛盾的,属于无奈之举。

新的版本里他们认为这条应该被废弃并允许多处返回,因为如果使用RAII机制,那么天然就会有多个地方的返回处理; 只要想想自定义对象的析构是应用层代码自己写的就明白,外部使用者在退出作用域的时候就隐式地引入了不同的处理。

代码的复杂性是另外一个问题,强制单一返回势必引入许多额外的条件判断和退出控制逻辑,使得代码的可读性急剧下降, 有时候很难看清楚应该被重点关注的业务逻辑。变量未被初始化引入的未定义行为在复杂的逻辑掩盖下,甚至很难被直接发现; 这些在无形之中都提高了软件维护的成本。

综合来考虑,还是允许可以在多处返回好,代码的清晰度会极大提高,方便代码评审和长期维护。

异常的使用

是否允许使用异常是另外一个极具争议的话题,一方面有Google不许使用异常的先例在前,另一方面的现实情况是, 现代的标准语言库自身就大量使用了异常来处理各个边边角角的情况。 同时 Core Guideline 建议使用异常来传递错误。

AuotSAR的做法是可以使用异常,只要满足

  • 不强制用异常来做错误报告
  • 不能假设代码可以忽略异常
  • 正确使用它并在合适的地方用,譬如不能用异常来控制业务逻辑
  • 在main函数出需要捕获所有可能的异常,避免默认的coredump引起线上事故

其它一些关于异常的思考还包括,如何避免异常的滥用,比如如果函数所作的事情可以正常继续下去,就不要使用异常; 异常安全保证的考虑方面,不强制很高的安全保证等级,仅支持基本的安全约束,因为写出异常安全的代码在C++里面是极其复杂的, 只要想想Herb Sutter在这方面也写了三本书就知道其中的水有多深了。

性能损失上的考量,这个其实是和你所用的编译器密切相关的。幸运的是GCC和Clang这两大主流编译器都提供了保证, 在异常没有被触发的情况下,基本可以做到没有额外开销。同时他们的规范还额外谨慎地加上了如下

  • 最坏的可能情况必须被仔细分析
  • 最坏情况下,行为必须是确定的
动态内存分配

这又是一个嵌入式开发中绕不开的基本问题:很多传统的嵌入式开发都是不允许动态申请和释放内存的; 然而这在现代的C++语言中是几乎不可能的任务,因为标准语言库自身会默默分配、释放内存。 有采用极端策略的规范禁止使用标准库,那么你的工程师得花费大量的时间造一些质量低劣的轮子。 AutoSAR的策略是允许使用动态内存分配,前提是仔细的使用。

首先一个需要考虑的问题是内存泄漏,应对之道是尽可能使用RAII机制来封装,不使用裸的new/delete;毕竟标准库中的智能指针可以满足所有的情况。 内存碎片的问题,可以使用订制内存分配器的方法来缓解,而Jan认为默认的分配器已经足够好了;没有特殊原因可以先从标准库提供的开始; 因为过早优化是万恶之源,怎样界定优化是否太早还是恰当其实是个复杂的问题。 分配器的执行时间必须是确定可分析的是另外一个需要保证的地方;默认的分配器满足这个约束,如果自己订制分配器,那么必须也满足这个要求, 否则会带来意料不到的后果。

其它一些语言特性

Jan 也谈到了其他一些考虑的特性,这里简要记述一些重要的。

现代C++最显著的特性之一就是lambda表达式和函数式编程的支持;他们的策略是可以使用,但是应该谨慎使用

  • 必须显示指定lambda中需要捕获的上下文变量,避免可能的对象生存期的诡异问题
  • 总是显示列出参数列表,即使是空函数;这样可以极大提高代码的可读性
  • 禁止嵌套的lambda表达式,因为可读性实在太差了

自动类型推导是另外一个有趣的语言特性,然而其对代码可读性的影响可以是有好有坏,同样需要谨慎使用

  • 可以用在函数调用返回参数的类型声明上
  • 或者声明一个非基本类型并且这个类型写起来比较臃肿时

AutoSAR也禁用了一些标准库设施以便提高代码的可维护性,譬如基本的原子类型、线程和同步原语等都不让在应用层代码中使用, 也许是考虑到应用层代码的程序员大多很难驾驭这些复杂的语言特性吧。

参考和引用

  1. A tour of C++ 17: if constexpr
  2. CPP Reference
  3. CppCon 2017 Github Repo

Leave a Comment

Your email address will not be published. Required fields are marked *

Loading...