探索C++14新特性:更强大、更高效的编程
C++14并没有太大的改动,就连官方说明中也指出,C++14相对于C++11来说是一个比较小的改动,但是在很大程度上完善了C++11,所以可以说C++14就是在C++11标准上的查漏补缺。
C++14在2014年8月18日正式批准宣布,同年12月15日正式发布release版本。本文中将就变动部分做一个总结,有需要改进和提升的地方希望大家批评指正。
1、引言
1.1新的语言特性
- 变量模板
- 泛型 lambda
- lambda 初始化捕获
- 新建/删除省略
- 放宽对 constexpr 函数的限制
- 二进制文字
- 数字分隔符
- 函数的返回类型推导
- 具有默认非静态成员初始值设定项的 聚合类。
1.2新库功能
-
std::make_unique
-
std::shared_timed_mutex 和 std::shared_lock
-
std::整数序列
-
标准::交换
-
std::引用
-
以及对现有图书馆设施的许多小改进,例如
-
某些算法的两范围重载
-
类型特征的类型别名版本
-
用户定义的basic_string、持续时间和复杂的文字
-
ETC。
-
2、变量模板
在C++11 及之前,我们只有针对类和函数的模板。C++14 中,新增了变量模板:
template<class T>
constexpr T pi = T(3.1415926535897932385L);
template<class T>
T circular_area(T r)
{
return pi<T> *r * r;
}
变量模板同样可以在类变量中使用:
template<class T>
class X {
static T s;
};
template<class T>
T X<T>::s = 0;
X<int> x_int;
X<float> x_float;
int main() {
int value_int = X<int>::s; // 获取 int 类型的静态成员变量值
float value_float = X<float>::s; // 获取 float 类型的静态成员变量值
// ...
return 0;
}
类似函数和类模板,当变量模板被引用时,则会发生实例化。
3、lambda 表达式的新增功能
3.1 泛化
支持在 lambda 表达式中使用 auto 定义变量类型:这一特性允许 Lambda 的形参或者内部变量的类型由编译器自动推导,而不必显式指定。
#include <iostream>
#include <algorithm>
int main() {
// Lambda 表达式 glambda,用于输出元素
auto glambda = [](auto& a) { std::cout << a << " "; };
int a[] = { 4, 2, 6, 3, 7, 5 };
// 使用 for_each 算法遍历数组,并对每个元素调用 Lambda 表达式 glambda
std::for_each(a, a + sizeof(a) / sizeof(int), glambda);
std::cout << std::endl;
return 0;
}
3.2 对捕获的变量和引用进行初始化
include <iostream>
using namespace std;
int main()
{
int x = 4;
auto y = [&r = x, x = x + 1]()->int
{
r += 2;
return x * x;
}();
cout << "x = " << x << " y = " << y << endl;
}
// 输出结果:x = 6 y = 25
auto y = [&r = x, x = x + 1]() -> int { ... }();
定义了Lambda表达式并立即调用。
[&r = x, x = x + 1]
:捕获了变量x
,通过引用捕获了r
,同时对x
进行了初始化,将x
的值增加了1。()->int { r += 2; return x * x; }
:Lambda表达式的主体,对捕获的变量进行操作。r
是通过引用捕获的,所以对r
的修改会影响到外部的x
。Lambda表达式返回x * x
的结果。
4、constexpr 函数可以包含多个语句
在 C++11 中,如果想使用 constexpr 方法,只能包含一个返回语句。
#include <iostream>
constexpr int square(int x) {
return x * x;
}
int main() {
constexpr int result = square(5); // 合法的 constexpr 函数调用
// 在 C++11 中,下面的调用将导致编译错误
// constexpr int invalidResult = []() {
// int sum = 0;
// for (int i = 1; i <= 5; ++i) {
// sum += i;
// }
// return sum;
// }();
std::cout << "Result: " << result << std::endl;
return 0;
}
C++14 中,放宽了此要求,允许 constexpr 函数中声明变量,使用循环和条件语句等:
#include <iostream>
#include <cmath>
using namespace std;
constexpr bool isPrimitive(int number) {
if (number <= 0) {
return false;
}
for (int i = 2; i <= sqrt(number) + 1; ++i) {
if (number % i == 0) {
return false;
}
}
return true;
}
int main() {
cout << boolalpha << isPrimitive(102) << " " << isPrimitive(103);
return 0;
}
需要注意的是,虽然C++14对constexpr
函数的要求放宽了一些,但仍然有一些限制。例如,递归和复杂的控制流结构可能仍然无法在constexpr
函数中使用。
在C++11中,我们一般需要通过递归来实现相同的功能:
constexpr bool isPrimitive(int number, int currentFactor, int maxFactor) {
return currentFactor == maxFactor ? true :
(number % currentFactor == 0 ? false :
isPrimitive(number, currentFactor + 1, maxFactor));
}
constexpr bool isPrimitive(int number) {
return number <= 0 ? false : isPrimitive(number, 2, sqrt(number) + 1);
}
5、整型字面量
5.1 二进制字面量
支持使用 0b 开头的一串数字作为二进制表示的整型:
int a = 0b10101001110; // 1358
5.2 数字分隔符
支持在数字中使用单引号进行分割(便于阅读)。在编译时,这些单引号会被忽略。
int a = 123'456'789; // 123456789
6、返回类型自动推导
在 C++14 中,我们可以使用 auto 作为函数返回值并且不需要指明其返回类型的推导表达式
int x = 1;
auto f() { return x; }
/* c++11
auto f() -> decltype(x) { return x; }
*/
这种类型推导有一些限制:
-
相同类型的推导: 在一个函数中,所有的返回语句必须推导出相同的类型。
int x = 1; auto f() { return x; } // 合法
-
使用
{}
包裹的数据: 对于使用{}
包裹的数据作为返回值时,无法推导其类型。auto g() { return {1, 2, 3}; } // 不合法
-
虚函数和 coroutine 不能被推导: 虚函数和协程不能使用此类型推导。
-
函数模板中的类型推导: 函数模板中可以使用类型推导,但显式实例化和特化版本必须使用相同的返回类型描述符。
template <typename T> auto h(T value) { return value; } template auto h<int>(int); // 合法
这些限制是为了保证类型推导的一致性和可靠性。虽然C++14引入了更多的自动类型推导,但在一些情况下,显式指定返回类型仍然是必要的。
7、exchange
exchange 用于移动语义,可以使用指定的新值替换掉原值,并返回原值。其定义在C++20中被简单修改如下:
template<class T, class U = T>
constexpr // since C++20
T exchange(T& obj, U&& new_value)
{
T old_value = std::move(obj);
obj = std::forward<U>(new_value);
return old_value;
}
其使用如下:
#include <iostream>
#include <vector>
#include <utility>
using namespace std;
int main() {
vector<int> v = {5, 6, 7};
std::exchange(v, { 1,2,3,4 });
std::copy(begin(v), end(v), ostream_iterator<int>(cout, " "));
cout << endl;
}
8、quoted
该类用于字符串转义的处理。使用 out << quoted(s, delim, escape)
的形式,可以将字符串 s 的转义格式写入输出流中;
使用 in >> quoted(s, delim, escape)
可以将输入流去除转义格式后写入字符串 s 中。其中,delim 指明了需要转义的字符,escape 指明了修饰该转移字符的字符:
#include <iostream>
#include <iomanip>
#include <sstream>
using namespace std;
int main() {
stringstream ss;
string in = "String with spaces, and embedded \"quotes\" too";
string out;
auto show = [&](const auto& what) {
&what == &in
? cout << "read in [" << in << "]\n"
<< "stored as [" << ss.str() << "]\n"
: cout << "written out [" << out << "]\n\n";
};
ss << quoted(in);
show(in);
ss >> quoted(out);
show(out);
ss.str("");
in = "String with spaces, and embedded $quotes$ too";
const char delim{ '$' };
const char escape{ '%' };
ss << quoted(in, delim, escape);
show(in);
ss >> quoted(out, delim, escape);
show(out);
}
9、C++14 经常考到的知识点
9.1 C++14 引入了哪些新特性?
C++14 引入了一些新特性,包括但不限于以下内容:
- 通用Lambda表达式:允许在lambda函数中使用auto关键字来推导参数类型。
- 自动返回类型推导:允许使用auto关键字自动推导函数返回值类型。
- 初始化列表的泛型支持:可以使用auto关键字在初始化列表中推导元素类型。
- 带有二进制分隔符的整数字面量:可以在整数常量中使用单撇号作为分隔符,提高可读性。
- constexpr函数的扩展:constexpr函数可以包含更多操作,例如循环和条件判断。
- 变长参数模板(Variadic Templates)的改进:支持递归处理变长参数模板的展开。
- 返回void类型的lambda表达式:允许定义返回void类型的lambda函数。
9.2 C++14 中 auto 关键字的用法和限制是什么?
在 C++14 中,auto 关键字用于自动类型推导,可以根据初始化表达式的类型来确定变量的类型。它的使用和限制如下:
- 自动类型推导:使用 auto 关键字声明变量时,编译器会根据初始化表达式的类型自动推导出变量的类型。
auto x = 42; // 推导为int型 auto name = “John”; // 推导为const char*型 - 声明时必须初始化:使用auto声明变量时,必须进行初始化。因为编译器需要根据初始化表达式来推导出变量的类型。
auto y; // 错误,未初始化 - 可与引用结合使用:auto关键字可以与引用一起使用,从而推导出引用的类型。
int a = 10; auto& ref = a; // 推导为int&型,ref是a的引用 - 不支持数组或函数指针:auto不能直接用于数组或函数指针的声明。但可以通过decltype结合auto来实现对数组或函数指针类型进行推导。
int arr[] = {1, 2, 3}; auto arrRef = arr; // 错误,无法推导arr的数组类型 decltype(arr) arrType; // 使用decltype获取arr的数组类型并声明arrType void foo(); auto funcPtr = foo; // 错误,无法推导foo的函数指针类型 decltype(foo)* funcPtrType; // 使用decltype获取foo的函数指针类型并声明funcPtrType
需要注意的是,auto 在 C++14 中的用法和限制可能与之后的标准(如 C++17、C++20 等)有所不同,具体取决于编译器和所使用的标准版本。
9.3 C++14 中如何使用 Lambda 表达式?有什么改进?
在C++14中,使用Lambda表达式的语法与之前的C++版本相似。Lambda表达式是一种可以在代码中内联定义匿名函数的方式。
下面是一个使用Lambda表达式的示例:
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};
// 使用Lambda表达式进行遍历打印
std::for_each(numbers.begin(), numbers.end(), [](int num) {
std::cout << num << " ";
});
return 0;
}
在 Lambda 表达式中,方括号 []
用于捕获外部变量(可选)。小括号 ( )
内指定参数列表(可选),箭头 ->
后面指定返回类型(可选)。
C++14 对于 Lambda 表达式有一些改进,其中最显著的改进是可以自动推导返回类型。这意味着你不需要显式地指定返回类型,编译器会根据表达式体来推断返回类型。
以下是一个示例:
auto lambda = [](int a, int b) {
return a + b;
};
在上述示例中,我们没有显式指定返回类型,但编译器会自动推断出返回类型为整数(因为a和b都是整数)。
此外,在 C++14 中还引入了泛型 lambda,使得可以在 lambda 函数中使用 auto 关键字作为参数类型,更加灵活和方便。
9.4 C++14 对于 constexpr 关键字有何改进?
C++14 对于 constexpr
关键字进行了一些改进,使得其更加灵活和强大。在 C++11 中,constexpr
只能用于表示常量表达式的函数和构造函数,而在 C++14 中,它还可以用于一些额外的情况。
首先,在 C++14 中,constexpr
函数可以包含一些非常量表达式的逻辑,只要这部分逻辑在运行时不会执行即可。这意味着我们可以在constexpr
函数内使用循环、条件语句等非常量表达式的控制流程。
其次,C++14引入了对变量模板(Variable Templates)的支持,并且允许将变量声明为constexpr
。这样我们就可以定义并初始化一个编译期间可计算的常量变量。
此外,在 C++14 中,对于某些标准库类型(如数组、字符串等),它们也提供了更多的支持以便于使用在编译期间计算出来的常量值。
9.5 C++14 中提供了哪些新的标准库组件和功能?
C++14引入了一些新的标准库组件和功能,以下是其中的一些主要特性:
- std::make_unique:提供了在堆上创建 unique_ptr 对象的便捷方式。
- std::integer_sequence:支持编译时整数序列的操作,用于元编程。
- std::user_defined_literals:允许用户定义自己的字面量后缀,扩展了语言的表达能力。
- 通用 lambda 表达式:允许使用 auto 参数声明参数类型,使得 lambda 表达式更加灵活。
- 变长模板参数折叠(Variadic template parameter packs expansion):可以将多个参数打包传递给模板函数或类,并且可以对它们进行展开操作。
- std::experimental 命名空间:引入了一些实验性质的标准库组件,如 optional、any、string_view 等。
9.6 在 C++14 中,变长参数模板是如何使用的?
在 C++14 中,可以使用变长参数模板(Variadic Templates)来处理可变数量的函数参数。通过使用递归展开参数包的方式,可以灵活地处理任意数量的参数。
下面是一个示例:
#include <iostream>
// 递归终止条件:当没有剩余参数时停止递归
void printArgs() {
std::cout << "All arguments have been printed." << std::endl;
}
// 可变参数模板:展开第一个参数并调用自身处理剩余参数
template<typename T, typename... Args>
void printArgs(T first, Args... args) {
std::cout << "Argument: " << first << std::endl;
printArgs(args...); // 递归调用自身处理剩余参数
}
int main() {
printArgs(1, "Hello", 3.14, 'A');
return 0;
}
输出结果:
Argument: 1
Argument: Hello
Argument: 3.14
Argument: A
All arguments have been printed.
在上述代码中,printArgs
是一个可变参数模板函数。它首先处理第一个传入的参数 first
,然后递归地调用自身处理剩余的 args
参数。当所有参数都被展开并打印完毕后,最终会到达递归终止条件。
这种方式使得我们能够在编译时处理不同数量和类型的函数参数,并且可以灵活地进行操作。
9.7 在 C++14 中,是否允许在 lambda 函数内定义其他函数或类?
在 C++14 中,lambda 函数内是不允许定义其他函数或类的。Lambda 函数是一个匿名的函数对象,它通常用于简化代码,提供一种在局部范围内编写小型函数的方式。Lambda 函数本质上是一个闭包,它可以捕获外部作用域中的变量,并且具有与普通函数相似的行为。
然而,在 C++17 中引入了嵌套lambda的概念,使得在 lambda 函数内定义其他 lambda 函数成为可能。在这种情况下,内层的 lambda 函数可以访问外层 lambda 函数的变量。所以如果你想要在 C++14 中定义其他函数或类,建议将其定义在 lambda 之外的范围内。
9.8 C++14 是否支持原始字符串字面量(raw string literals)?如何使用它们?
是的,C++14 支持原始字符串字面量(raw string literals)。
原始字符串字面量可以用来表示包含特殊字符(例如转义序列和引号)的字符串,而无需使用转义符号。它们由R"delim(raw_characters)delim"的语法表示,其中delim可以是任何非空字符序列,并且在开始和结束位置上必须匹配。
以下是一个示例:
#include <iostream>
int main() {
const char* str1 = R"(Hello \n World!)";
std::cout << str1 << std::endl; // 输出:Hello \n World!
const char* str2 = R"###(This is a "quoted" string.)###";
std::cout << str2 << std::endl; // 输出:This is a "quoted" string.
return 0;
}
在上面的示例中,我们使用了原始字符串字面量来创建包含特殊字符的字符串,而不需要使用额外的转义符号。
9.9 在 C++14 中,std::make_unique和std::make_shared这两个函数的作用是什么?
在 C++14 中,std::make_unique
和std::make_shared
是用于创建智能指针的函数模板。
std::make_unique
:用于创建一个std::unique_ptr
对象,它拥有独占所有权的动态分配对象。这个函数接受参数并返回一个std::unique_ptr
,它会自动管理内存释放。示例:
auto ptr = std::make_unique<int>(42);
std::make_shared
:用于创建一个std::shared_ptr
对象,它可以被多个指针共享的动态分配对象。这个函数接受参数并返回一个std::shared_ptr
,它使用引用计数来管理内存释放。示例:
auto ptr = std::make_shared<int>(42);
这两个函数可以减少手动进行资源管理的工作量,并提供了更安全、更简洁的方式来处理动态分配对象。
9.10 C++14 引入了统一初始化语法(uniform initialization syntax),具体有哪些变化?
C++14 引入了统一初始化语法(uniform initialization syntax),它允许使用一种更统一和一致的方式进行初始化。具体的变化包括以下几个方面:
- 初始化列表(initializer list):可以使用花括号
{}
来初始化对象,无论是简单类型还是复杂类型。例如:
int num{ 42 }; std::vector vec{ 1, 2, 3 }; - 自动类型推导:在使用统一初始化语法时,编译器可以自动推导出变量的类型。
auto value{ 3.14 }; // 推导为 double 类型 auto str{ “Hello” }; // 推导为 const char[6] 类型 - 统一构造函数调用语法:通过统一初始化语法,可以直接调用类的构造函数进行对象的创建。
class MyClass { public: MyClass(int value) { /* 构造函数实现 */ } // … }; MyClass obj{ 42 }; // 调用构造函数创建对象 - 空初始化:可以使用
{}
或()
进行空初始化,不再需要显式地指定默认值。
int num{}; // 初始化为0 std::string str{}; // 初始化为空字符串
zer list):可以使用花括号 {}
来初始化对象,无论是简单类型还是复杂类型。例如:
int num{ 42 }; std::vector vec{ 1, 2, 3 };
2. 自动类型推导:在使用统一初始化语法时,编译器可以自动推导出变量的类型。
auto value{ 3.14 }; // 推导为 double 类型 auto str{ “Hello” }; // 推导为 const char[6] 类型
3. 统一构造函数调用语法:通过统一初始化语法,可以直接调用类的构造函数进行对象的创建。
class MyClass { public: MyClass(int value) { /* 构造函数实现 */ } // … }; MyClass obj{ 42 }; // 调用构造函数创建对象
4. 空初始化:可以使用 {}
或 ()
进行空初始化,不再需要显式地指定默认值。
int num{}; // 初始化为0 std::string str{}; // 初始化为空字符串
这些变化使得初始化更加灵活和一致,并且提供了更强大的类型推导能力。注意,在使用统一初始化语法时,要注意类型的精确匹配和可能的隐式转换。