6

[译]C++17, 语言核心层有哪些新的变化?

 3 years ago
source link: https://blog.csdn.net/tkokof1/article/details/81662421
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

[译]C++17, 语言核心层有哪些新的变化?

看到一个介绍 C++17 的系列博文(原文),有十来篇的样子,觉得挺好,看看有时间能不能都简单翻译一下,这是第一篇~

C++11, C++14, 以及 C++17. 我猜你已经看出了其中的命名模式: 今年(2017)的晚些时候,我们便会迎来新的C++标准(C++17). 今年的3月份, C++17已经达到了标准草案阶段. 在我深入新标准的细节之前, 让我们先来总体浏览一下C++17.(译注:作者的文章写于2017年初,当时C++17标准仍未正式发布)

让我们首先来看下C++标准整体的(特性)时间线.

The big picture

这里写图片描述

从 C++98 到 C++14,图中只列出了较大的特性要点.图中也缺少了关于 C++03 的特性描述, 因为C++03标准非常小,内容上更多是为了修复 C++98 的一些缺陷.如果你熟悉C++,那么你一定知道 C++98(第一个C++标准) 和 C++11 是两个非常大的C++标准, 但C++14,特别是C++03则是两个小标准.

那么 C++17 是大标准还是小标准呢?从我的观点来看,答案其实挺简单的: C++17 介于 C++14 和 C++11 之间,既不属于大标准也不属于小标准,至于原因,看看下面的说明吧.

C++17 在语言核心层标准库方面都有很多新改动.我们首先来看下语言核心层.

语言核心层

fold expressions(折叠表达式)

C++11 开始支持可变参数模板(即支持任意多数量参数的模板).其中任意数量的模板参数保存在参数包(parameter pack)中.在C++17中,你可以使用二元运算符直接化简(reduce)参数包:
(译注:译文对作者的原始示例代码做了些许调整,原始代码请参看原文)

#include <iostream>

template<typename... Args>
bool all(Args... args)
{
	return (... && args);
}

int main()
{
	std::cout << std::boolalpha;

	std::cout << "all(): " << all() << std::endl;
	std::cout << "all(true): " << all(true) << std::endl;
	std::cout << "all(true, true, true, false): " << all(true, true, true, false) << std::endl;

	std::cout << std::endl;

	return 0;
}

上述代码中(第6行)使用的二元运算符是逻辑与(&&).程序的输出如下:

输出图

对于折叠表达式我想说的就是这些,如果你想了解更多的细节,可以看看我之前的一篇关于折叠表达式的文章.

我们继续来看看编译期的改动

constexpr if

constexpr if 可以实现源代码的条件编译.

template <typename T>
auto get_value(T t) 
{
	if constexpr (std::is_pointer_v<T>)
		return *t; // deduces return type to int for T = int*
	else
		return t;  // deduces return type to int for T = int
}

如果 T 是指针类型,那么上述代码中的第5行分支就会被编译,反之则编译第7行的代码分支.这里有两个要点: 函数 get_value 有两种不同的返回类型并且 if 语句的两个分支都必须有效.

在C++17中, for 语句的语法同样适用于 if 和 switch 语句了.

initializers in if and switch statements

现在你可以直接在 if 和 switch 语句中初始化变量了.

std::map<int, std::string> myMap;

if (auto result = myMap.insert(value); result.second) 
{
	useResult(result.first);
	// ...
}
else 
{
	// ...
} // result is automatically destroyed

初始化的变量仅在对应的 if 和 else 语句的作用域内有效,不会影响到外层作用域.

如果我们再结合使用一下C++17中新引入的结构化绑定声明(structured binding declaration),那么语法会更加优雅.

structured binding declaration(结构化绑定声明)

借助结构化绑定,我们可以直接将 std::tuple 或者某个结构的元素绑定到变量上去,让我们用结构化绑定声明来改写一下之前的示例代码:

std::map<int, std::string> myMap;

if (auto[iter, succeeded] = myMap.insert(value); succeeded) 
{
	useIter(iter);
	// ...
}
else 
{
	// ...
} iter and succeded are automatically be destroyed

第3行的 auto [iter, succeeded] 自动创建了两个变量(iter 和 succeeded),他们会在第 11 行代码执行中(离开if的作用域)被销毁.

结构化绑定声明可以简化代码,构造函数的模板参数推导同样也可以.

Template deduction of constructors(构造函数的模板参数推导)

