C++14 Standards

C++14 Standards

1. C++14 的介绍

在这里我们可以发现 c++14 是·c++11的这要版本之后的一个次要版本,主要包括了改进和缺陷修复,更新的语法和并发库不是很多。他是在2014年8月18日获得批准,2015年12月15日首发。

那么就让我们开始学习 c++14 标准的新特性和语法吧。 🌈 🌈

2. 变量模版

c++14 允许使用变量模版,还可以适用于变量。

一个简单的例子:

#include <iostream>
template<class T>
constexpr T pi = T(3.1415926535897932385L); // 变量模板 
template<class T>
T circular_area(T r) // 函数模板 
{
 return pi<T> *r * r; // pi<T> 是变量模板实例化 
}
template<std::size_t N>
constexpr std::size_t factorial = N * factorial<N - 1>;
// 特化 
template<>
constexpr std::size_t factorial<0> = 1;
template< class T >
constexpr bool is_const_v = is_const<T>::value;
int main() {
	 // 使⽤不同精度的π
	 std::cout.precision(6);
	 std::cout << "float π: " << pi<float> << std::endl; 
	 std::cout.precision(10);
	 std::cout << "double π: " << pi<double> << std::endl; 
	 std::cout.precision(6);
	 float radius1 = 2.5;
	 std::cout << "半径为 " << radius1 << " 的圆⾯积: " << circular_area(radius1) << std::endl;
	 std::cout.precision(10);
	 double radius2 = 2.5;
	 std::cout << "半径为 " << radius2 << " 的圆⾯积: " << circular_area(radius2) << std::endl;
	 std::cout << factorial<5> << std::endl;
	 std::cout << factorial<10> << std::endl;
	 return 0;
}

 

这样的操作使得一些特殊场景更加的简便! 😋

 

3. 泛型 lambda 表达式

  • C++14 中允许 lambda 表达式使用auto 作为参数类型,使其成为泛型和前面的语法模版高度相似, auto& 代表左值引用的形参,auto&& 代表万能引用的形参, auto&&... 代表可变参数的模板参数的万能引用.
// 另⼀个泛型Lambda⽰例 - 返回两个参数中较⼤的⼀个 
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
	auto getMax = [](const auto& a, const auto& b) {

		return a > b ? a : b;
		};
	std::cout << "最⼤整数: " << getMax(10, 20) << std::endl;
	std::cout << "最⼤字符串: " << getMax(std::string("apple"),
		std::string("banana")) << std::endl;
	// 这⾥参数写成auto&&时类似引⽤折叠部分讲的万能引⽤ 
	// 实参是左值就是左值引⽤,实参是右值就是右值引⽤ 
	auto func = [](auto&& x, auto& y) {
		x += 97;
		y += 97;
		};
	int i = 0, j = 1;
	func(10, i);
	func(j, i);
	std::string s1("hello worldxxxxxxxxxxxx");
	func(move(s1), i);
	func(s1, i);
	// 也可以写成这样带可变模板参数的写法 
	std::vector<std::string> v;
	auto f1 = [&v](auto&&... ts)
		{
			// 可变参数模版的折叠表达式,后面会学习
			v.emplace_back(std::forward<decltype(ts)>(ts)...);
		};
	f1(move(s1));
	f1("1111111");
	f1(10, 'y');
	for (auto& e : v)
	{
		std::cout << e << " ";
	}
	std::cout << std::endl;

	return 0;
}

 

  • C++14lambda 捕获红可以直接使用表达式初始化啦捕获变量,这个变量可以是当前域定义的,也可以是没有定义的.
#include <iostream>
#include <vector>
#include <algorithm>
#include <memory>
int main() {
	std::vector<int> numbers = { 1, 2, 3, 4, 5 };

	// 使⽤表达式初始化捕获的变量 
	auto p = std::make_unique<int>(10);
	auto lambda1 = [value = 5, ptr = std::move(p), &numbers]() {
		std::cout << "捕获的值: " << value << std::endl;
		std::cout << "捕获的智能指针值: " << *ptr << std::endl;
		std::cout << "捕获的vector大小: " << numbers.size() << std::endl;
		};

	lambda1();

	// 另⼀个例⼦ - 在捕获时计算值 
	int x = 10;
	auto lambda2 = [y = x * 2]() {
		std::cout << "y = x * 2 = " << y << std::endl;
		};
	lambda2();
	return 0;
}

 

4. 函数的返回类型推导

  • c++11 中普通的函数使用auto 做返回来类型推导时,一般要求配合尾置类型使用,星队来说比较有局限性.

#include <iostream>
using namespace std;
// C++11中auto不能直接做函数返回类型,⼀般是跟尾置返回类型配合使⽤的 
int x = 1;
auto f1() -> int { return x; } // 返回类型是 int 
auto f2() -> int& { return x; } // 返回类型是 int& 
auto f3(int x) -> decltype(x * 1.5) { return x * 1.5; } // 必须显式推导 
int main()
{
	cout << f1() << endl;
	int& ret = f2();
	cout << f3(3) << endl;
	ret++;
	cout << x << endl;
	cout << ret << endl;
	return 0;
}

 

  • C++14 直接使用auto 进行推导,如果函数声明的声明说明符序列包含auto ,那么尾随返回类型可以省略,且编译器讲舍弃的返回一句中所使用的操作数的类型推到他,如果有多条返回语句,那么就必须推导出相同的类型!

#include <iostream>
using namespace std;
// C++14 普通函数可以直接⽤auto做返回类型,⾃动推导返回类型 
int x = 1;
auto f1() { return x; } // 返回类型是 int 
auto& f2() { return x; } // 返回类型是 int& 
auto f3(int x) { return x * 1.5; } // 返回类型是double 
// 报错,多返回语句需类型⼀致 
auto f4(int x) {
	if (x > 0)
		return 1.0; // double
	else
		return 2; // int → 错误:类型不⼀致 
}
int main()
{
	cout << f1() << endl;
	int& ret = f2();
	cout << f3(3) << endl;
	ret++;
	cout << x << endl;
	cout << ret << endl;
	return 0;
}

 

  • 如果返回的类型没有使用decltype(auto) ,那么推导遵循模版实参推导的规则进行. 如果返回的类型是decltype(),那么返回类型是将返回语句中的所有的操作数包裹在decltype 中时所具有的类型.

#include <iostream>
using namespace std;

int x = 1;
decltype(auto) f1() { return x; } // 返回类型是 int,同 decltype(x) 
decltype(auto) f2() { return (x); } // 返回类型是 int&,同 decltype((x)) 
// C++14引⼊的decltype(auto)常⽤于完美转发返回类型: 
// 需要精确保持返回值的值类别(左值/右值) 
template < typename F, typename... Args>
decltype(auto) call(F&& f, Args&&... args)
{
	return std::forward<F>(f)(std::forward<Args>(args)...);
}
int main()
{
	cout << f1() << endl;
	int& ret = f2();
	ret++;
	cout << x << endl;
	cout << ret << endl;

	cout << call([](int a, int b) { return a + b; }, 1, 2) << endl;
	return 0;
}

 

 

5. 二进制自变量

  • 十进制字面量是一个非0十进制数字(1,2,3,4,5,6,7,8,9)后面随0或者是多十进制数字
  • 八进制字面量是数字0后面随零或者多个八进制数字(0, 1, 2, 3, 4, 5, 6, 7).
  • 十六进制数字就是在字符序列0x或字符序列0x后随一或是多个十六进制数字(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, a, A, b, B, c, C, d, D, e, E, f, F)
  • 二进制字面量就是字符序列0b或字符序列oB后随一个或者是多个二进制数字(0, 1),二进制字面量是C++14才开始支持的.

下面是代码的示例:

#include<iostream>
#include<bitset>
using namespace std;
int main()
{
	int d = 42;
	int o = 052;
	int x = 0x2a;
	int X = 0X2A;
	int b = 0b101010; // C++				
	cout << d << endl;
	cout << o << endl;
	cout << x << endl;
	cout << X << endl;
	cout << b << endl;
	// 实际应⽤⽰例 - 位标志 
	const int FLAG_A = 0b0001; // 		1	
	const int FLAG_B = 0b0010; // 		2	
	const int FLAG_C = 0b0100; // 		4	
	const int FLAG_D = 0b1000; // 		8			
	int flags = FLAG_A | FLAG_C; // 0b0101					
	std::cout << "标志位: " << std::bitset<4>(flags) << std::endl;
	if (flags & FLAG_A) {
		std::cout << "FLAG_A 被设置" << std::endl;
	}
	if (flags & FLAG_B) {
		std::cout << "FLAG_B 被设置" << std::endl;
	}
	else {
		std::cout << "FLAG_B 未被设置" << std::endl;
	}
	return 0;
}

 

6. 数字分隔符

  • c++14 允许在数字字面量中使用单引号作为分隔符, 提高可读性.
#include <iostream>
#include <bitset>
int main() {
	// 使⽤数字分隔符提⾼⼤数字的可读性 
	int million = 100'0000;
	long hexValue = 0xDEAD'BEEF;
	double pi = 3.141'592'653'59;
	unsigned long long bigBinary = 0b1010'1010'1010'1010;

	std::cout << "一百万: " << million << std::endl;
	std::cout << "十六进制值: 0x" << std::hex << hexValue << std::dec << std::endl;
	std::cout << "π: " << pi << std::endl;
	std::cout << "二进制值: " << std::bitset<16>(bigBinary) << std::endl;

	// 实际应⽤⽰例 - 定义常量 
	const int MAX_USERS = 1'000'000;
	const double EARTH_RADIUS_KM = 6'371.0;
	const long long BIG_NUMBER = 123'456'789'123;

	std::cout << "最大用户数: " << MAX_USERS << std::endl;
	std::cout << "地球半径(km): " << EARTH_RADIUS_KM << std::endl;
	std::cout << "大数字: " << BIG_NUMBER << std::endl;
	return 0;
}

 

7. 使用默认非静态成员初始化器的聚合类

聚合类的定义

聚合类是指满足以下的条件的类(包括结果体):

  • 没有用户提供的构造函数
  • 没有时候或者是受到保护的非静态数据成员
  • 没有基类
  • 没有虚函数

关键点

  1. 默认成员初始化器: 档使用默认构造或者是值初始化时,成员会使用这些默认值
  2. 聚合初始化: 任然使用花括号初始化列表,未指定的成员使用默认值.
  3. 初始化顺序: 初始化列表中的值按成员进行声明顺序应用于成员

聚合类的定义和初始化方式的变化

  • c++11之前,聚合类不能有任何的初始化器,但在c++14的时候这个限制被放宽了.
  • C++14允许聚合类宝娟默认的非晶态成员初始化器(default member initializers), 这使得聚合类的使用更加的灵活.
  • 在后续的 C++17/C++20对嵌套类定义在不断的放宽,并且初始化的方式也再进一步优化,具体的文档在下面,感兴趣的uu可以深入学习了解一下.
#include <iostream>
#include <string>

struct Employee {
		std::string name = "Unknown";
		int id = -1;
		double salary = 0.0;
		void print() {
				std::cout << "Name: " << name << ", ID: " << id << ", Salary: " << salary << std::endl;
		}
};
int main() {
		// C++98
		Employee e1 = { "xxx", 1, 1.1 };
		e1.print();
		// C++ 14
		Employee e2{ "John" }; // name="John", id=-1, salary=0.0
		Employee e3{ "Alice", 123 }; // name="Alice", id=123, salary=0.0
		Employee e4{}; // 值初始化,等同于e1 

		// C++20中初始化聚合类的⽅式,解决C++14必须按顺序给值初始化的问题 
		Employee e5{ .id = {1}, .salary{1.1} }; // name="Alice", id= 1, salary=1.1
		// C++20中还⽀持嵌套类的初始化 
		struct Inner {
			int a;
			int b;
	};
		struct Outer {
			Inner i;
			int c;
	};
		Outer o1{ .i{1,2}, .c = 3 };
		// 或 
		Outer o2{ .i = {.a = 1, .b = 2}, .c = 3 };
		return 0;
}

 

8. 标准库的新增功能std::exchange

  • std::exchangeC++14 标准库在<utility> 头文件中引入的一个实用的模板,特提供了一种简洁高效的方式来替换一个对象的值并返回其旧值.
  • 相比与我们自己实现的替换而言 std::exchange在使用上个国家的简介,并且在c++20之后库里面将这个函数实现了 constexpr,效率更高!

这个更能非常的实用,下面展示一下他的具体使用代码示例:

#include <iostream>
#include <iterator>
#include <utility>
#include <vector>
class stream
{
public:
	using flags_type = int;
public:
	flags_type flags() const { return flags_; }
	/// 以 newf 替换 flags_ 并返回旧值。 
	flags_type flags(flags_type newf) { return std::exchange(flags_, newf); }
private:
	flags_type flags_ = 0;
};
void f() { std::cout << "f()"; }
int main()
{
	stream s;
	std::cout << s.flags() << 'n';
	std::cout << s.flags(12) << 'n';
	std::cout << s.flags() << "nn";
	std::vector<int> v = { 10,20,30 };
	// 因为第⼆模板形参有默认值,故能以花括号初始化式列表为第⼆实参。 
	// 下⽅表达式等价于 std::exchange(v, std::vector<int>{1, 2, 3, 4}); 
	std::vector<int> ret = std::exchange(v, { 1, 2, 3, 4 });
	for (auto e : v)
		std::cout << e << " ";
	std::cout << "n";
	for (auto e : ret)
		std::cout << e << " ";
	std::cout << "n";
	std::cout << "nn斐波那契数列: ";
	for (int a{ 0 }, b{ 1 }; a < 100; a = std::exchange(b, a + b))
		std::cout << a << ", ";
	std::cout << "...n";
	return 0;
}

 

注意使用 std::exchange 的对象一定是支持移动构造的才可以!

9. 标准库新增功能std::make_unique

  • std::make_uniquec++14的一个只智能指针的创建工具函数,用于安全的创建和管理std::unique_ptr对象, 他是现代c++,推荐的对象创建的方式之一,类似于c++11std::make_shared
  • std::make_unique相比于直接构造std::unique_ptr对象而言更加的安全,当构造函数或者是初始化过程中抛出了异常时, std::make_unique确保已经分配的资源可以正确的释放. 相比于直接使用new可能导致智能指接管前发生异常,造成内存泄漏.

所以在现代c++中更推荐使用std::make_sharedstd::make_unique这类的工具常见智能指针

#include <iostream>
// 必须包含智能指针头文件
#include <memory>

// 定义一个简单的测试类(用于演示对象创建/销毁)
class TestClass {
public:
    // 无参构造函数
    TestClass() {
        std::cout << "TestClass 无参构造函数调用n";
    }

    // 带参构造函数
    TestClass(int num, const std::string& str) : num_(num), str_(str) {
        std::cout << "TestClass 带参构造函数调用:num=" << num_ << ", str=" << str_ << "n";
    }

    // 析构函数(验证智能指针自动释放)
    ~TestClass() {
        std::cout << "TestClass 析构函数调用(对象被自动释放)n";
    }

    // 成员方法(演示智能指针访问对象)
    void showInfo() const {
        std::cout << "当前对象信息:num=" << num_ << ", str=" << str_ << "n";
    }

private:
    int num_ = 0;
    std::string str_ = "";
};

int main() {
    // ====================== 1. std::make_unique 示例(独占智能指针) ======================
    std::cout << "----- 创建独占智能指针(make_unique) -----n";
    
    // 创建无参对象的 unique_ptr
    auto uPtr1 = std::make_unique<TestClass>();
    
    // 创建带参对象的 unique_ptr
    auto uPtr2 = std::make_unique<TestClass>(100, "hello unique_ptr");
    uPtr2->showInfo(); // 访问对象成员方法

    // unique_ptr 独占所有权,无法拷贝(如下行注释打开会编译报错)
    // auto uPtr3 = uPtr2; 

    // ====================== 2. std::make_shared 示例(共享智能指针) ======================
    std::cout << "n----- 创建共享智能指针(make_shared) -----n";
    
    // 创建无参对象的 shared_ptr
    auto sPtr1 = std::make_shared<TestClass>();
    
    // 创建带参对象的 shared_ptr
    auto sPtr2 = std::make_shared<TestClass>(200, "hello shared_ptr");
    sPtr2->showInfo(); // 访问对象成员方法

    // shared_ptr 支持共享所有权(拷贝后引用计数+1)
    auto sPtr3 = sPtr2; 
    std::cout << "sPtr2 引用计数:" << sPtr2.use_count() << "n"; // 输出 2
    std::cout << "sPtr3 引用计数:" << sPtr3.use_count() << "n"; // 输出 2

    // ====================== 3. 智能指针自动释放(无需手动 delete) ======================
    std::cout << "n----- 函数结束,智能指针自动释放对象 -----n";
    return 0;
}

 

10. 标准库新增功能 std::integer_sequence

  • std::integer_sequence:
所属标准头文件作用
C++14#include<utility>编译期生成固定的整数序列,解决模板参数包无法直接遍历 / 展开的问题,是模版元编程编译时计算的一个得力工具

这个涉及到模版元编程,到了后面会更加详细的展开讲解.

// T 是整数类型(如 int, size_t 等) 
// Ints... 是⼀个⾮类型模板参数包,表⽰实际的整数序列 
template < class T, T... Ints >
class integer_sequence;
#include <cstddef>
#include <iostream>
#include <tuple>
#include <utility>
template < typename T, T... ints>
void print_sequence(int id, std::integer_sequence<T, ints...> int_seq)
{
	std::cout << id << " 大小为 " << int_seq.size() << " 的序列: ";
	((std::cout << ints << ' '), ...);
	std::cout << 'n';
}
int main()
{
	print_sequence(1, std::integer_sequence<unsigned, 9, 2, 5, 1, 9, 1, 6>{});
	print_sequence(2, std::make_integer_sequence<int, 12>{});
	print_sequence(3, std::make_index_sequence<10>{});
	print_sequence(4, std::index_sequence_for<std::ios, float, signed>{});
	return 0;
}

 

大家简单看一下这个即可, 在实际的开发应用中使用相对较少一些.

11. 标准库新增功能std::quoted

所属标准头文件用处
C++14#include<iomanip>一个IO操作器用于简化引号字符串的输入输出操作
  • 输出时: 自动为字符串添加引号;
  • 输出时:自动去除字符串周围的引号
  • 处理转义字符: 自动处理引号内的转义序列

基本的用法示例

#include <iostream>
#include <sstream>
#include <iomanip>
#include <string>
int main() {
	// 输出带引号的字符串 
	std::string text = "Hello, World!";
	std::cout << "Without quoted: " << text << std::endl;
	std::cout << "With quoted: " << std::quoted(text) << std::endl;
	// 输出: 
	// Without quoted: Hello, World!
	// With quoted: "Hello, World!"
	// 输⼊带引号的字符串 
	std::istringstream input(""Hello, World!"");
	input >> std::quoted(text);
	//input >> text; // 普通提取再遇到空格的时候直接截断,且会带有引号
	std::cout << "Extracted: " << text << std::endl;
	// 输出: Extracted: Hello, World! 
}

 

 

