探索C++14新特性:更强大、更高效的编程

探索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; } 
*/

这种类型推导有一些限制:

  1. 相同类型的推导: 在一个函数中,所有的返回语句必须推导出相同的类型。

    int x = 1;
    auto f() { return x; }  // 合法
    
  2. 使用 {} 包裹的数据: 对于使用 {} 包裹的数据作为返回值时,无法推导其类型。

    auto g() { return {1, 2, 3}; }  // 不合法
    
  3. 虚函数和 coroutine 不能被推导: 虚函数和协程不能使用此类型推导。

  4. 函数模板中的类型推导: 函数模板中可以使用类型推导,但显式实例化和特化版本必须使用相同的返回类型描述符。

    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 引入了一些新特性,包括但不限于以下内容:

  1. 通用Lambda表达式:允许在lambda函数中使用auto关键字来推导参数类型。
  2. 自动返回类型推导:允许使用auto关键字自动推导函数返回值类型。
  3. 初始化列表的泛型支持:可以使用auto关键字在初始化列表中推导元素类型。
  4. 带有二进制分隔符的整数字面量:可以在整数常量中使用单撇号作为分隔符,提高可读性。
  5. constexpr函数的扩展:constexpr函数可以包含更多操作,例如循环和条件判断。
  6. 变长参数模板(Variadic Templates)的改进:支持递归处理变长参数模板的展开。
  7. 返回void类型的lambda表达式:允许定义返回void类型的lambda函数。

9.2 C++14 中 auto 关键字的用法和限制是什么?

在 C++14 中,auto 关键字用于自动类型推导,可以根据初始化表达式的类型来确定变量的类型。它的使用和限制如下:

  1. 自动类型推导:使用 auto 关键字声明变量时,编译器会根据初始化表达式的类型自动推导出变量的类型。
    auto x = 42; // 推导为int型 auto name = “John”; // 推导为const char*型
  2. 声明时必须初始化:使用auto声明变量时,必须进行初始化。因为编译器需要根据初始化表达式来推导出变量的类型。
    auto y; // 错误,未初始化
  3. 可与引用结合使用:auto关键字可以与引用一起使用,从而推导出引用的类型。
    int a = 10; auto& ref = a; // 推导为int&型,ref是a的引用
  4. 不支持数组或函数指针: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引入了一些新的标准库组件和功能,以下是其中的一些主要特性:

  1. std::make_unique:提供了在堆上创建 unique_ptr 对象的便捷方式。
  2. std::integer_sequence:支持编译时整数序列的操作,用于元编程。
  3. std::user_defined_literals:允许用户定义自己的字面量后缀,扩展了语言的表达能力。
  4. 通用 lambda 表达式:允许使用 auto 参数声明参数类型,使得 lambda 表达式更加灵活。
  5. 变长模板参数折叠(Variadic template parameter packs expansion):可以将多个参数打包传递给模板函数或类,并且可以对它们进行展开操作。
  6. 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_uniquestd::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),它允许使用一种更统一和一致的方式进行初始化。具体的变化包括以下几个方面:

  1. 初始化列表(initializer 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{}; // 初始化为空字符串

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{}; // 初始化为空字符串

这些变化使得初始化更加灵活和一致,并且提供了更强大的类型推导能力。注意,在使用统一初始化语法时,要注意类型的精确匹配和可能的隐式转换。

  • 27
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

霜晨月c

谢谢老板地打赏~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值