一个函数模板可以通过传递的函数参数进行参数的类型推导,但这条规则对于一个特殊的函数模板却不适用:类模板的构造函数.在 C++17 中,类模板的构造函数也能进行参数的类型推导了:

#include <iostream>

template <typename T>
void showMe(const T& t) 
{
	std::cout << t << std::endl;
}

template <typename T>
struct ShowMe 
{
	ShowMe(const T& t) 
	{
		std::cout << t << std::endl;
	}
};

int main()
{
	std::cout << std::endl;

	showMe(5.5);  // no need showMe<double>(5.5);
	showMe(5);    // no need showMe<int>(5);

	ShowMe(5.5);  // with C++17: no need ShowMe<double>(5.5);
	ShowMe(5);    // with C++17: no need ShowMe<int>(5);

	std::cout << std::endl;

	return 0;
}

22行和23行代码从C++第一个标准开始(C++98)便是合法的,但是25行及26行代码则只能在C++17中编译通过,因为在C++17之前,你必须使用尖括号(<>)来指定需要实例化的类模板的类型参数.

除了功能特性,C++17中还有一些旨在提升代码运行效率的特性.

guaranteed copy elision

RVO是返回值优化(Return Value Optimisation)的简称,他的作用是允许编译器移除一些不必要的复制操作,但RVO一直都只是一种可能优化步骤(并没有标准规范,编译器可以选择进行RVO或者不进行RVO),C++17中通过定义 guaranteed copy elision 保证了这种优化的执行.

MyType func()
{
    return MyType{};         // no copy with C++17
}

MyType myType = func();    // no copy with C++17

在这几行代码的执行中可能会发生2次不必要的复制操作.第1次发生在第3行,第2次则发生在第6行.但在C++17中,这2次多余的复制操作都(保证)不会发生.

如果返回值有名称,我们便称他为NRVO(Named Return Value Optimization,命名返回值优化):

MyType func()
{
    MyType myVal;
    return myVal;            // one copy allowed 
}

MyType myType = func();    // no copy with C++17

上述代码(第4行)与之前代码的一个细微差别是:在C++17中,编译器仍然可以执行一次 myVal 的复制操作(也可以不执行复制),但第7行代码仍然保证不会发生复制操作.

如果你不再需要某个特性,甚至于某个特性可能会造成"危险",那么你就应该移除他.C++17中就移除了auto_ptr 和 trigraphs 这两个语言特性.

移除 auto_ptr 和 trigraphs

auto_ptr

std::auto_ptr 是C++标准中第一个智能指针,他的设计目的是为了正确的管理资源.但是他存在一个很大的缺陷: std::auto_ptr 可以进行复制(和赋值)操作,但内部执行的却是移动(move)操作!(译注:意为 std::auto_ptr 复制(和赋值)操作会改变源操作数的内部数据,因此其不能进行逻辑独立的复制(和赋值)操作,也是因为这个原因, std::auto_ptr 不能作为标准库容器的元素).正因为 std::auto_ptr 的这个缺陷, C++11 中作为替代引入了不可复制(只可移动)的 std::unique_ptr.

std::auto_ptr<int> ap1(new int(2011));
std::auto_ptr<int> ap2= ap1;              // OK     (1)

std::unique_ptr<int> up1(new int(2011));
std::unique_ptr<int> up2= up1;            // ERROR  (2)
std::unique_ptr<int> up3= std::move(up1); // OK     (3)
trigraphs(三字符组)

所谓三字符组(trigraphs),是指源代码中由特定的3个字符组成的转义字符序列(该转义序列用以表达某个单字符),目的是解决一些键盘不能输入某些特殊字符的问题.

C++ 中移除了三字符组(trigraphs),这意味着你不能使用C++17写出下面这种"混乱"的代码了:

int main()
??<
  ??(??)??<??>();
??>

我猜你也许能看懂上面的代码,如果不能的话,你就必须把其中的三字符组(trigraphs)转成对应的单字符了.

这里写图片描述

如果你对着上面的表格进行了转换,你会发现上面的代码实际上就是定义了一个就地执行(just-in-place)的 lambda 函数.

int main()
{
  []{}();
}

(译注:文章中的不少说明涉及到了代码行号,但译文中的示例代码并没有行号显示,原因是自己未找到markdown中源码显示行号的简易方法,有知道的朋友可以告诉一声)


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK