现代C++:C++17中的新语言特性

现代C++:C++17中的新语言特性

一.新语言特性

1.1结构化绑定

https://cppreference.cn/w/cpp/language/structured_binding

结构化绑定的使用很简单,比如下面这个例子:

cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;

int main()
{
	//绑定数组
	int arr[3] = { 1,2,3 };
	auto [a, b, c] = arr;
	//解包tuple与pair
	tuple<int, double, string> t(10, 10.0, "1111");
	auto& [d, e, f] = t;
	pair<int, double> p{ 1,1.0 };
	auto& [g, h] = p;
	return 0;
}

首先对于a,b,c...h这些变量,他们的类型推导遵循模板函数的推导规则,如果想要推出的变量能够带上引用属性,可以带上&,&&标识。

关于能够绑定的目标对象,简单来说有三类:

  1. 数组(包括原始数组和 std::array)
cpp 复制代码
int arr[3] = {1,2,3};
auto [x,y,z] = arr;   // x,y,z 分别为 1,2,3
  1. 所有非静态成员均为 public 的类或结构体(无基类,且所有成员定义在同一个类中)
cpp 复制代码
struct Point { int x, y; };
Point p{1,2};
auto [a,b] = p;       // a=1, b=2
  1. 提供了 std::tuple_sizestd::get 的"tuple‑like"类型(如 std::pair、std::tuple、std::complex 等)
cpp 复制代码
std::pair<int,double> pr{42, 3.14};
auto [i,d] = pr;      // i=42, d=3.14

其次还需要注意的是:

  1. 结构化绑定必须参数个数与绑定目标中的元素个数一致,否则会报错:
cpp 复制代码
	int arr[3] = { 1,2,3 };
	auto [a, b] = arr;//错误
  1. 结构化绑定的变量生命周期与绑定的对象相同:
  2. 结构化绑定的变量是表达式中对应元素的副本或引用。
  3. 结构化绑定不能用于类的私有成员,除非在类的成员函数内部。
  4. 结构化绑定不支持嵌套绑定。

1.2inline变量

https://cppreference.cn/w/cpp/language/inline

像我们之前在定义一个多个文件中都需要使用的变量时,我们可以通过const,staticextern来定义,这样不会爆链接错误,但是前两者有一个单定义(ODR)的问题,具体解释如下:

.h 定义全局变量 ,多个 .cpp 包含就会被定义多次,会报链接错误conststatic 全局变量默认有内部链接(Internal Linkage) ,不会报链接错误,但是意味着每个包含 .h.cpp 文件(翻译单元 )都会自己创建一个独立的 bufferSizecacheSize 变量。虽然这些变量名字和值都一样,但它们的地址是不同的 。这违背了"全局唯一常量"的初衷。

我们来看下面一个例子:

cpp 复制代码
//Cache.h
#pragma once
#include <iostream>
#include <vector>

//以下两个变量在其他源文件使用时会产生所谓'副本'
const int num_1 = 1000;
static int num_2 = 2000;
//虽然不会有上述问题,但是num_3在那里定义的还需要我们自己找
extern int num_3;

void test_print();
//Cache.cpp
#include "Cache.h"
int num_3 = 3000;
void test_print()
{
	std::cout << "Cache::" << std::endl;
	std::cout << &num_1 << std::endl;
	std::cout << &num_2 << std::endl;
	std::cout << &num_3 << std::endl;
}
//test.cpp
#include "Cache.h"

int main()
{
	std::cout << "test::" << std::endl;
	std::cout << &num_1 << std::endl;
	std::cout << &num_2 << std::endl;
	std::cout << &num_3 << std::endl;
	test_print();
	return 0;
}

运行结果如下:

cpp 复制代码
test::
00007FF6ACB8BC44
00007FF6ACB8E00C
00007FF6ACB8E000
Cache::
00007FF6ACB8BBB0
00007FF6ACB8E004
00007FF6ACB8E000

extern虽然可以解决这个问题,但是很难受用起来,不过C++17中引入了inline变量很好的解决了这个问题,比如我们将上面的num_1与num_2均前面加上一个inline标识看看():

cpp 复制代码
test::
00007FF6F6F6BBB4
00007FF6F6F6E00C
00007FF6F6F6E000
Cache::
00007FF6F6F6BBB4
00007FF6F6F6E004
00007FF6F6F6E000

解释以下为什么inline static仍然产生了多个副本:

  1. static 在命名空间(全局)作用域中表示内部链接
    在全局/命名空间作用域中,static 修饰的变量具有内部链接(internal linkage)。这意味着 每个包含该定义的翻译单元(.cpp 文件)都会拥有自己独立的一份副本,彼此互不干扰。
  2. inline 变量主要影响外部链接和 ODR
    C++17 允许 inline 变量在多个翻译单元中定义(通常放在头文件里),并保证所有引用都指向同一个实体(类似函数内联)。
    但是,static 已经强制将链接属性改为内部,此时 inline 的"多单元统一实体"作用被覆盖(或无效化)。编译器要么忽略 inline,要么认为它无意义。

inline能够达到这种效果的原理如下:

  • inline 关键字用于变量时,其主要含义是:允许(甚至要求)在多个翻译单元 中定义相同的变量,并且保证这些定义指向的是同一个实体同一个地址)。
  • 链接器会从多个定义中挑选一个,并丢弃其他的,从而确保整个程序中只有一个该变量的实例。这完美地契合了在头文件中定义变量的需求。

那么此时除了上面单定义的问题可以解决,也方便了我们初始化类内静态成员变量:

cpp 复制代码
class MyClass
{
public:
	//TODO:
private:
	//不用像之前那样类外初始化了
	inline static std::string str = "114514";
};

1.3if/switch初始化器

https://cppreference.cn/w/cpp/language/if

https://cppreference.cn/w/cpp/language/switch

你像我们之前用for循环的时候,最开始都可以使用一个初始化条件去帮助我们完成后续的循环,在C++17之后,if/switch也支持在开始加上一个初始化语句协助我们进行条件判断:

cpp 复制代码
int checkvalue(int value)
{
	return value % 2;
}

int main()
{
	std::set<std::string> s{"114514","0000","12345"};
	if (auto it = s.find("114514"); it != s.end())
	{
		std::cout << "找到目标值" << std::endl;
	}
	else {
		std::cout << "未找到目标值" << std::endl;
	}

	switch (auto status = checkvalue(2);status) {
		case 1:
			break;
		case 0:
			break;
		default:
			std::cout << "无效值" << std::endl;
	}
	return 0;
}

1.4强制拷贝省略

我们先来看下面这样一段代码:

cpp 复制代码
struct Noisy
{
	Noisy() { std::cout << "constructed at " << this << '\n'; }
	Noisy(const Noisy&) { std::cout << "copy-constructed\n"; }
	Noisy(Noisy&&) { std::cout << "move-constructed\n"; }
	~Noisy() { std::cout << "destructed at " << this << '\n'; }
};

Noisy f()
{
	Noisy v = Noisy(); 
	return v; 
}

int main()
{
	Noisy v = f();
	return 0;
}

如果是正常的逻辑没有任何优化的情况,应该是输出如下结果:

cpp 复制代码
constructed at 0x7ffe7284a887 //临时对象Noisy构造
move-constructed//临时对象Noisy移动拷贝给f()::v
destructed at 0x7ffe7284a887//临时对象Noisy析构
move-constructed//f()::v资源被移动拷贝给返回的临时Noisy对象
destructed at 0x7ffe7284a886//f()::v析构
move-constructed//返回的临时Noisy对象资源被移动拷贝给main()::v
destructed at 0x7ffe7284a8b7//返回的临时Noisy对象析构
destructed at 0x7ffe7284a8b6//main()::v析构

但是大多数编译器在c++17之前,对这种情况都进行了优化,无论是gcc还是msvc都是仅进行一次构造:

cpp 复制代码
constructed at 00000075566FF6E4
destructed at 00000075566FF6E4

这种优化到C++17之后得到了正式规范,就是上面的代码即使在gcc中加上-fno-elide-constructors选项后也简化了许多(当然还是没有gcc与msvc那样极端),结果如下:

cpp 复制代码
constructed at 0x7ffee1cd9967//临时对象Nosiy构造,但是直接构造给了f()::v
move-constructed//f()::v直接移动赋值给main()::v
destructed at 0x7ffee1cd9967
destructed at 0x7ffee1cd9997

具体的省略规则如下,具体分为匿名对象和具名对象的省略:

匿名对象省略(强制语法规定)

当用纯右值(prvalue,例如 T()、T(expr)、return T() 等)直接初始化一个同类型的对象时,不再产生临时对象,而是直接将构造体放在目标对象的内存中

注意此时即便拷贝/移动构造函数有副作用(如打印信息),也不会被调用。

就像上面最后一段的结果,Noisy v = Noisy(); 这里就发生了NRVO。

这里再补充一个例子,我们看下面这个例子:

cpp 复制代码
struct NonMoveable {
    NonMoveable() = default;
    NonMoveable(const NonMoveable&) = delete; // 禁止拷贝
    NonMoveable(NonMoveable&&) = delete;      // 禁止移动
};

NonMoveable make() {
    return NonMoveable(); // 在 C++14 中编译错误!
    // 因为语言逻辑上要求调用已删除的移动构造函数
}

int main() {
    NonMoveable nm = make(); // 同样错误
}

因为语法上规定了可以不去掉移动或拷贝构造,所以C++17是可以直接跑通这段代码的,但是C++14之前,虽然编译器的优化可以不走任何拷贝,但是因为语法上不允许,所以会报错。

具名对象省略(并无语法规定)

对于 return 语句中返回一个具名的局部变量 (例如 return v;),C++17 允许 编译器执行命名返回值优化(NRVO) ,但并没有强制要求

  • 如果编译器进行优化,则不会调用拷贝/移动构造(相当于将局部变量直接构造到调用方接收的内存中)。
  • 如果不优化(如使用 -fno-elide-constructors),则会调用移动构造 (C++11 起),因为 return v; 会将 v 视为右值。

注意 :在 C++17 之前,这只是可选的优化;在 C++17 中,仍然只是一种优化(不是语言强制要求)。

也就是说,我们拿匿名对象省略最后的例子改下:

cpp 复制代码
struct NonMoveable {
    NonMoveable() = default;
    NonMoveable(const NonMoveable&) = delete; // 禁止拷贝
    NonMoveable(NonMoveable&&) = delete;      // 禁止移动
};

NonMoveable make() {
    NonMoveable v = NonMoveable();
    return v;
}

int main() {
    NonMoveable nm = make();
}

这里v是一个具名对象,虽然编译器优化会不去调用任何拷贝构造,但是语法上并没有规定,所以语法上过不去导致编译报错。

具名对象不强制的原因,主要因为存在复杂情况(例如多个 return 路径返回不同的局部变量,或者局部变量是函数参数等),强制省略会对实现造成较大负担。标准委员会保留了优化自由度。

1.5if constexpr

https://cppreference.cn/w/cpp/language/if

之前我们学习模板元编程的时候其实就已经见过if constexpr了,它的作用就是在编译器对部分判断语句进行丢弃,最常见的使用场景就是针对不同的类型执行不同的操作,如下:

cpp 复制代码
#include <iostream>
using namespace std;

template <typename T>
void test_print(T t)
{
	if constexpr(is_integral_v<T>)
	{
		cout << t + 10 << endl;
	}
	else if constexpr(is_same_v<T, std::string>)
	{
		cout << t << "\t" << t.size() << endl;
	}
	else
	{
		cout << "other type" << endl;
	}
}

int main()
{
	test_print(10);
	test_print("hello");
	test_print(3.14);
	return 0;
}

因为之前其实已经详细的展示过其用法了,所以这里不再过多赘述。

1.6折叠表达式

https://cppreference.cn/w/cpp/language/fold

我们之前如果想要去解一个参数包的时候,一般都只能通过递归或者模板解包的方式对参数包进行提取,非常的麻烦,不过有了折叠表达式之后,这一操作变得简便许多,比如我想要打印一个tuple中的所有参数,就可以这么写了:

cpp 复制代码
#include <iostream>
using namespace std;

template<typename Tuple,size_t... Is>
void _print_tuple(Tuple& t, integer_sequence<size_t, Is...>)
{
    ((cout << get<Is>(t) << "\n"), ...);
}

template<typename... Args>
void print_tuple(tuple<Args...>& t)
{
    _print_tuple(t, index_sequence_for<Args...>{});
}

int main() {
    tuple<int, std::string, double> t(10, "xiu114514", 10.0);
    print_tuple(t);
    return 0;
}

(tips:折叠表达式必须使用括号将其括起来,否则视为语法错误)

需要注意的是,折叠表达式 仅支持与二元操作符 一同使用,且分为如下四种主要使用情况(以二元操作符 op 为例):

a. 一元右折叠 (pack op ...)

  • 展开形式:(pack1 op (pack2 op (pack3 op ... (packN-1 op packN))))
  • 计算顺序:从右向左

b. 一元左折叠 (... op pack)

  • 展开形式:((((pack1 op pack2) op pack3) op ...) op packN)
  • 计算顺序:从左向右

c. 带初始值的二元右折叠 (pack op ... op init)

  • 展开形式:(pack1 op (pack2 op (pack3 op ... (packN op init))))
  • 计算顺序:从右向左

d. 带初始值的二元左折叠 (init op ... op pack)

  • 展开形式:((((init op pack1) op pack2) op ...) op packN)
  • 计算顺序:从左向右

我们来以求差来看下前两种情况的使用场景:

cpp 复制代码
template<int... Ints>
void subtract_nums(integer_sequence<int, Ints...> sq)
{
    int left_subtract = (... - Ints);
    cout << "left_subtract:" << left_subtract << endl;
    int right_subtract = (Ints - ...);
    cout << "right_subtract:" << right_subtract << endl;

}

int main() {
    subtract_nums(make_integer_sequence<int,10>{});
    return 0;
}

结果如下:

cpp 复制代码
left_subtract:-45
right_subtract:-5

而二元折叠则是给这种情况加上了一个初始值,可以看下面这个例子:

cpp 复制代码
template<int... Ints>
void subtract_nums(integer_sequence<int, Ints...> sq)
{
    int left_subtract = (100 - ... - Ints);
    cout << "left_subtract:" << left_subtract << endl;
    int right_subtract = (Ints - ... - 100);
    cout << "right_subtract:" << right_subtract << endl;

}

int main() {
    subtract_nums(make_integer_sequence<int,10>{});
    return 0;
}
cpp 复制代码
left_subtract:55
right_subtract:95

关于逗号的特殊情况

这里再说关于逗号这个二元运算符的特殊情况,直接说不好解释,我们看下面这个例子:

cpp 复制代码
template<int... Ints>
void print_nums(integer_sequence<int, Ints...> sq)
{
    ((cout << Ints << " "), ...);
    (..., (cout << Ints << " "));
}

如果按照上面的逻辑,此处两个二元表达式展开应该分别如下(假设此时参数包中有三个元素):

cpp 复制代码
((cout << 1 << " "), ((cout << 2 << " "), (cout << 3 << " ")))

(((cout << 1 << " "), (cout << 2 << " ")), (cout << 3 << " "))

然后按照正常的逻辑走,前者因该是从右向左打印,而后者应该是从左向右打印。但实际上,因为逗号表达式运算的特殊性,二者打印的结果是相同的。

也就是说,无论是左折叠还是右折叠,关于逗号的折叠表达式都会严格遵循从左向右进行运算的规则

多提一嘴,无论是左折叠还是右折叠,关于逗号的折叠表达式都会严格遵循从左向右进行运算这个也是C++17标准中严格规定的。

空参数包的特殊情况

参数包为空时,折叠表达式的行为取决于操作符和是否有初始值:

对于一元折叠(没有初始值):

  • 如果对空包使用 &&,其值为 true
  • 如果对空包使用 ||,其值为 false
  • 如果对空包使用 ,,其值为 void()
  • 对其他所有操作符使用一元折叠会导致编译错误

对于二元折叠(有初始值):

  • 如果参数包为空,表达式的结果就是初始值 init

这使得带初始值的版本 更加安全通用

简单看几个例子:

cpp 复制代码
template<int... Ints>
void empty_nums(integer_sequence<int, Ints...> sq)
{
    cout << (Ints && ...) << endl;//1
    cout << (... || Ints) << endl;//0
    cout << is_void_v<decltype((Ints, ...))> << endl;//1
}

int main() {
    empty_nums(make_integer_sequence<int, 0>{});
    return 0;
}

其他的使用方式因为很有可能报错,所以我们一般使用折叠表达式的时候都会加上一个初始值,即常用的折叠表达式其实是二元折叠那两种情况。

1.7类模板参数自动推导(CTAD)

https://cppreference.cn/w/cpp/language/class_template_argument_deduction

C++17 引入了类模板参数自动推导(Class Template Argument Deduction, CTAD) 功能,它允许编译器根据构造函数的参数自动推导模板参数类型,从而简化类模板的实例化。

我们来看几个例子:

cpp 复制代码
// 示例1: 标准库容器的自动推导
void containerDeduction() {
    // C++17之前需要指定模板参数
    std::vector<int> v1 = { 1, 2, 3 };

    // C++17可以自动推导
    std::vector v2 = { 4, 5, 6 };                 // 推导为 vector<int>
    std::vector v3{ "hello", "world" };           // 推导为 vector<const char*>
    std::vector v4(10, 1);                        // 推导为 vector<int>
    std::vector v5(v1.begin(), v1.end());         // 推导为 vector<int>
    // std::vector v6;                            // 报错:无法推导

    std::cout << typeid(v2).name() << std::endl;
    std::cout << typeid(v3).name() << std::endl;
    std::cout << typeid(v4).name() << std::endl;
    std::cout << typeid(v5).name() << std::endl;

    // 类似地适用于其他容器
    std::array a = { 1, 2, 3 };                   // 推导为 array<int, 3>
    std::pair p(1, 2.0);                          // 推导为 pair<int, double>
    std::tuple t(1, 2.0, "three");                // 推导为 tuple<int, double, const char*>

    std::cout << "v2 size: " << v2.size() << std::endl;
    std::cout << "a size: " << a.size() << std::endl;
    std::cout << "p first: " << p.first << ", second: " << p.second << std::endl;
}

int main() {
    containerDeduction();
    return 0;
}
cpp 复制代码
class std::vector<int,class std::allocator<int> >
class std::vector<char const * __ptr64,class std::allocator<char const * __ptr64> >
class std::vector<int,class std::allocator<int> >
class std::vector<int,class std::allocator<int> >
v2 size: 3
a size: 3
p first: 1, second: 2

1.8使用auto声明的非类型模板参数

非类型模板参数 是一个具有特定类型的常量值。在 C++17 之前 ,非类型模板参数的类型必须被明确指定。C++17 允许我们使用 auto 关键字作为非类型模板参数的占位符,编译器会根据实例化时提供的实参自动推导出该参数的类型。

C++20 之前 ,非类型模板参数的类型被严格限制在:整型 / 枚举类型 / 指针类型 / 左值引用 / std::nullptr_tC++20 打破了这一限制,允许浮点数 ,允许你使用满足特定条件的类类型 (称为**"字面量类类型"**)的对象作为编译期常量,并传递给模板。字面量类类型 C++20 会具体讲解,简单来说,它必须是一个可以在编译期完全构造和析构的简单、透明的数据结构,如 std::array、简单的自定义结构体 struct Point { int x; int y; }; 等。

cpp 复制代码
#include <iostream>
#include <string>
#include <string_view>
#include <array>
#include <typeinfo>

template<auto Value>
void printValue() {
    // std::cout << "Value: " << Value << std::endl;
    std::cout << "Type: " << typeid(Value).name() << std::endl; // 打印类型名(编译器相关)
    std::cout << "---" << std::endl;
}

// 一个简单的字面量类类型
struct Point {
    int x;
    int y;
    // 编译器会为我们生成一个隐式的 constexpr 构造函数
    // 析构函数也是隐式 constexpr 的
};

template<auto... Values>
struct ValueList {
    ValueList() {
        // 逗号折叠表达式
        ((std::cout << Values << ' '), ...);
    }
}; // 一个编译期的值列表

const char arr[] = "hello";

int main() {
    printValue<42>();                    // auto 被推导为 int
    printValue<3.14>();                  // auto 被推导为 double(C++20 开始才支持)
    printValue<'A'>();                   // auto 被推导为 char
    printValue<true>();                  // auto 被推导为 bool
    printValue<nullptr>();               // auto 被推导为 nullptr_t
    // printValue<"hello">();            // 错误:字符串字面量不能作为非类型模板参数
    printValue<arr>();                   // auto 被推导为 const char*

    // C++20 开始支持字面量类类型作为非类型模板参数
    printValue<Point{ 10, 20 }>();

    constexpr std::array arr{ 1, 2, 3, 4, 5 };
    printValue<arr>();

    // 计算值列表中的值的类型
    ValueList<1, 2.2, 'a'> vl;
    // printValue<string("Hello")>();    // 报错
}

1.9嵌套命名空间定义

C++17 引入了简化的嵌套命名空间定义 语法,使得定义多层嵌套的命名空间更加简洁方便。这是对传统命名空间定义方式的一种语法糖 改进。

简单来看一个例子就能明白了:

cpp 复制代码
#include <iostream>

//传统写法
//namespace A {
//    namespace B {
//        namespace C {
//            void func() {
//                // 函数实现
//            }
//        }
//    }
//}

// C++17 简化嵌套命名空间定义
namespace A::B::C {
    void func() {
        std::cout << "Hello from A::B::C" << std::endl;
    }
    
    int value = 42;
}

int main() {
    A::B::C::func();           // 输出: Hello from A::B::C
    std::cout << A::B::C::value << std::endl;  // 输出: 42
    return 0;
}

1.10属性

https://cppreference.cn/w/cpp/language/attributes

属性这个东西其实在C++11开始就有了,不过到17这里才算是比较常用一些,所以我们放到这里再进行认识:

属性的基本语法如下:

cpp 复制代码
[[attribute]] // 单个属性
[[attribute1, attribute2]] // 多个属性
[[namespace::attribute]] // 带命名空间的属性

C++11属性

C++11 将属性语法标准化,引入了 [[ ]] 语法,但只提供了两个标准属性。C++14 / C++17 / C++20 中属性逐步丰富化。

  • [[noreturn]](C++11) :指示函数不会返回到它的调用点 ,主要用于优化和错误检测。通过明确告知编译器函数不会返回,编译器可以优化代码逻辑,例如跳过某些无效的后续代码执行等。标记为 noreturn 的函数返回时,编译器会提示警告。

  • [[carries_dependency]](C++11) :高级并发属性,它是为专家级并发优化提供的工具,但对于 99.9% 的开发者来说,使用 std::atomic 默认内存序或其他高级并发工具是更明智的选择。(这里主要和我们之前说六种内存重排序相关,想要了解这个属性的读者可以结合那里认识下,因为后面被废弃了预计在C++26,所以这里我们只是知道有这么个东西就行)

我们来看一个关于[[noreturn]]属性的例子:

cpp 复制代码
//像下面这个例子不可能走到return,可以声明`[[noreturn]]`
[[noreturn]] void exit_func()
{
    exit(0);
    //exit(0) 之后的代码是不可达代码,编译器通常会优化掉或在无优化时保留但永远不执行。
    std::cout << "代码执行ing..." << std::endl;
    return;
}

int main() {
    exit_func();
    return 0;
}

我们看下反汇编(vs2022下debug模式下反汇编依旧生成相应指令,但是release下不会生成相关指令,下面是release下的反汇编):

cpp 复制代码
//像下面这个例子不可能走到return,可以声明`[[noreturn]]`
[[noreturn]] void exit_func()
{
00007FF636701000  sub         rsp,28h  
    exit(0);
00007FF636701004  xor         ecx,ecx  
00007FF636701006  call        qword ptr [__imp_exit (07FF636702118h)]  
00007FF63670100C  int         3  
--- 无源文件 -----------------------------------------------------------------------
00007FF63670100D  int         3  
00007FF63670100E  int         3  
00007FF63670100F  int         3  
--- C:\code\Cplusplus__code\C++17\C++17\test.cpp -------------------------------
    //exit(0) 之后的代码是不可达代码,编译器通常会优化掉或在无优化时保留但永远不执行。
    std::cout << "代码执行ing..." << std::endl;
    return;
}

但是如果我们函数实际上会返回,但是我们声明了此属性会怎么样?

(在vs2022下的release模式会直接卡死,g++10.+版本则是直接爆段错误)。

所以关于属性,如果我们不是确定代码执行后就是预期结果,不要去使用,它并不会像我们之前写constexpr函数或变量那样自动退化保证代码正常运行。它属于一种未定义行为

C++14属性

C++14多了一个属性[[deprecated]],这个属性在生产环境中还是很好用的,它有两种使用方式:

cpp 复制代码
[[deprecated]]
[[deprecated("reason")]]

我们来看一个场景就明白了,比如我们现在有两个过期的接口,此接口已经更新到3.0版本了,我们想要别人使用的时候不要去使用先前的接口而是来用我最新版本的接口:

cpp 复制代码
[[deprecated]] void func_version1()
{
    return;
}

[[deprecated("此函数接口已经过时,请使用最新的func_version3")]] void func_version2()
{
    return;
}

void func_version3()
{
    return;
}

int main() {
    func_version1();
    func_version2();
    func_version3();
    return 0;
}

当我们调用过期接口时会直接报错:

cpp 复制代码
1>C:\code\Cplusplus__code\C++17\C++17\test.cpp(86,5): error C4996: 'func_version1': 被声明为已否决
1>C:\code\Cplusplus__code\C++17\C++17\test.cpp(87,5): error C4996: 'func_version2': 此函数接口已经过时,请使用最新的func_version3

加上自定义信息可以更明显的告诉使用者原因,不填则是默认消息。

当然deprecated也可以⽤于类型、变量、枚举等,大家可以自行下去了解下。

C++17属性

C++17多了下面三个属性,都还是很有用的属性:

  • [[fallthrough]](C++17) :用于在 switch 语句中显式地表明从一个 case 标签**"贯穿"到下一个 case 标签是有意为之**的,以避免编译器发出警告。

  • [[nodiscard]](C++17) :用于标记一个函数的返回值非常重要 ,调用者不应该忽略它。如果调用者没有使用返回值,编译器会发出警告。C++20 允许添加原因信息:[[nodiscard("xxx")]]

  • [[maybe_unused]](C++17):用于抑制编译器对未使用实体发出的"未使用变量/参数"警告。

我们一个一个来看下例子,首先第一个:

cpp 复制代码
int main() {
    int x = 10;
    switch (x) {
    case 10:
        cout << "贯穿执行" << endl;
        //不加此属性可能报警
        [[fallthrough]];
    default:
        cout << "贯穿执行到目标位置" << endl;
    }
    return 0;
}

第二个主要还是建议调用者最好是把返回值接收下,我们看下面这样一个例子:

cpp 复制代码
[[nodiscard]] int func1()
{
    return 1;
}
//虽然说是C++20才支持,但是vs2022下C++17版本即可使用
[[nodiscard("求你了接收返回值,不然似给你喵")]] int func2()
{
    return 1;
}

int main() {
    func1();
    func2();
    return 0;
}

//警告信息如下
1>C:\code\Cplusplus__code\C++17\C++17\test.cpp(81,10): warning C4834: 放弃具有 [[nodiscard]] 属性的函数的返回值
1>C:\code\Cplusplus__code\C++17\C++17\test.cpp(82,10): warning C4858: 正在放弃返回值: 求你了接收返回值,不然似给你喵

第三个属性平时经常能接触到,这里大家可以自己尝试下直接在对应变量前加此属性名称即可。

C++20属性

\[likely]与\[unlikely]

C++20主要更新了如下三个属性,我们先来介绍前两个,这两个属性它们可以单独使用,也可以一起使用,视情况而定:

  • [[likely]] :向编译器指示,它所在的代码路径(例如,if 语句的 true 分支,或 switch 语句的某个 case)在程序执行过程中非常可能被采用。
  • [[unlikely]] :向编译器指示,它所在的代码路径非常不可能被采用。

标准规定 :这些属性只是一个提示,编译器可以完全忽略它们。编译器可能会根据自己的分析结果覆盖你的提示。

比如如下这种情况:

cpp 复制代码
void func(int value)
{
    if (value > 0) [[likely]] {
        //此分支在绝大多数情况下都会执行
        std::cout << value * 2 << std::endl;
    }
    else [[unlikely]] {
        std::cout << "invalid value." << std::endl;
    }
}

int main() {
    func(10);
    return 0;
}

虽然编译器有可能会忽略你的建议,但是还是最好在你十分确定的情况下再去使用这两个属性。主要是和性能方面相关的原因,这里我们简单的说下:

当代计算机cpu在处理指令的时候,通常在未执行指令的时候先进行预判,以便提高指令处理的速度。

这里我们以流水线做食品罐头为例子,流程大致为:挑选食材-处理食材-加工食材。当第一环节的工人执行完毕工作后,当然不可能闲着,他会立马去挑选下一批食材,那么大致的流水线过程就是这样的:
#mermaid-svg-EFoepv4FbXNgDwe3{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-EFoepv4FbXNgDwe3 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-EFoepv4FbXNgDwe3 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-EFoepv4FbXNgDwe3 .error-icon{fill:#552222;}#mermaid-svg-EFoepv4FbXNgDwe3 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-EFoepv4FbXNgDwe3 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-EFoepv4FbXNgDwe3 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-EFoepv4FbXNgDwe3 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-EFoepv4FbXNgDwe3 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-EFoepv4FbXNgDwe3 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-EFoepv4FbXNgDwe3 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-EFoepv4FbXNgDwe3 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-EFoepv4FbXNgDwe3 .marker.cross{stroke:#333333;}#mermaid-svg-EFoepv4FbXNgDwe3 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-EFoepv4FbXNgDwe3 p{margin:0;}#mermaid-svg-EFoepv4FbXNgDwe3 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-EFoepv4FbXNgDwe3 .cluster-label text{fill:#333;}#mermaid-svg-EFoepv4FbXNgDwe3 .cluster-label span{color:#333;}#mermaid-svg-EFoepv4FbXNgDwe3 .cluster-label span p{background-color:transparent;}#mermaid-svg-EFoepv4FbXNgDwe3 .label text,#mermaid-svg-EFoepv4FbXNgDwe3 span{fill:#333;color:#333;}#mermaid-svg-EFoepv4FbXNgDwe3 .node rect,#mermaid-svg-EFoepv4FbXNgDwe3 .node circle,#mermaid-svg-EFoepv4FbXNgDwe3 .node ellipse,#mermaid-svg-EFoepv4FbXNgDwe3 .node polygon,#mermaid-svg-EFoepv4FbXNgDwe3 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-EFoepv4FbXNgDwe3 .rough-node .label text,#mermaid-svg-EFoepv4FbXNgDwe3 .node .label text,#mermaid-svg-EFoepv4FbXNgDwe3 .image-shape .label,#mermaid-svg-EFoepv4FbXNgDwe3 .icon-shape .label{text-anchor:middle;}#mermaid-svg-EFoepv4FbXNgDwe3 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-EFoepv4FbXNgDwe3 .rough-node .label,#mermaid-svg-EFoepv4FbXNgDwe3 .node .label,#mermaid-svg-EFoepv4FbXNgDwe3 .image-shape .label,#mermaid-svg-EFoepv4FbXNgDwe3 .icon-shape .label{text-align:center;}#mermaid-svg-EFoepv4FbXNgDwe3 .node.clickable{cursor:pointer;}#mermaid-svg-EFoepv4FbXNgDwe3 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-EFoepv4FbXNgDwe3 .arrowheadPath{fill:#333333;}#mermaid-svg-EFoepv4FbXNgDwe3 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-EFoepv4FbXNgDwe3 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-EFoepv4FbXNgDwe3 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-EFoepv4FbXNgDwe3 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-EFoepv4FbXNgDwe3 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-EFoepv4FbXNgDwe3 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-EFoepv4FbXNgDwe3 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-EFoepv4FbXNgDwe3 .cluster text{fill:#333;}#mermaid-svg-EFoepv4FbXNgDwe3 .cluster span{color:#333;}#mermaid-svg-EFoepv4FbXNgDwe3 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-EFoepv4FbXNgDwe3 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-EFoepv4FbXNgDwe3 rect.text{fill:none;stroke-width:0;}#mermaid-svg-EFoepv4FbXNgDwe3 .icon-shape,#mermaid-svg-EFoepv4FbXNgDwe3 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-EFoepv4FbXNgDwe3 .icon-shape p,#mermaid-svg-EFoepv4FbXNgDwe3 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-EFoepv4FbXNgDwe3 .icon-shape .label rect,#mermaid-svg-EFoepv4FbXNgDwe3 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-EFoepv4FbXNgDwe3 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-EFoepv4FbXNgDwe3 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-EFoepv4FbXNgDwe3 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 第三批食材
第二批食材
第一批食材
挑选食材
处理食材
加工食材
挑选食材
处理食材
加工食材
挑选食材
处理食材
加工食材

但是如果一个环节出了问题,那么整条线的执行都会出乱子造成生产效率损失。

当代 CPU 采用流水线执行指令,会预取后续指令(类似食品罐头流水线:挑选→处理→加工,工人不空闲)。
当遇到条件分支时,CPU 会进行分支预测,猜测接下来走哪条路。如果猜对了,流水线顺畅;如果猜错,整条流水线必须清空并重新加载正确指令,造成性能损失(几十个时钟周期)。

[[likely]] / [[unlikely]] 就是给编译器的提示,编译器会调整代码布局(比如把 likely 分支放在紧接条件语句的位置),帮助 CPU 更准确地进行静态分支预测。

但如果你在概率不确定或实际相反的情况下使用,反而可能增加预测错误,降低性能。

因此,不是 70%~80% 以上的确定情况,请不要轻易使用这两个属性。

\[no_unique_address]
  • [[no_unique_address]](C++20) 用于优化类的内存布局。它告诉编译器:被修饰的非静态空成员可能不需要在对象中拥有独立的地址空间。空成员就是无非静态成员变量的类对象。

  • 当前的问题 :在 C++ 中,任何对象(即使是空类 class Empty {}; 的对象)都必须拥有一个唯一的地址 。这意味着它的 sizeof 至少为 1(通常为 1),以确保在数组中,每个对象都能被区分开。当你有一个类(structclass)包含多个空类类型的成员时,每个空成员都会至少占用 1 字节,再加上内存对齐的填充字节,会导致大量内存浪费。

  • 空基类优化(EBO - Empty Base Optimization):C++ 标准允许编译器对空基类进行优化。如果一个类继承自空基类,编译器可以不为基类子对象分配任何空间,使其大小为零。

  • [[no_unique_address]] 是类似 EBO 的优化,它可以应用于非静态数据成员,提示编译器:如果该成员是空的,则不需要为其分配一个唯一的地址。它允许这个成员与类中的其他非静态成员共享地址空间。

  • 要注意 :如果是相同类型的多个空成员,这个优化可能会失效。

  • 编译器支持情况 :VS2019 和 VS2022 均未验证出这里的优化效果,GCC 9.0 验证出了这里的优化效果。

cpp 复制代码
struct Empty1 {};
struct Empty2 {};

struct Foo {
    int x;                           // 4 bytes
    [[no_unique_address]] Empty1 e1; // 可能被优化掉
    [[no_unique_address]] Empty2 e2; // 可能被优化掉
};

// 空基类优化 (EBO - Empty Base Optimization)
struct Fxx : Empty1 {
    int x;
};

struct Fyy : Empty1, Empty2 {
    int y;
};

// 实践使用场景
template<typename Allocator = std::allocator<int>>
class Vector {
private:
    int* data;
    size_t size;
    size_t capacity;
    [[no_unique_address]] Allocator alloc; // allocator 没有非静态成员变量
};

int main() {
    std::cout << sizeof(Empty1) << std::endl;
    std::cout << sizeof(Foo) << std::endl;

    // EBO 可以优化一个空基类,多个空基类就不行了
    std::cout << sizeof(Fxx) << std::endl;
    std::cout << sizeof(Fyy) << std::endl;

    std::cout << sizeof(std::allocator<int>) << std::endl;
    return 0;
}

1.11_has_include

https://cppreference.cn/w/cpp/preprocessor/include

__has_include 是 C++17 引入的一个特殊预处理器功能,用于在编译时检查某个头文件是否可以被包含。

  • __has_include (<头文件名>)__has_include ("头文件名") 返回一个可以被预处理器解释的整型常量表达式:1 表示头文件存在且可被包含,0 表示头文件不可用。

  • 它的主要目的是为了编写跨平台跨编译器 的可移植代码。不同的系统或编译器版本可能提供不同的头文件库。使用 __has_include,你可以有条件地包含头文件或采取备选方案,从而避免编译错误。

cpp 复制代码
#include <iostream>

// 示例1: 检查标准库头文件
#if __has_include(<optional>)
    #include <optional>
    #define HAS_OPTIONAL 1
#else
    #define HAS_OPTIONAL 0
#endif

// 示例2: 检查自定义头文件
#if __has_include("my_header.h")
    #include "my_header.h"
    #define HAS_MY_HEADER 1
#else
    #define HAS_MY_HEADER 0
#endif

// 示例3: 检查是否支持文件系统相关库
#if __has_include(<filesystem>)
    #include <filesystem>
    namespace fs = std::filesystem;
#elif __has_include(<experimental/filesystem>)//实验性filesystem
    #include <experimental/filesystem>
    namespace fs = std::experimental::filesystem;
#else
    #error "需要 filesystem 支持"
#endif

int main() {
    std::cout << "Optional support: " << HAS_OPTIONAL << std::endl;
    std::cout << "My header support: " << HAS_MY_HEADER << std::endl;

    // 实际使用
    #if HAS_OPTIONAL
        std::optional<int> opt = 42;
        std::cout << "Optional value: " << *opt << std::endl;
    #else
        std::cout << "Optional not available" << std::endl;
    #endif

    return 0;
}

1.12新的求值顺序(了解即可,日常不建议写出这样的代码)

https://cppreference.cn/w/cpp/language/eval_order

在 C++17 之前,很多表达式的求值顺序是未指定的(unspecified) ,这意味着编译器可以自由选择求值顺序,这导致了不确定性和潜在的 bug。C++17 的求值顺序规则大大提高了代码的可预测性安全性 ,消除了许多历史遗留的未定义行为问题。

建议 :还是不要依赖未指定的求值顺序,即使 C++17 修复了一些问题,最好还是写出不依赖特定求值顺序 的代码,否则代码可读性和维护性都会变差。建议将复杂的表达式分解成多个简单的语句

表达式类型 C++17 前 C++17 后
a = b 未指定 先求值 b,再求值 a
a += b, a -= b 未指定 先求值 b,再求值 a
a << b, a >> b 未指定 从左到右求值
a[b] 未指定 先求值 a,再求值 b
a->b 未指定 先求值 a,再求值 b
a.b 未指定 先求值 a,再求值 b
f(a, b, c) 未指定 参数求值顺序仍未指定,但都在函数调用前完成
new Type(a, b) 未指定 先求值所有参数,再分配内存

实践中不要写这种求值顺序不明显的代码,可读性差不说还有可能搬起石头砸自己的脚。

相关推荐
草莓熊Lotso1 小时前
【Linux网络】深入理解 HTTP 协议(一):从基础概念到 URL 编码解码
linux·网络·c++·网络协议·http·软件工程
一只旭宝1 小时前
【C++入门精讲17】序列容器
开发语言·c++
Demon1_Coder1 小时前
Day1-SpringAI-1.0.0版本
java·开发语言·前端
郝学胜-神的一滴1 小时前
Qt 高级开发 021:零基础吃透 QVBoxLayout 垂直布局
开发语言·c++·qt·程序人生·用户界面
basketball6161 小时前
C++进阶:2. std::move 和 std::forward 函数
java·开发语言·c++
_oP_i1 小时前
105、word 出现 {TOCO“1-2“HZ}
开发语言·c#·word
玖釉-1 小时前
LeetCode Hot 100 知识点总结与算法指南
c++·windows·算法·leetcode
yong99901 小时前
基于MATLAB的雷达数字信号处理
开发语言·matlab·信号处理
SilentSamsara1 小时前
HTTP 客户端实战:httpx/重试/限速/连接池/中间件设计
开发语言·网络·python·http·青少年编程·中间件·httpx