C++17特性(3)

9. ⾮类型模板参数

⾮类型模板参数是⼀个具有特定类型的常量值,在 C++17 之前,⾮类型模板参数的类型必须被明确 指定。C++17 允许我们使⽤ auto 关键字作为⾮类型模板参数的占位符,编译器会根据实例化时
提供的实参⾃动推导出该参数的类型。
在 C++20 之前,⾮类型模板参数的类型被严格限制在:整型/枚举类型/指针类型/左值引
⽤/ std::nullptr_t ,C++20 打破了这⼀限制,允许浮点数,允许你使⽤满⾜特定条件的类类
型(称为"字⾯量类类型")的对象作为编译期常量,并传递给模板。字⾯量类类型C++20会具体
讲解,简单来说,它必须是⼀个可以在编译期完全构造和析构的简单、透明的数据结构,如
std::array ,简单的⾃定义结构体 struct Point { int x; int y; }; 等

复制代码
#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")>(); // 报错
}

10. 嵌套命名空间定义

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

复制代码
#include <iostream>
// C++17之前的嵌套命名空间
namespace A {
namespace B {
namespace C {
void foo() {
std::cout << "Old nested namespace style" << std::endl;
}
}
}
}
// C++17的简洁嵌套命名空间
namespace A::B::C {
void bar() {
std::cout << "New nested namespace style" << std::endl;
}
}
namespace A::B {
void qux() {
std::cout << "Partially nested namespace" << std::endl;
}
}
int main() {
A::B::C::foo();
A::B::C::bar();
A::B::qux();
return 0;
}

11. __has_include

• __has_include 是 C++17 引⼊的⼀个特殊预处理器功能,⽤于在编译时检查某个头⽂件是否可以被 包含。
__has_include (< 头⽂件名 >) 或 __has_include (" 头⽂件名 ") 它返回⼀个可以被预
处理器解释的整型常量表达式,1 表⽰头⽂件存在且可被包含 0 表⽰头⽂件不可⽤。
• 它的主要⽬的是为了编写跨平台或跨编译器的可移植代码。不同的系统或编译器版本可能提供不同 的头⽂件库。使⽤ __has_include ,你可以有条件地包含头⽂件或采取备选⽅案,从⽽避免编
译错误。

复制代码
#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>)
#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;
}

12. 属性

• 属性是为代码实体(变量、函数、类等)添加额外信息的⼀种机制,这些信息可以帮助编译器进⾏ 优化、⽣成警告或错误,以及指导静态分析⼯具。
• 在 C++11 之前,各编译器使⽤⾃⼰的语法。

复制代码
// GCC/Clang:
__attribute__((noreturn)) void func() {}
__attribute__((deprecated)) void old_func() {}
// MSVC
__declspec(noreturn) void func() {}
__declspec(deprecated) void old_func() {}
// C++以后,使⽤统⼀的标准库属性
[[noreturn]] void func() {}
[[deprecated]] void old_func() {} // 注意:deprecated是C++14加⼊的

C++11将属性语法标准化引⼊了 \[ ] 语法,但只提供了两个标准属性。C++14/C++17/C++20
中属性逐步丰富化。
\[noreturn] (C++11),指⽰函数不会返回到它的调⽤点,主要⽤于优化和错误检测。通过明
确告知编译器函数不会返回,编译器可以优化代码逻辑,例如跳过某些⽆效的后续代码执⾏等。标
记为 noreturn 的函数返回时,编译器会提⽰警告。

\[carries_dependency] (C++11),⾼级并发属性,它是为专家级并发优化提供的⼯具,
但对于 99.9% 的开发者来说,使⽤ std::atomic 默认内存序或其他⾼级并发⼯具是更明智的
选择。

复制代码
#include <cstdlib>
#include <iostream>
#include <stdexcept>
// 1. 终⽌程序的函数
[[noreturn]] void fatal_error(const std::string& message) {
std::cerr << "Fatal error: " << message << std::endl;
std::exit(EXIT_FAILURE);
// 不需要 return 语句
}
// 2. 总是抛出异常的函数
[[noreturn]] void throw_runtime_error(const char* message) {
throw std::runtime_error(message);
// 不会执⾏到这⾥
}
// 3. ⽆限循环的函数
[[noreturn]] void event_loop() {
while (true) {
// 处理事件...
}
// 不会执⾏到这⾥
}
// 编译器优化⽰例
int main() {
fatal_error("error exit");
}

\[deprecated] (C++14),它⽤于标记某个实体(如函数、类、变量、类型等)已被弃⽤,不
建议再使⽤。当编译器遇到使⽤被标记为 deprecated 的实体时,会发出编译

复制代码
#include <iostream>
// 标记⼀个函数为弃⽤
[[deprecated]]
void old_function();
// 可以提供⼀条⾃定义的警告信息
[[deprecated("Use the new_function() instead, which is safer and faster.")]]
void legacy_function();
// 也可以标记类、类型别名、变量等
[[deprecated]]
typedef char* CString;
struct [[deprecated]] OldStruct {};
[[deprecated]]
int obsolete_variable;
[[deprecated("This function is prone to errors. Use calculate() instead.")]]
int compute() {
return 42;
}
// deprecated也可以⽤于类型、变量、枚举等
// v1.0
[[deprecated("这是v1.0版本,请使⽤v2.0改⽤connect(url, options)")]]
void connect(std::string url)
{}
// v2.0
void connect(std::string url, int options)
{}
int main() {
int x = compute(); // 编译时会产⽣警告或错误
std::cout << x << std::endl;
CString ptr = nullptr;
connect("https://en.cppreference.com/w/cpp/language/attributes.html");
return 0;
}

C++17 对属性系统进⾏了更重⼤的扩展,引⼊了三个⾮常重要的新属性,并极⼤地扩展了属性的应
⽤范围。
\[fallthrough] (C++17), ⽤于在 switch 语句中显式地表明从⼀个 case 标签"贯
穿"到下⼀个 case 标签是有意为之的,以避免编译器发出警告。
\[nodiscard] (C++17),⽤于标记⼀个函数的返回值⾮常重要,调⽤者不应该忽略它。如果
调⽤者没有使⽤返回值,编译器会发出警告。C++20 允许添加原因信息:

\[nodiscard("xxx")\]

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

复制代码
#include <iostream>
// [[fallthrough]] 属性
void checkValue(int x) {
switch (x) {
case 1:
std::cout << "Case 1" << std::endl;
[[fallthrough]]; // 故意不break,明确告知编译器
case 2:
std::cout << "Case 2" << std::endl;
break;
default:
std::cout << "Default case" << std::endl;
}
}
// [[nodiscard]] 属性
[[nodiscard]] int computeSomethingImportant() {
return 42;
}
// [[maybe_unused]] 属性
void someFunction([[maybe_unused]] int unusedParam) {
// 当你确实有意定义⼀个可能不会使⽤的变量(例如,
// 条件编译、为了清晰的代码结构、或者为了将来扩展)时,避免编译器产⽣烦⼈的警告
[[maybe_unused]] int unusedVar = 10;
// 不会产⽣未使⽤变量的警告
}
int main() {
checkValue(1);
// 忽略[[nodiscard]]结果的警告
computeSomethingImportant(); // 编译器可能会警告
// 正确的⽅式是使⽤返回值
int result = computeSomethingImportant();
std::cout << "Result: " << result << std::endl;
someFunction(100);
checkValue(1);
return 0;
}

\[likely] 和 \[unlikely] (C++20),这两个属性是⽤于向编译器提供分⽀预测的提
⽰,帮助编译器优化代码,使其在条件分⽀更可能或更不可能发⽣时,⽣成效率更⾼的机器代码。
• 现代 CPU 采⽤流⽔线技术,可以预先获取和解码后续的指令。当遇到条件分⽀(如 if 或
switch )时,CPU 必须猜测(预测)会⾛哪条路径以便提前加载指令。如果猜错了,就需要清
空流⽔线,重新加载正确的指令,这会带来显著的性能惩罚(称为分⽀预测错误惩罚)。
\[likely] : 向编译器指⽰,它所在的代码路径(例如, if 语句的 true 分⽀,或
switch 语句的某个 case )在程序执⾏过程中⾮常可能被采⽤。 \[unlikely] : 向编译器
指⽰,它所在的代码路径⾮常不可能被采⽤。准规定这些属性只是⼀个提⽰,编译器可以完全忽略
它们。编译器可能会根据⾃⼰的分析结果覆盖你的提⽰。
• 不要滥⽤: 只有在你有充分的理由证明某个分⽀确实存在极端的概率偏差时,才使⽤这些属性。 盲 ⽬使⽤可能会误导编译器,会适得其反,反⽽导致性能下降。