#include <iostream>
#include <iomanip>
#include <string>
#include <sstream>
int main() {
	// 使⽤单引号作为分隔符 
	std::string text = "It's a test";
	std::cout << "Default: " << std::quoted(text) << std::endl;
	std::cout << "Single quotes: " << std::quoted(text, ''') << std::endl;
	// 处理包含分隔符的字符串 
	std::string complex = R"(He said "Hello" and left)";
	std::cout << "Complex string: " << std::quoted(complex) << std::endl;
	// 使⽤字符串流测试输⼊输出 
	std::stringstream ss1;
	ss1 << std::quoted(complex);
	std::string extracted;
	ss1 >> std::quoted(extracted);
	std::cout << "Extracted: " << extracted << std::endl;
	// 包含转义字符的字符串 
	std::stringstream ss2;
	text = "Line1nLine2tTabbed";
	ss2 << std::quoted(text);
	std::cout << "Original: " << text << std::endl;
	std::cout << "Quoted: " << ss2.str() << std::endl;
	ss2 >> std::quoted(extracted);
	std::cout << "Extracted: " << extracted << std::endl;
}

 

12. 字面量后缀

字面量后缀是附加在字面量后面的标识符,用于明确指出该字面量的具体类型.这在避免歧义、确保精度、控制转换和提升代码可读性等方面有很大的作用。

  • C++98的时候就有字面量后缀
// C++98就有的字⾯量后缀
// 整形和浮点数的字⾯量后缀 
auto a = 10; // int
auto b = 10u; // unsigned int
auto c = 10l; // long
auto d = 10ul; // unsigned long
auto e = 10ll; // long long
auto f = 10ull; // unsigned long long
auto g = 3.14; // double
auto h = 3.14f; // float
auto i = 3.14l; // long double

 

  • C++11开始,允许浮点数、证书、字符串和字符串字面量通过定义用户定义后缀生成用户定义类型的对象。 这是通过operator""实现的。它允许程序员为字面量(数字、字符、字符串)定义自己的后缀,从而使这些“裸的”字面量转换为具有特定类型的和语义的对象。 在c++14/17/20里面定义了一些实用的时间、字符串字面量后缀。
//ReturnType operator "" _YourSuffix(Parameters);
// ReturnType:你希望转换后的⽬标类型。 
// _YourSuffix:关键:你⾃定义的后缀名。必须以下划线 _ 开头。不以 _ 开头的(如 s, h, i)保留给标准库使⽤。
// Parameters:参数类型取决于你处理的是哪种字⾯量(整型、浮点、字符、字符串或原始形式)。 
#include<string>
#include<string_view>
std::string operator "" _s(const char* str, size_t len) {
	return std::string(str, len);
}
std::string_view operator "" _sv(const char* str, size_t len) {
	return std::string_view(str, len);
}
float operator ""_e(const char* str)
{
	return std::stof(std::string(str));
}
constexpr long double operator "" _km(unsigned long long int x) {
	return x * 1000.0; // 将公⾥转换为⽶ 
}
constexpr long double operator "" _pi(long double x) {
	return x * 3.14159265358979323846L;
}
int main()
{
	auto s1 = "hello"_s;
	auto s2 = "Hello World"_s;
	auto sv1 = "hello"_sv;
	auto distance = 5_km; // 相当于 auto distance = 5000.0L; 
	auto angle = 2.0_pi; // 相当于 auto angle = 6.28318530717958647692L; 
	auto x = 12.3_e;
}

 

  • C++14起,标准库就提供了很多开箱即用的字面量,他们定义在内联命名空间之中 std::literals中。
后缀例子转换后的类型头文件
s"hello"sstd::string<string>
sv"hello"svstd::string_view<string_view>
h24hstd::chrono::hours<chrono>
min30minstd::chrono::minutes<chrono>
s10sstd::chrono::seconds<chrono>
ms100msstd::chrono::milliseconds<chrono>
us100usstd::chrono::microseconds<chrono>
ns100nsstd::chrono::nanoseconds<chrono>
i2.0 + 3.0istd::complex<double><complex>
ifiil1.0ifstd::complex<float><complex>
#include <iostream>
#include <string>
#include <chrono>
#include <thread>
// 使⽤标准的字⾯量 
using namespace std::literals;
int main() {
	// 字符串字⾯量 
	auto str = "Hello"s; // 类型是 std::string,不是 const char* 
	std::cout << str.size() << std::endl;
	std::this_thread::sleep_for(std::chrono::milliseconds(500));
	// 时间字⾯量 
	std::this_thread::sleep_for(500ms);
	return 0;
}