看到一个介绍 C++17 的系列博文(原文),有十来篇的样子,觉得挺好,看看有时间能不能都简单翻译一下,这是第一篇~
C++11, C++14, 以及 C++17. 我猜你已经看出了其中的命名模式: 今年(2017)的晚些时候,我们便会迎来新的C++标准(C++17). 今年的3月份, C++17已经达到了标准草案阶段. 在我深入新标准的细节之前, 让我们先来总体浏览一下C++17.(译注:作者的文章写于2017年初,当时C++17标准仍未正式发布)
让我们首先来看下C++标准整体的(特性)时间线.
从 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 在语言核心层和标准库方面都有很多新改动.我们首先来看下语言核心层.
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 可以实现源代码的条件编译.
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 语句了.
现在你可以直接在 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),那么语法会更加优雅.
借助结构化绑定,我们可以直接将 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的作用域)被销毁.
结构化绑定声明可以简化代码,构造函数的模板参数推导同样也可以.
一个函数模板可以通过传递的函数参数进行参数的类型推导,但这条规则对于一个特殊的函数模板却不适用:类模板的构造函数.在 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中还有一些旨在提升代码运行效率的特性.
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 这两个语言特性.
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),是指源代码中由特定的3个字符组成的转义字符序列(该转义序列用以表达某个单字符),目的是解决一些键盘不能输入某些特殊字符的问题.
C++ 中移除了三字符组(trigraphs),这意味着你不能使用C++17写出下面这种"混乱"的代码了:
int main()
??<
??(??)??<??>();
??>
我猜你也许能看懂上面的代码,如果不能的话,你就必须把其中的三字符组(trigraphs)转成对应的单字符了.
如果你对着上面的表格进行了转换,你会发现上面的代码实际上就是定义了一个就地执行(just-in-place)的 lambda 函数.
int main()
{
[]{}();
}
(译注:文章中的不少说明涉及到了代码行号,但译文中的示例代码并没有行号显示,原因是自己未找到markdown中源码显示行号的简易方法,有知道的朋友可以告诉一声)