复制代码
int process(int value) {
if (value > 0) [[likely]] {
// 这个分⽀在绝⼤多数情况下都会执⾏
return value * 2;
}
else [[unlikely]] {
// 错误处理路径,很少执⾏
//log_error("Invalid value");
return -1;
}

}
void func()
{
int status = 0;
std::cin >> status;
// 也可以⽤于 switch 语句
switch (status) {
case 1: [[likely]]
//handle_success();
break;
case 2: [[unlikely]]
//handle_error();
break;
default: [[unlikely]]
// handle_unknown();
break;
}
}
int main()
{
process(1);
func();
return 0;
}

\[no_unique_address](C++20)⽤于优化类的内存布局。它告诉编译器:被修饰的⾮静态数据
空成员可能不需要在对象中拥有独⽴的地址空间,空成员就是⽆⾮静态成员变量的类对象。
• 当前的问题,在 C++ 中,任何对象(即使是空类 class Empty {}; 的对象)都必须拥有⼀个
唯⼀的地址。这意味着它的 sizeof ⾄少为 1(通常为 1),以确保在数组中,每个对象都能被
区分开。当你有⼀个类( struct 或 class )包含多个空类类型的成员时,每个空成员都会⾄
少占⽤ 1 字节,再加上内存对⻬的填充字节,会导致⼤量内存浪费。
• 空基类优化 (EBO - Empty Base Optimization),C++ 标准允许编译器对空基类进⾏优化。如果⼀
个类继承⾃空基类,编译器可以不为基类⼦对象分配任何空间,使其⼤⼩为零。
\[no_unique_address] 的类似 EBO的优化 ,它可以应⽤于⾮静态数据成员,提⽰编译
器:如果该成员是空的,则不需要为其分配⼀个唯⼀的地址。它允许这个成员与类中的其他⾮静态
成员共享地址空间。
• 要注意如果是相同类型的多个空成员,这个优化可能会失效。
• vs2019和vs2022均未验证出这⾥的优化效果,gcc9.0验证出了这⾥的优化效果。

复制代码
块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;
}
相关推荐
qq_452396231 分钟前
第九篇:《Dockerfile 指令精讲(二):WORKDIR、ENV、ARG、EXPOSE》
java·开发语言·docker
JAVA社区3 分钟前
Java高级全套教程(九)—— SpringCloud超详细实战详解
java·开发语言·后端·spring cloud·面试·职场和发展
还在点灯@5 分钟前
基于visual studio的MFC上位机实现界面切换
c++·visualstudio·mfc
wyjcxyyy5 分钟前
java反序列化-cc1链
java·c语言·开发语言
山上三树5 分钟前
Python 高频报错速查表(开发通用版)
开发语言·python
傻啦嘿哟10 分钟前
解决DNS污染:防止OpenClaw解析API域名到虚假地址
开发语言·php
MY_TEUCK10 分钟前
【MYTRUCK - AI 应用】MetaGPT 0.8.2 安装与排错完整实录(Python 3.10 + 虚拟环境)
开发语言·人工智能·python·ai
视图猿人13 分钟前
ROS2 JAZZY+Gazebo harmonic小车机器人建模、激光雷达使用、图像传感器使用、构建导航地图、SLAM自动导航仿真
c++·机器人
林森lsjs15 分钟前
【日耕一题】2. 面向对象 Java 基础:构造方法与 toString
java·开发语言
广_17 分钟前
用AI写一个Python实时硬件监控与日志可视化界面
开发语言·人工智能·python