掌握这7个技巧,轻松实现C++新功能!

发表时间: 2018-09-10 11:14

你是否希望,在生产代码中,拥有更高版本的C ++?今天很多C ++开发人员,用的编译器,都不支持最新版本的标准。

其中可能有很多原因,也许你或你的客户,有很多遗留代码需要移植,也许你的硬件,没有足够的基础设施。

关键在于,语言提供的最新功能,并不能给大家带来好处,而且很遗憾的是,其中一些功能,肯定会让代码更具表现力。

但是,即使你无法使用这些功能,也不一定要放弃它们的好处。至少不用放弃全部。 有一些方法可以使用代码中新功能的思路,更准确地传达你的意图。

当然,这些方法肯定不如使用新版本C++本身的功能那么好,这就是你还是需要更新编译器的原因。 但与此同时,我将介绍7种方法来模拟这些功能,以最低的成本改进你的代码。

= default, = delete

在C++ 11中,= default可以向编译器发出指令生成以下内容之一:

• 一个默认的构造函数

• 一个拷贝构造函数;

• 一个拷贝赋值运算符;

• 一个移动构造函数;

• 一个移动赋值运算符;

• 一个析构函数。

在某些情况下,编译器无论如何都会生成这些函数。但是对于C++ 11,一些开发人员喜欢在他们的界面中表现这一点,以向读者保证他们知道这些方法是自动生成的,并且这也是他们想要的类。

在C++ 11之前没有办法用原生的方法表现这一点。但你照样可以在注释中注明:

class X{ /* X(const X& other) = default; */ /* X& operator=(const X& other) = default; */ /* ~X() = default;*/ // rest of X ...};

类似地,为了阻止编译器生成这些函数,在C++ 11之前我们不得不将它们声明为private,并且不实现它们:

class X{ // rest of X ...private: X(const X& other); X& operator=(const X& other);};

在C++ 11中,我们可以将它们声明为public,并通过“= delete”禁止编译器生成这些函数。

在C++ 11之前,我们需要更加明确,不仅需要声明为private,还需要设置“= delete”(但不是真的设置,只是加注释):

class X{ // rest of X ...private: X(const X& other) /* = delete */; X& operator=(const X& other) /* = delete */;};

标准算法

实用的STL算法库随着新版本C++的出现而不断发展,不断加入新算法。其中一些算法非常泛用。例如copy_if,或all_of,以及其类似的any_of和none_of。

听起来令人惊讶,但在C++ 11之前它们并不是标准算法。

但是在C++ 11之前的代码库中访问它们的方法非常简单:只需去某个参考网站(例如cppreference.com)上复制它们的实现方法(copy_if的实现:
https://en.cppreference.com/w/cpp/algorithm/copy;all_of及其类似的算法的实现:
https://en.cppreference.com/w/cpp/algorithm/all_any_none_of),然后把实现方法粘贴到你的代码就可以了。整个操作大约需要10秒钟,通过在代码中使用它们可以节省更多时间。

属性

属性是方括号之间的关键字:[[example_attribute]]。它们是C++ 11中引入的,在C++ 17中更多属性被加了进来。对于属性的深入分析,你可以参照Bartek的文章《详解C++ 17:属性》(
https://www.bfilipek.com/2017/07/cpp17-in-details-attributes.html),但属性的一般概念是你可以将它们当作代码中的标记,用以向阅读代码的人和编译器表达你的意图。

我们以[[fallthrough]]属性为例。这个属性可以在switch语句中使用,假设你故意没有在其中一个case中加break,那么为了执行如下case代码:

switch (myValue){ case value1: { // do something break; } case value2: { // do something } case value3: { // do something break; }}

注意这里的case Value2没有break。这不免让人担心会出bug。大多数情况下它就是个bug,除非你想同时执行case Value2和其他case语句。如下所示,[[fallthrough]]可以更明确地表达这一点:

switch (myValue){ case value1: { // do something break; } case value2: { // do something [[fallthrough]]; } case value3: { // do something break; }}

它可以防止编译器报错,也可以向其他开发人员表明:你在写这段代码的时候,知道自己在干什么。

在C++ 17之前,如果你想利用这个技巧来省略break的话,那么尽管依然会收到警告,但是至少你可以通过[[fallthrough]]向其他开发者表明你的意图:

switch (myValue){ case value1: { // do something break; } case value2: { // do something //[[fallthrough]]; } case value3: { // do something break; }}

C++ 11和C++ 17的其他属性也有类似的功能。

概念

概念是C++非常令人期待的特性,它通常应该属于C++ 20的一部分。概念本质上是模板的接口。概念允许编写比typename更精确的东西来定义模板参数。实际上,typename仅表示“这是一种类型”,却并没有说明该类型的任何其他内容。

像Iterator这样的概念应该替换模板代码中操作迭代器的typename,而且Iterator应该被定义为拥有特定的操作(递增,解引用等)。传递没有这些特定操作的类型将会造成编译错误,并产生明确的错误消息,以解释为什么该类型不是预期的Iterator。

我不打算想你介绍如何在C++语言引入这些之前,自行模拟概念。这是一个非常棘手的事情,如果你想了解实现方法,那么可以看看range-v3(
https://github.com/ericniebler/range-v3),它使用非常先进的技术来模拟这个功能。

我建议你用更容易方法:谨慎选择模板参数名称,并尽可能使用概念的名称。即使你无法在拥有概念之前替换typename,但是你依然有很大的自由来选择类型参数的名称。

以在为Iterator示例时,不要把将模板参数命名为typename命名为T或typename I,而是命名为使用typename Iterator。我们永远不会因为某个变量是int而叫它int i,但对于模板类型,面对模板类型时我们会更倾向于这么做。

模板类型的名称在模板代码中到处都是,所以让我们给它取一个好名字,并使用正在开发的概念的标准名称。当C++(以及我们的代码库)实际引入概念时,良好的命名可以让我们的代码非常妥帖。

范围算法

STL是一个很棒的库,但有个东西用起来有点麻烦:迭代器。实际上,每个STL都接受两个迭代器,以定义算法需要操作的输入范围。

当你需要将算法应用在范围的一部分上时,这个功能很有用,但如果要遍历整个范围(绝大多数情况下如此),迭代器就很碍事了:

auto positionOf42 = std::find(begin(myCollection), end(myCollection), 42);

如果能将范围作为整体传递就会方便许多:

auto positionOf42 = std::find(myCollection, 42); 

这就是有关范围的提案在C++ 20里的目标(同时还有许多其他功能)。但这个功能即使在C++ 98中也很容易模拟,只需要将调用STL算法的语句包裹在一个接受范围的函数中即可:

template<typename Range, typename Value>typename Range::iterator find(Range& range, Value const& value){ return std::find(begin(range), end(range), value);}template<typename Range, typename Value>typename Range::const_iterator find(Range const& range, Value const& value){ return std::find(begin(range), end(range), value);}

模拟标准组件的函数库

与上面包裹算法的函数相比,一些标准库组件更难实现,因此在代码中模拟需要更多的工作。

比如std::optional,或std::variant,这两者出现在C++ 17中。如果你没有C++ 17,那么想要编写自己的实现并可靠地替换标准库的接口并通过完整的测试,并不是件容易的事情。

幸运的是,我们不需要自己这么干,因为有人帮你做好了。

仅次于标注库的就是Boost。它实现了一些组件,包括Optional、Variant以及一些更先进的STL算法。但是,要注意Boost库的接口可能会烟花,因为Boost更关注于压榨语言本身的能力,而不是尽一切可能保持向后兼容。

而且,一些标准库与Boost中的相应部分有这不小的区别。例如,boost::optional接受引用类型,但std::optional不接受。所以std::optional并不能在任何情况下无缝替换boost::optional。

其他函数库也在C++ 11上提供C++ 17的标准组件,如Google的Abseil(https://abseil.io/)。Abseil的网站声称,“Google开发了许多抽象,许多都与C++ 14、C++ 17以及后续版本的功能一致,或者类似。使用Abseil版的抽象可以让你立即体验这些新功能,即使你的代码还没准备好享受C++ 11后的世界。”

在其源代码中,我们确实能看到一些组件会在标准库函数存在的情况下解析成它们的别名(
https://github.com/abseil/abseil-cpp/blob/master/absl/types/optional.h#L48)。

元类

从时间上来看这也许是最古老的提案,但也是C++社区中最流行的提案。元类(Metaclass,
https://www.fluentcpp.com/2017/08/04/metaclasses-cpp-summary/)允许在编译时定义类,在struct和class之外进一步扩展了类型定义的手段。

该提案的一个标准里子就是interface元类,允许使用interface关键字定义接口的方法,而编译器会考虑写虚描述符、将方法设置为纯虚方法、确保没有数据或私有成员等问题,简单来说就是符合接口的一切特征。

代码看起来像这样:

interface Player{ void play(); void pause(); void stop();};

相反,我们现在写接口的方式如下:

class Player{public:virtual void play() = 0;virtual void pause() = 0;virtual void stop() = 0;virtual ~Player() = 0;};

现在没什么好办法模拟元类,但我们可以让它看起来像是个interface元类,来表明我们的意图:

class /* interface */ Player{public: virtual void play() = 0; virtual void pause() = 0; virtual void stop() = 0; virtual ~Player() = 0;};

这不需要任何代价,但能为下个阅读代码的人提示你的意图。而且对于其他提案中的元类也是如此。

早晚你还是需要升级

以上7条技巧能以最小的代价,可以立即给你带来现代(甚至后现代)C++的好处。至少,比你现在升级编译器的代价要小得多。它们还能让你练习并熟悉C++后续版本的特性。

但这并不意味着你应当浅尝辄止。这只是现代C++的一点体验,而C++每三年就会有一次进步。如果不想被时代抛弃,就要升级编译器,然后再模拟最新的功能,再升级,再模拟……

这是一场与现代代码的永无止境的竞赛,我们需要一起加油。

原文:
https://www.fluentcpp.com/2018/08/31/modern-cpp-fake-it-until-you-have-it/

作者:Jonathan Boccara,Murex的C++开发人员。

译者:弯月,责编:胡巍巍