C++ 11/14作为一个奠基版本,构造了近年来编写C++的新范式。
本人熟悉的主要语言技术栈有C/C++, Python, Matlab, C#, 相比之下,C++的变化是最频繁的,也是最有趣的
多数人已然熟悉C++11/14的用法,本Gist仓库旨在总结一些17及以后版本的特性。
欢迎在讨论区发表相应见解。
目录
C++ 11/14作为一个奠基版本,构造了近年来编写C++的新范式。
本人熟悉的主要语言技术栈有C/C++, Python, Matlab, C#, 相比之下,C++的变化是最频繁的,也是最有趣的
多数人已然熟悉C++11/14的用法,本Gist仓库旨在总结一些17及以后版本的特性。
欢迎在讨论区发表相应见解。
目录
引入语法糖,解构类型为 tuple、pair 或具有 get<>
成员的结构。
std::pair<int, double> p{42, 3.14};
auto [a, b] = p; // a: int, b: double
结构绑定等价于调用 std::get<0>(p)
和 std::get<1>(p)
。
允许在条件判断前进行局部变量定义:
if (int x = f(); x > 0) {
// 使用 x
}
用于限制作用域并提升代码清晰性。
针对可变参数模板提供简洁聚合运算方式。
template<typename... Args>
auto sum(Args... args) {
return (... + args); // 从左到右相加
}
形式支持:
(... op pack)
(pack op ...)
(pack op ... op init)
在 constexpr 函数体中允许使用:
constexpr int factorial(int n) {
int result = 1;
for (int i = 1; i <= n; ++i) result *= i;
return result;
}
用于头文件中全局 constexpr 定义,避免多重定义。
inline constexpr int version = 1;
用于警告被忽略的返回值(避免遗漏重要调用结果)。
[[nodiscard]] int compute() { return 42; }
compute(); // 可能被编译器警告
用于模板上下文下的条件编译:
template<typename T>
void print(const T& val) {
if constexpr (std::is_integral_v<T>)
std::cout << "Integral: " << val << "\n";
else
std::cout << "Other: " << val << "\n";
}
可选值容器,可能有值也可能无值。
std::optional<int> get() {
if (...) return 42;
return std::nullopt;
}
if (auto val = get(); val)
std::cout << *val;
标准实现简析(摘自 libc++)
template <class T>
class optional {
union { T val; };
bool has_value;
};
类型安全的联合体,支持多种类型之一。
std::variant<int, std::string> v = "text";
v = 10;
std::visit([](auto&& x) { std::cout << x; }, v);
使用索引或 std::get_if<T>
来访问成员。
可存放任意类型对象(运行时类型擦除)。
std::any a = 1;
a = std::string("test");
if (a.type() == typeid(std::string))
std::cout << std::any_cast<std::string>(a);
本质上封装了类型信息和析构函数指针。
非拥有、只读的字符串视图(避免复制)。
void log(std::string_view msg) {
std::cout << msg;
}
log("hello"); // 允许字面量
与 std::string
不兼容构造(不拥有内存)。
跨平台文件与路径处理 API。
#include <filesystem>
namespace fs = std::filesystem;
if (fs::exists("data.txt")) {
auto size = fs::file_size("data.txt");
}
常用操作:
path / "file"
directory_iterator
用于一次加锁多个 mutex,避免死锁。
std::scoped_lock lock(m1, m2); // 原子加锁
提供多个读者 / 单个写者访问:
std::shared_mutex mutex;
{
std::shared_lock lock(mutex); // 多读
}
{
std::unique_lock lock(mutex); // 独写
}
统一调用普通函数、成员函数、函数对象等:
std::invoke(f, args...);
将 tuple 参数展开用于函数调用:
auto args = std::make_tuple(1, 2);
auto result = std::apply([](int a, int b) { return a + b; }, args);
C++17 是一个“可用性增强”版本,重点改进包括:
相比 C++11/14,C++17 在实践中大幅降低了模板复杂度和常见代码样板,是现代 C++ 编程的推荐入门版本之一。
对模板参数施加约束,提高模板可读性和错误提示质量。
template<typename T>
concept Number = std::is_arithmetic_v<T>;
template<Number T>
T add(T a, T b) { return a + b; }
统一实现 <
, ==
, >
等比较操作。
#include <compare>
struct Point {
int x, y;
auto operator<=>(const Point&) const = default;
};
用 views::
管道操作表达序列变换:
#include <ranges>
for (int i : std::views::iota(1, 10) | std::views::filter([](int x){ return x % 2 == 0; }))
std::cout << i << " ";
通过 co_await
, co_yield
, co_return
实现非阻塞协程。
task<int> foo() {
co_return 42;
}
(完整协程需要协程 promise 和协程句柄封装)
替代头文件的模块系统(编译器支持有限)。
// math.ixx
export module math;
export int square(int x) { return x * x; }
使用:
import math;
int y = square(5);
consteval int cube(int x) { return x * x * x; }
constinit int global = 42;
支持 template<auto>
等:
template<auto N>
void printNTimes() { ... }
轻量非拥有数组视图,不拷贝数据。
void print(std::span<int> s) {
for (int i : s) std::cout << i;
}
类似 Python f-string 的格式化输出:
#include <format>
std::cout << std::format("value = {}", 42);
范围处理功能分为:
views::
(惰性生成、变换)actions::
(修改容器)ranges::
(通用接口)auto evens = vec | std::views::filter([](int x){ return x % 2 == 0; });
如:
std::same_as<T, U>
std::convertible_to<T, U>
std::invocable<F, Args...>
示例:
template<std::integral T>
void f(T x);
类型安全的强制转换(要求类型大小相同):
float f = 3.14f;
uint32_t u = std::bit_cast<uint32_t>(f);
自动 join 的线程:
std::jthread t([] { do_work(); }); // 析构时自动 join
用于线程取消协作机制:
void run(std::stop_token st) {
while (!st.stop_requested()) { ... }
}
仅可移动的泛型可调用封装器:
std::move_only_function<void()> f = [] { ... };
C++20 是继 C++11 之后又一次大的语言升级,核心目标包括:
C++20 被广泛认为是现代 C++ 成熟阶段的重要标志。
允许将 this
作为显式形参,支持更灵活的成员函数调用。
struct S {
void func(this S& self, int x) {
self.value = x;
}
int value;
};
用户自定义类型支持 operator[](...)
形式的多维下标。
struct Matrix {
int operator[](size_t i, size_t j) const;
};
用于判断当前是否处于 consteval
上下文。
consteval int always_constexpr() { return 1; }
constexpr int f() {
if consteval {
return always_constexpr();
} else {
return 0;
}
}
可直接引入类型别名:
template<typename T>
void f(alias A = typename T::value_type);
auto l = []<typename T>(T x) [[nodiscard]] { return x + 1; };
类模板参数推导支持更多构造情况,提升推导准确性。
template<typename T>
requires std::integral<T>
struct X { T value; };
X x = X{42}; // 仅当 T 满足 integral
用于替代 std::optional
表达错误信息:
std::expected<int, std::string> parse(std::string_view s);
if (res) {
int val = *res;
} else {
std::cerr << res.error();
}
与 std::format
结合的便捷输出:
std::print("value: {}
", 42);
std::println("hello {}", "world");
轻量协程生成器:
std::generator<int> gen() {
for (int i = 0; i < 3; ++i)
co_yield i;
}
泛型回调对象,移动语义支持:
std::move_only_function<void()> f = [] { do_something(); };
排序向量构造的 map/set,插入慢、查找快、占用小。
用于捕获运行时调用栈信息:
auto st = std::stacktrace::current();
std::cout << st;
constexpr
支持更多 STL 类型(如 std::vector
部分操作)operator[]
可 constexpr
static operator()
/ static []
(实验性提案)C++23 是对 C++20 的迭代补强版本,主要特点是:
C++23 兼容性强,适用于需要现代语法同时强调编译期控制与运行期效率的项目。
C++20 协程是一种语言扩展,支持将函数挂起并在未来恢复,适用于异步 IO、生成器、状态机等场景。
关键保留字:
co_await
: 暂停并等待一个 awaitable 对象完成co_yield
: 生成一个值,挂起协程(用于生成器)co_return
: 从协程返回值一个使用 co_await
, co_yield
, co_return
的函数会被编译器转换为状态机,其返回类型必须满足如下接口:
promise_type
handle_type = std::coroutine_handle<promise_type>
promise_type::get_return_object()
promise_type::initial_suspend()
promise_type::final_suspend()
promise_type::return_void()
或 return_value()
promise_type::unhandled_exception()
#include <coroutine>
#include <iostream>
struct Task {
struct promise_type {
Task get_return_object() { return {}; }
std::suspend_never initial_suspend() { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() {}
};
};
Task simple() {
std::cout << "In coroutine
";
co_return;
}
int main() {
simple();
}
该协程立即执行,不挂起。若需挂起/恢复,需定义 std::suspend_always
或 std::suspend_never
以控制。
满足以下接口即可被 co_await
:
await_ready()
→ bool:是否立即完成(返回 true 则不挂起)await_suspend(std::coroutine_handle<>)
:挂起行为,传入当前协程句柄await_resume()
:恢复后返回值struct Awaiter {
bool await_ready() const noexcept { return false; }
void await_suspend(std::coroutine_handle<> h) const {
std::cout << "Suspending
";
h.resume(); // 立即恢复
}
int await_resume() const noexcept {
return 42;
}
};
Task example() {
int val = co_await Awaiter{};
std::cout << "Got " << val << "
";
}
#include <coroutine>
#include <iostream>
template<typename T>
struct Generator {
struct promise_type;
using handle_type = std::coroutine_handle<promise_type>;
struct promise_type {
T value;
Generator get_return_object() { return Generator{handle_type::from_promise(*this)}; }
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
std::suspend_always yield_value(T v) {
value = v;
return {};
}
void return_void() {}
void unhandled_exception() { std::terminate(); }
};
handle_type h;
Generator(handle_type h): h(h) {}
~Generator() { if (h) h.destroy(); }
bool next() {
h.resume();
return !h.done();
}
T current_value() const {
return h.promise().value;
}
};
Generator<int> gen() {
for (int i = 0; i < 3; ++i)
co_yield i;
}
int main() {
auto g = gen();
while (g.next()) {
std::cout << g.current_value() << "
";
}
}
结合 std::stop_token
实现线程取消:
struct SleepAwaitable {
std::chrono::milliseconds dur;
bool await_ready() { return false; }
void await_suspend(std::coroutine_handle<> h) {
std::thread([h, d=dur]() {
std::this_thread::sleep_for(d);
h.resume();
}).detach();
}
void await_resume() {}
};
使用:
Task delayed() {
std::cout << "Wait...
";
co_await SleepAwaitable{1s};
std::cout << "Done
";
}
std::generator<T>
:标准生成器std::task<T>
(未来 TS):表示可等待任务std::sync_wait
, std::when_all
等协程组合工具(需配合执行器实现)C++ 协程机制高度底层,编译器将其转换为状态机,通过 promise_type
与 coroutine_handle
控制生命周期。
使用协程应理解:
co_await
本质是协程挂起点,依赖被等待对象是否决定挂起建议实践中使用封装好的框架如 cppcoro
或 libunifex
,以避免手动实现完整的状态管理。
在C++编程中,智能指针(如 std::unique_ptr
和 std::shared_ptr
)是管理动态内存、防止资源泄漏的重要工具。然而,当C++代码需要与传统的C风格API或一些期望通过输出参数(通常是 T**
或 T*&
)来分配或修改指针的库交互时,智能指针的非侵入式特性会带来一些不便和潜在风险。C++23引入的 std::out_ptr
和 std::inout_ptr
适配器旨在优雅地解决这一问题。
当我们使用智能指针时,获取其管理的裸指针通常通过 get()
方法或解引用操作符 *
。这对于接受 T*
参数(且不获取所有权)的C风格函数非常方便:
// C风格函数,不获取所有权
void ProcessObject(Foo* ptr);
// C++代码
std::unique_ptr<Foo> smart_foo = std::make_unique<Foo>();
ProcessObject(smart_foo.get()); // 安全且方便
然而,问题出现在当C风格API期望通过一个指向指针的指针(T**
)或指针的引用(T*&
)来返回一个新分配的对象或修改一个已有的指针时:
// C风格函数,通过出参分配新对象
bool CreateObject(Foo** ppObject);
// C风格函数,可能释放旧对象并分配新对象
bool RefreshObject(Foo*& pObject); // 或 Foo** pObject
直接将智能指针用于此类接口会导致编译错误,因为智能指针不提供直接获取其内部裸指针地址的操作。传统的C++做法是:
std::unique_ptr<Foo> sp_foo;
Foo* raw_ptr = nullptr;
if (CreateObject(&raw_ptr)) {
sp_foo.reset(raw_ptr); // raw_ptr现在由智能指针管理
} else {
// 如果CreateObject内部只分配了内存但返回false,raw_ptr可能需要手动delete
// delete raw_ptr; // 视CreateObject的具体行为而定
}
这种手动管理的方式存在一个资源泄漏的风险窗口:在 CreateObject
成功分配 raw_ptr
之后,到 sp_foo.reset(raw_ptr)
执行之前,如果发生任何异常,raw_ptr
所指向的内存将无法被 unique_ptr
接管,从而导致泄漏。
为了解决上述问题,一种常见的模式是使用一个临时的代理(Proxy)对象。这个代理对象在其构造函数中接收智能指针的引用,并提供一个可以隐式转换为 T**
的接口。在其析构函数中,它会将通过C API获取到的裸指针 reset
给智能指针。
一个简化的 UniquePtrProxy
可能如下:
template<typename T>
struct UniquePtrProxy {
std::unique_ptr<T>& m_output_sp; // 引用待更新的智能指针
T* m_raw_ptr_cache = nullptr; // 临时存储C API返回的裸指针
explicit UniquePtrProxy(std::unique_ptr<T>& sp) : m_output_sp(sp) {}
// RAII:确保在代理对象生命周期结束时,智能指针被更新
~UniquePtrProxy() {
if (m_raw_ptr_cache) { // 只有当C API确实输出了指针才reset
m_output_sp.reset(m_raw_ptr_cache);
}
}
// 禁止拷贝和赋值
UniquePtrProxy(const UniquePtrProxy&) = delete;
UniquePtrProxy& operator=(const UniquePtrProxy&) = delete;
// 允许隐式转换为 T**,供C API使用
operator T**() {
// C API可能会写入新的指针到m_raw_ptr_cache
// 如果智能指针已有对象,其所有权会在此处丢失,除非C API负责释放
// 或者像std::inout_ptr那样,在转换前release()
return &m_raw_ptr_cache;
}
// 对于某些需要 T*& 的API (更复杂,通常inout_ptr更适合)
operator T*&() {
m_raw_ptr_cache = m_output_sp.release(); // 智能指针释放所有权
return m_raw_ptr_cache; // C API可以直接修改这个裸指针
}
};
使用起来可能是这样:
std::unique_ptr<Foo> sp_foo;
if (CreateObject(UniquePtrProxy<Foo>(sp_foo))) {
// 使用 sp_foo
}
这种代理模式利用RAII特性,在代理对象(通常是临时对象)生命周期结束时自动更新智能指针,从而缩小了资源泄漏的风险窗口。
局限性: 这种临时代理对象的方式有一个经典的陷阱,即临时对象的生命周期问题。考虑以下代码:
std::unique_ptr<Foo> sp_foo;
// 错误示例:临时对象生命周期问题
if (CreateObject(UniquePtrProxy<Foo>(sp_foo)) && sp_foo) { // 检查sp_foo是否有效
// ...
}
在C++中,UniquePtrProxy<Foo>(sp_foo)
创建的临时对象,其析构(即sp_foo.reset()
的调用)通常会延迟到包含该临时对象的完整表达式(整个 if
语句条件)求值完毕之后。这意味着,在 && sp_foo
这个子表达式被求值时,sp_foo
还没有被 UniquePtrProxy
的析构函数所更新。因此,即使 CreateObject
成功,sp_foo
在检查时仍然是空的,导致 if
块永远不会执行。
C++23标准库提供了官方的解决方案:std::out_ptr_t
和 std::inout_ptr_t
适配器类,以及对应的辅助函数 std::out_ptr
和 std::inout_ptr
。
std::out_ptr_t<SmartPointer, Pointer, Args...>
:
用于纯输出参数的场景,即C API会分配新的对象,并将指针写入提供的 Pointer*
(通常是 T**
)。智能指针 SmartPointer
在此操作前应为空,或其原有对象会被正确处理(通常是先 release()
或 reset()
)。
std::unique_ptr<Foo> sp_foo;
// 显式使用适配器类,模板参数较多
if (CreateObject(std::out_ptr_t<std::unique_ptr<Foo>, Foo*>(sp_foo))) {
if (sp_foo) {
// 使用 sp_foo
}
}
std::inout_ptr_t<SmartPointer, Pointer, Args...>
:
用于输入/输出参数的场景。C API可能会读取传入的指针,释放其指向的资源,然后分配新的资源并更新指针。这个适配器通常会先调用智能指针的 release()
方法,将所有权转移给一个临时裸指针,传递给C API,然后在适配器析构时用C API返回的新指针(或原指针,如果未改变)重新构建智能指针。
std::unique_ptr<Foo> sp_foo = std::make_unique<Foo>(/*..initial_args..*/);
// 显式使用适配器类
// 假设RefreshObject是 Foo*& 类型,并且会先delete传入的指针,再分配新的
if (RefreshObject(std::inout_ptr_t<std::unique_ptr<Foo>, Foo*>(sp_foo))) {
if (sp_foo) {
// 使用 sp_foo
}
}
为了简化 std::out_ptr_t
和 std::inout_ptr_t
的使用,标准库提供了模板辅助函数,它们可以根据传入的智能指针类型自动推导模板参数:
std::out_ptr(SmartPointer& sp, Args&&... args)
: 返回 std::out_ptr_t
对象。std::inout_ptr(SmartPointer& sp, Args&&... args)
: 返回 std::inout_ptr_t
对象。使用辅助函数,代码变得更简洁:
// 使用 std::out_ptr
std::unique_ptr<Foo> sp_foo_out;
if (CreateObject(std::out_ptr(sp_foo_out))) { // 假设CreateObject接受 Foo**
if (sp_foo_out) { /* ... */ }
}
// 使用 std::inout_ptr
std::unique_ptr<Foo> sp_foo_inout = std::make_unique<Foo>();
// 假设RefreshObject接受 Foo*&
// 注意:std::inout_ptr(sp_foo_inout)的转换操作符会返回一个Foo**类型,
// 如果RefreshObject严格要求Foo*&,可能需要一个更特定的适配器或API调整。
// 标准库的std::inout_ptr主要还是针对 T**。
// 如果RefreshObject是 void RefreshObject(Foo** ppFoo) 类型:
if (RefreshObject(std::inout_ptr(sp_foo_inout))) {
if (sp_foo_inout) { /* ... */ }
}
临时对象的生命周期:
与传统的代理类方案类似,std::out_ptr
和 std::inout_ptr
返回的临时适配器对象,其生命周期问题依然存在。如下代码仍然是错误的:
std::unique_ptr<Foo> sp_foo;
// 错误:sp_foo在条件判断时尚未被更新
if (CreateObject(std::out_ptr(sp_foo)) && sp_foo) {
// ...
}
正确的做法是分步:
std::unique_ptr<Foo> sp_foo;
bool success = CreateObject(std::out_ptr(sp_foo)); // 适配器在此完整表达式结束后析构
if (success && sp_foo) { // 此处sp_foo已被正确更新
// ...
}
避免延长临时适配器对象的生命周期:
不要使用 auto&&
或其他方式来延长 std::out_ptr_t
/ std::inout_ptr_t
临时对象的生命周期,这会导致其析构延迟,使得智能指针的更新晚于预期。
std::unique_ptr<Foo> sp_foo;
// 错误:rrr延长了临时对象的生命周期
auto&& rrr = std::out_ptr(sp_foo);
if (CreateObject(rrr)) {
// 此时sp_foo仍为空,因为rrr还未析构
if (sp_foo) { /* ... */ }
}
// rrr在此处(或作用域结束时)析构,sp_foo才被更新
std::inout_ptr_t
与 std::shared_ptr
:
std::inout_ptr_t
的行为是先释放智能指针原有的所有权,然后用C API返回的指针重新初始化。这种操作模式与 std::shared_ptr
的共享所有权语义不兼容,因此 std::inout_ptr_t
(及 std::inout_ptr
) 不能用于 std::shared_ptr
。std::out_ptr_t
可以用于空的 std::shared_ptr
。
C API的指针处理语义:
使用这些适配器前,必须清楚C API对于传入的 T**
或 T*&
参数是如何操作的:
out_ptr
:API是否总是分配新内存?API是否会处理(如 delete
)原先通过 T**
传入的非空指针?标准 out_ptr
的行为是先 reset()
智能指针,所以如果C API不处理传入的指针,而智能指针原来管理着一个对象,该对象会被释放。inout_ptr
:API是否会 delete
或 free
传入的指针所指向的对象?如果API不释放,适配器在 release()
后将所有权交给临时裸指针,C API操作后,适配器析构时会用新指针 reset
智能指针。如果C API没有释放原对象且也没有返回新对象(而是修改了原对象),则需要确保所有权被正确传递。std::out_ptr
和 std::inout_ptr
适配器的引入,为C++开发者带来了诸多好处:
// C API: bool MakeObject(Foo** ppObject);
std::unique_ptr<Foo> sp_foo;
bool success = MakeObject(std::out_ptr(sp_foo)); // 适配器在此完整表达式结束后析构
if (success) {
if (sp_foo) { // sp_foo已被更新
std::cout << "Object created, value = " << sp_foo->GetValue() << std::endl;
}
}
假设 RefreshObject(Foo** ppObject)
会读取 *ppObject
,可能会释放它,然后将 *ppObject
指向新分配的对象。
// C API: bool RefreshObject(Foo** ppObject);
std::unique_ptr<Foo> sp_foo = std::make_unique<Foo>("initial_data");
Foo* old_raw_ptr = sp_foo.get(); // 仅为观察
// std::inout_ptr(sp_foo)会做类似:
// 1. temp_raw_ptr = sp_foo.release(); (sp_foo变为空,所有权交给temp_raw_ptr)
// 2. RefreshObject(&temp_raw_ptr); (C API操作temp_raw_ptr)
// 3. sp_foo.reset(temp_raw_ptr); (适配器析构时,sp_foo接管新/修改后的指针)
bool refreshed = RefreshObject(std::inout_ptr(sp_foo));
if (refreshed) {
if (sp_foo) {
std::cout << "Object refreshed, new value = " << sp_foo->GetValue() << std::endl;
if (sp_foo.get() != old_raw_ptr) {
std::cout << "Pointer was changed by RefreshObject." << std::endl;
}
}
}
std::out_ptr
和 std::inout_ptr
是C++23中非常实用的工具,它们弥合了现代C++智能指针与传统C风格API在指针管理上的鸿沟。正确理解和使用这些适配器,能够显著提升代码的健壮性和简洁性,尤其是在与大量遗留C代码或底层库交互的场景中。务必注意其生命周期和与特定智能指针(如 shared_ptr
)的兼容性问题,以及C API的具体行为,以发挥其最大效用。
C++作为一种静态强类型语言,在编译期就确定了大部分类型信息,这带来了性能和安全上的优势。然而,在某些设计场景下,过于严格的类型约束反而会限制代码的灵活性和通用性。类型擦除(Type Erasure)技术应运而生,它允许我们编写能够操作多种不同具体类型的通用代码,而这些通用代码仅关注这些类型共有的、符合某种抽象定义的“特定行为”,仿佛这些类型的其他特有部分被“擦除”了一样。
实现类型擦除的两个关键要素:
优点:
缺点:
std::any_cast
)以确保操作的正确性,这可能导致运行时错误。类型擦除技术特别适用于以下场景:
采用类型擦除技术的设计通常具备以下特点:
C标准库中的 qsort()
函数是类型擦除的一个早期体现:
void qsort(void *base, size_t nmemb, size_t size,
int (*compare)(const void *, const void *));
qsort()
的排序算法本身不关心待排序元素的具体类型,通过 void*
隐藏了类型信息。compare
函数(特定行为的抽象)来确定元素间的顺序。qsort()
的地方(调用点)知道数组元素的具体类型,并负责提供与该类型匹配的比较函数。当 compare
函数被回调时,需要将 void*
具化回原始类型进行比较。int less_int(const void *lhs, const void *rhs) {
return *(const int*)lhs - *(const int*)rhs;
}
int arr[8] = { 1, 8, 4, 7, 6, 2, 3, 9 };
qsort(arr, sizeof(arr)/sizeof(arr[0]), sizeof(arr[0]), less_int);
虽然 void*
在C++中通常被认为是不够类型安全的,但 qsort()
的设计思想体现了类型擦除的基本原理。
这是最常见的基于继承和虚函数的多态实现,也可以看作一种类型擦除。
// 抽象基类(接口)
class Shape {
public:
virtual ~Shape() = default; // 重要:虚析构函数
virtual void Draw(const Context& ctx) const = 0;
};
// 具体实现
class Rectangle : public Shape {
public:
Rectangle(double w, double h) { /* ... */ }
void Draw(const Context& ctx) const override { /* ... */ }
};
class Circle : public Shape {
public:
Circle(double r) { /* ... */ }
void Draw(const Context& ctx) const override { /* ... */ }
};
// 通用代码
void DrawAllShapes(const std::vector<Shape*>& shapes) {
Context ctx{ /* ... */ };
for(const auto* shape_ptr : shapes) {
if (shape_ptr) {
shape_ptr->Draw(ctx); // 通过基类指针调用虚函数,具体类型被"擦除"
}
}
}
存在的问题与反思:
Draw
方法作为 Shape
的一部分,使得所有形状都必须依赖 Context
。如果图形绘制逻辑并非形状的核心职责,这种设计可能不佳。Triangle
类)必须继承自 Shape
才能被 DrawAllShapes
处理,这限制了代码复用。Shape
增加新行为(如 Serialize
),就需要修改基类 Shape
,违反了开闭原则(OCP),并可能影响整个继承体系。现代C++更倾向于使用模板来实现非侵入式的类型擦除,它不要求具体类型继承自某个公共基类。
// 具体类型,无需共同基类
class Rectangle { /* ... */ };
class Circle { /* ... */ };
// 针对具体类型的独立绘制函数 (特定行为)
void DrawShape(const Context& ctx, const Circle& circle);
void DrawShape(const Context& ctx, const Rectangle& rect);
// 通用代码 (通过模板参数擦除类型)
template <typename ShapeType>
void DrawSingleShape(const ShapeType& shape) {
Context ctx{ /* ... */ };
DrawShape(ctx, shape); // 依赖于ShapeType存在匹配的DrawShape重载
}
这种方法擦除了 DrawSingleShape
内部对具体类型的依赖,但无法将不同类型的 ShapeType
存储在同一个异构容器中(如 std::vector
),因此不是真正的运行时多态容器。
这是一种更强大的技术,它创建一个包装类(Wrapper),该包装类对外提供统一的接口,内部则通过PIMPL(Pointer to Implementation)模式或类似机制来持有一个指向“概念模型”的指针,该模型再间接持有所包装的具体类型对象。
Klaus Iglberger 在 CppCon 2022 的演讲中展示了这种思路。下面是一个简化的示例:
// 假设独立的绘制函数已存在
// void DrawShape(const Context& ctx, const ConcreteShape& shape);
class ShapeWrapper {
private:
// 1. 行为概念接口 (Concept Interface)
struct ShapeConcept {
virtual ~ShapeConcept() = default;
virtual void DoDraw(const Context& ctx) const = 0;
// 如果需要支持拷贝,还需要虚克隆方法
virtual std::unique_ptr<ShapeConcept> Clone() const = 0;
};
// 2. 具体类型模型 (Model) - 模板类,适配具体类型到Concept接口
template<typename ConcreteShape>
struct ShapeModel : public ShapeConcept {
ConcreteShape m_shape_object; // 持有具体类型的对象
ShapeModel(ConcreteShape shape) : m_shape_object(std::move(shape)) {}
void DoDraw(const Context& ctx) const override {
DrawShape(ctx, m_shape_object); // 将调用转发给具体类型的DrawShape
}
std::unique_ptr<ShapeConcept> Clone() const override {
return std::make_unique<ShapeModel<ConcreteShape>>(m_shape_object); // 实现拷贝
}
};
std::unique_ptr<ShapeConcept> m_pimpl; // PIMPL 指针
public:
// 模板构造函数,接受任意类型,并创建对应的Model
template<typename ConcreteShape>
ShapeWrapper(ConcreteShape shape)
: m_pimpl(std::make_unique<ShapeModel<ConcreteShape>>(std::move(shape))) {}
// 拷贝构造函数 (如果支持)
ShapeWrapper(const ShapeWrapper& other) : m_pimpl(other.m_pimpl ? other.m_pimpl->Clone() : nullptr) {}
// 移动构造函数
ShapeWrapper(ShapeWrapper&& other) noexcept = default;
// 拷贝赋值 (如果支持)
ShapeWrapper& operator=(const ShapeWrapper& other) {
if (this != &other) {
m_pimpl = (other.m_pimpl ? other.m_pimpl->Clone() : nullptr);
}
return *this;
}
// 移动赋值
ShapeWrapper& operator=(ShapeWrapper&& other) noexcept = default;
// 公共接口,调用PIMPL的虚方法
void Draw(const Context& ctx) const {
if (m_pimpl) {
m_pimpl->DoDraw(ctx);
}
}
};
现在,我们可以将不同类型的对象存储在 ShapeWrapper
的容器中:
// 假设Triangle类和对应的DrawShape(ctx, Triangle)也已定义
class Triangle { /* ... */ };
void DrawShape(const Context& ctx, const Triangle& triangle);
void DrawAllWrappedShapes(const std::vector<ShapeWrapper>& shapes) {
Context ctx{ /* ... */ };
for(const auto& shape_wrapper : shapes) {
shape_wrapper.Draw(ctx); // 调用的是ShapeWrapper::Draw
}
}
int main() {
std::vector<ShapeWrapper> shapes;
shapes.emplace_back(Circle{5.8});
shapes.emplace_back(Rectangle{15.0, 22.0});
shapes.emplace_back(Triangle{/*...*/}); // 阿猫阿狗的Triangle也能用
DrawAllWrappedShapes(shapes);
return 0;
}
优点:
Circle
, Rectangle
, Triangle
无需继承共同基类。DrawShape
函数。ShapeWrapper
可以(如果实现了 Clone
)支持值拷贝,避免了裸指针管理。ShapeWrapper
的代码。潜在问题与改进:
ShapeModel
中对 DrawShape(ctx, m_shape_object)
的调用是硬编码的。如果不同类型有不同名称的绘制方法,或参数略有差异,就需要更复杂的适配。
std::function
存储具体行为,并在 ShapeWrapper
构造时传入可调用对象。或者使用更高级的元编程技巧。ShapeWrapper
支持更多行为(如 Serialize
),仍需修改 ShapeConcept
和所有 ShapeModel
特化。
ShapeConcept
,或者如原文提及,通过多重继承等方式扩展 ShapeConcept
的能力。std::function
: 封装任意可调用对象(函数指针、lambda、仿函数等),对外提供统一的调用接口,擦除了具体的可调用对象类型。std::any
(C++17): 可以存储任意可拷贝构造类型(CopyConstructible
)的单个值。配合 std::any_cast
进行类型安全的取回。它本身就是一种类型擦除容器。#include <functional>
#include <any>
#include <iostream>
#include <string>
#include <vector>
void print_int(int i) { std::cout << "int: " << i << std::endl; }
void print_string(const std::string& s) { std::cout << "string: " << s << std::endl; }
int main() {
// std::function
std::vector<std::function<void(int)>> callables;
callables.push_back(print_int);
callables.push_back([](int x){ std::cout << "lambda: " << x * x << std::endl; });
for (auto& func : callables) { func(5); }
// std::any
std::vector<std::any> items;
items.push_back(10);
items.push_back(std::string("hello"));
items.push_back(3.14f);
for (const auto& item : items) {
if (item.type() == typeid(int)) {
std::cout << "any (int): " << std::any_cast<int>(item) << std::endl;
} else if (item.type() == typeid(std::string)) {
std::cout << "any (string): " << std::any_cast<const std::string&>(item) << std::endl;
} // ...
}
return 0;
}
类型擦除后的对象复制是个挑战,因为构造函数不能是虚的。
Clone
模式 (原型模式):在 ShapeConcept
中添加一个虚的 Clone()
方法,由 ShapeModel
实现具体的克隆逻辑。这是实现值语义拷贝的常用方式。
// 在ShapeConcept中:
// virtual std::unique_ptr<ShapeConcept> Clone() const = 0;
// 在ShapeModel中:
// std::unique_ptr<ShapeConcept> Clone() const override {
// return std::make_unique<ShapeModel<ConcreteShape>>(m_shape_object); // 依赖ConcreteShape的拷贝构造
// }
// 在ShapeWrapper中:
// ShapeWrapper(const ShapeWrapper& other) : m_pimpl(other.m_pimpl ? other.m_pimpl->Clone() : nullptr) {}
“如果它像鸭子一样走路,像鸭子一样嘎嘎叫,那它可能就是一只鸭子。” —— 鸭子类型关注对象的行为而非其继承关系。C++模板使得静态鸭子类型成为可能,结合类型擦除,可以实现运行时的外部多态。
类型擦除容器通常基于值语义设计,其拷贝、移动和构造的性能至关重要。
std::function
和 std::string
(在某些实现中) 都使用了此技术。template<typename T>
concept HasDrawMethod = requires(T t, const Context& ctx) {
{ t.Draw(ctx) } -> std::same_as<void>; // 假设Draw方法返回void
};
template<HasDrawMethod ShapeType> // 使用Concept约束
struct ShapeModel : public ShapeConcept {
// ...
void DoDraw(const Context& ctx) const override {
m_shape_object.Draw(ctx); // 直接调用,因为Concept保证了其存在
}
// ...
};
类型擦除是一种强大的设计技术,通过在适当的抽象层级隔离依赖关系,来应对软件设计中的变化。它允许我们创建灵活的、可扩展的系统,能够处理异构类型的对象集合,而无需强制它们继承自共同的基类。基于模板的外部多态是现代C++中实现类型擦除的常用且高效的方法,它能有效避免传统继承体系的弊端,如侵入性、接口膨胀和编译时依赖过重。结合小对象优化、概念等技术,可以构建出既灵活又高效的类型擦除解决方案。
在C++17之前,实例化一个类模板时,程序员通常需要显式指定所有的模板参数,即使这些参数可以从构造函数的参数中推断出来。例如:
std::pair<int, double> p(2, 4.5);
std::tuple<int, int, double> t(4, 3, 2.5);
这无疑增加了代码的冗余度。为了简化模板类的使用,C++17引入了类模板实参推导(Class Template Argument Deduction, CTAD)。CTAD允许编译器在特定上下文中,根据初始化器(initializer)的类型自动推导出缺失的模板实参。
CTAD主要在以下几种语境中生效:
当声明一个类模板类型的变量(可以带cv限定符)并进行初始化时,如果只提供了类模板名而没有模板实参列表,编译器会尝试推导。
std::pair p(2, 4.5); // 编译器推导出 std::pair<int, double> p(2, 4.5);
std::tuple t(4, 3, 2.5); // 类似于 auto t = std::make_tuple(4, 3, 2.5);
// 推导出 std::tuple<int, int, double>
std::less l; // 对于无参构造,如std::less,会推导为 std::less<void> l;
当使用new
表达式创建类模板对象时,如果类型只指定了模板名,编译器会进行推导。
template<class T>
struct A {
A(T, T);
};
auto y = new A{1, 2}; // 分配的类型被推导为 A<int>
在函数式风格的类型转换(也常用于对象构造)中,如果目标类型是类模板名,会触发CTAD。
std::mutex mtx;
auto lck = std::lock_guard(mtx); // 推导出 std::lock_guard<std::mutex>
std::vector<int> vi1 = {1,2,3}, vi2;
std::copy_n(vi1.begin(), 3,
std::back_insert_iterator(vi2)); // 推导出 std::back_insert_iterator<std::vector<int>>
// (T 是容器 vi2 的类型)
auto my_lambda = [&](int i) { /* ... */ };
std::for_each(vi1.begin(), vi1.end(),
Foo(my_lambda)); // 假设Foo是模板类,推导出 Foo<LambdaType>
// 其中 LambdaType 是lambda表达式的唯一类型
当一个类模板作为另一个模板的非类型模板参数的类型,并且其实参是一个常量表达式时,可以推导该类模板的模板参数。
template<class T>
struct X {
constexpr X(T) {}
};
template<X x_obj> // x_obj 是一个 X 类型的非类型模板参数
struct Y {};
Y<0> y; // C++20起,OK。推导出 X x_obj = X<int>(0),所以 Y 的模板实参是 X<int>(0)
// 实例化为 Y<X<int>(0)> y;
当编译器遇到一个只指定了类模板名 C
(没有模板实参列表)的场景时,它会构建一组**虚设的函数模板(fictitious function templates)**作为推导的候选集。这个过程分为几个步骤:
对于类模板 C
,编译器会根据其定义自动生成一些推导指引。
如果类模板 C
已经定义,并且声明了构造函数(或构造函数模板)Ci
,那么对于每一个 Ci
,都会生成一个对应的虚设函数模板 Fi
,其特征如下:
Fi
的模板参数列表是 C
的模板参数列表,其后跟随(如果 Ci
是构造函数模板)Ci
的模板参数列表。默认模板实参也会被包含。Fi
的约束是 C
的约束和 Ci
的约束的逻辑与(AND)。Fi
的函数参数列表与 Ci
的参数列表相同。Fi
的返回类型是 C
的模板名后跟由 <>
包围的 C
的模板参数(例如,C<T, U>
)。特殊情况:
C
未定义或未声明任何构造函数,会添加一个从假想的默认构造函数 C()
生成的虚设函数模板。C(C)
生成的虚设函数模板,称为复制推导候选 (copy deduction candidate)。如果类模板 C
满足聚合类型的要求(假设其任何待决基类没有虚函数或虚基类),并且没有用户定义的推导指引,且初始化是通过非空的初始化列表(如 C c{arg1, arg2, ..., argN};
,可使用指派初始化式)进行的,那么编译器可能会添加一个聚合推导候选。
这个候选的形参列表根据聚合体元素的类型和初始化器 argi
的形式来确定:
ei
是数组且 argi
是花括号列表,则对应形参 Ti
是到 ei
声明类型的右值引用。ei
是数组且 argi
是字符串字面量,则对应形参 Ti
是到 const
限定的 ei
声明类型的左值引用。Ti
是 ei
的声明类型。聚合推导候选是从假想的构造函数 C(T1, T2, ..., Tn)
生成的虚设函数模板。
示例:
// 隐式生成的指引示例 (基于构造函数)
template<class T>
struct UniquePtr {
UniquePtr(T* t);
};
UniquePtr dp{new auto(2.0)}; // 实际初始化表达式
// 编译器生成的虚设函数模板 (推导指引候选):
// F1: template<class T> UniquePtr<T> F(T* p); // 来自 UniquePtr(T*)
// F2: template<class T> UniquePtr<T> F(UniquePtr<T>); // 复制推导候选
// 编译器会像这样进行重载决议:
// struct X { // 假想类
// template<class T> X(T* p); // 对应 F1
// template<class T> X(UniquePtr<T>); // 对应 F2
// };
// X temp_obj{new auto(2.0)}; // 使用初始化器进行直接初始化
// 重载决议选择 F1 (当 T = double)。F1 的返回类型是 UniquePtr<double>。
// 最终结果:UniquePtr<double> dp{new auto(2.0)};
// C++20 聚合推导示例
template<class T>
struct AggPair {
T first;
int second;
};
AggPair ap = {3.14, 42}; // 推导出 AggPair<double>
// 聚合推导候选大致为: template<class T> AggPair<T> F(T, int);
除了编译器隐式生成的指引,开发者还可以提供自己的推导指引。这对于处理复杂情况(如从迭代器范围构造容器并推导元素类型)非常有用。
语法: 用户定义的推导指引看起来像一个带有尾随返回类型的函数声明,但函数名是类模板名。
非模板指引:
// explicit(可选) 模板名 ( 形参列表 ) -> 简单模板标识 requires子句(可选,C++20) ;
template<class T> struct MyVec { MyVec(size_t); /*...*/ };
MyVec(const char*) -> MyVec<std::string>; // 用户定义指引
模板指引:
// template <模板形参列表> requires子句(可选,C++20)
// explicit(可选) 模板名 ( 形参列表 ) -> 简单模板标识 requires子句(可选,C++20) ;
template<class Iter>
container(Iter b, Iter e) -> container<typename std::iterator_traits<Iter>::value_type>;
要点:
auto
),即不能是简写函数模板形式。explicit
构造函数,或者用户定义的推导指引本身被声明为 explicit
,那么在复制初始化的语境中(如 C c = {...};
),这个指引(或其对应的虚设构造函数)将不被考虑。一旦收集了所有隐式和用户定义的推导指引(表现为虚设函数模板或函数),编译器就会像对普通函数重载一样,使用提供的初始化器对这些指引进行模板实参推导和重载决议。
Fi
的返回类型就决定了最终推导出的类模板特化。重载决议的特殊规则:
std::tuple t1{1}; // 推导为 std::tuple<int>
std::tuple t2{t1}; // 推导为 std::tuple<int> (使用复制推导)
// 而不是 std::tuple<std::tuple<int>> (包装推导)
当使用别名模板且未提供模板实参列表时,CTAD也会发生。编译器会基于别名模板所指向的原始类模板的推导指引来生成一组新的指引,用于别名模板的推导。这个过程涉及到从原始指引的返回类型推导别名模板的参数,并进行替换和约束组合。
template<class T> class MyVector { /* ... */ };
template<class T> MyVector(T*, T*) -> MyVector<T>; // 指引 #1
template<class T> using VecAlias = MyVector<T>;
int arr[] = {1,2,3};
VecAlias va(arr, arr+3); // 推导 VecAlias<int>,进而 MyVector<int>
// 编译器会基于 MyVector 的指引 #1 为 VecAlias 生成一个类似的指引。
<...>
实参列表时触发。
std::tuple t1(1, 2, 3); // OK:推导
std::tuple<int, int, int> t2(1, 2, 3); // OK:显式指定
std::tuple<> t3(1, 2, 3); // 错误:不推导,tuple<>无匹配构造
X<T>
)。此时不发生CTAD。要触发CTAD,需要使用全局作用域解析 ::X
。T&&
,其中 T
是类模板自身的模板参数,那么这个 T&&
不是转发引用。但在用户定义的推导指引中,template<class T> C(T&&) -> C<...>;
里的 T&&
是转发引用。这会影响推导出的类型(值类型还是引用类型)。__cpp_deduction_guides
201703L
(C++17): 基本的类模板实参推导。201907L
(C++20): 增加了对聚合体和别名模板的CTAD支持。类模板实参推导(CTAD)是C++17引入的一项重要便利特性,它通过编译器的智能推导,显著减少了在实例化类模板时显式指定模板参数的需要,使得代码更加简洁易读。理解其工作机制,特别是隐式生成指引、用户定义指引以及重载决议规则,有助于我们更有效地利用这一特性,并编写出更优雅的现代C++代码。随着C++20对聚合体和别名模板推导的增强,CTAD的应用范围进一步扩大,成为现代C++编程中不可或缺的一部分。
自C++11起,标准引入了属性(Attributes) 的概念,允许开发者向编译器提供关于代码实体的额外信息,这些信息可能用于优化、警告、代码生成或其他特定行为。属性使用双方括号 [[...]]
的形式书写。本教程将梳理从C++11到C++23标准中引入的主要属性,并提供代码示例。
[[noreturn]]
的函数确实返回了,行为是未定义的。#include <iostream>
#include <stdexcept> // For std::terminate
[[noreturn]] void terminate_program(const std::string& message) {
std::cerr << "Error: " << message << std::endl;
std::exit(EXIT_FAILURE); // 或者 std::terminate();
}
int main() {
terminate_program("Fatal error occurred.");
// 编译器知道这里的代码不会被执行
std::cout << "This line will not be reached." << std::endl;
return 0;
}
std::memory_order_consume
内存序相关,但 memory_order_consume
的支持在实践中非常有限,并且在C++17中被临时劝阻使用。// (此示例仅为演示语法,实际应用需谨慎)
#include <atomic>
#include <thread>
std::atomic<int*> g_ptr;
int g_data;
void producer() {
int* p = new int(42);
g_data = 100;
g_ptr.store(p, std::memory_order_release);
}
// consume_data 期望通过 p 的依赖关系来看到 g_data 的正确值
// [[carries_dependency]] 告知编译器不要破坏这种依赖
void consume_data([[carries_dependency]] int* p) {
if (p) {
// 如果没有[[carries_dependency]]且编译器优化激进,
// 对g_data的读取可能先于p的依赖,导致读到旧值。
// 使用std::memory_order_consume和[[carries_dependency]]旨在避免这种情况。
// 但memory_order_consume本身支持度不高。
// (实际代码会更复杂,这里只是示意)
// int local_data = g_data;
}
}
void consumer() {
int* p;
while (!(p = g_ptr.load(std::memory_order_consume))) {
std::this_thread::yield();
}
consume_data(p);
// ...
}
std::memory_order_consume
的复杂性和实现挑战,C++标准委员会正在重新审视其规范。目前,更推荐使用 std::memory_order_acquire
。#include <iostream>
[[deprecated("Use NewFunction() instead.")]]
void OldFunction(int x) {
std::cout << "OldFunction called with: " << x << std::endl;
}
void NewFunction(int x) {
std::cout << "NewFunction called with: " << x << std::endl;
}
class [[deprecated("This class is outdated.")]] OldClass {
public:
int value;
};
int main() {
OldFunction(10); // 编译器通常会在此处发出警告
OldClass oc; // 编译器警告
NewFunction(20);
return 0;
}
switch
语句中,明确指示某个 case
标签后的代码执行会故意“贯穿”到下一个 case
标签,以抑制编译器可能因此发出的警告。switch
语句中的空语句(通常在 case
标签和下一个 case
或 default
之间)。#include <iostream>
void process_case(int val) {
switch (val) {
case 1:
std::cout << "Case 1 executed." << std::endl;
// 故意贯穿
[[fallthrough]];
case 2:
std::cout << "Case 2 executed (possibly due to fallthrough from 1)." << std::endl;
break;
case 3:
std::cout << "Case 3 executed." << std::endl;
// 没有 break 或 [[fallthrough]],编译器可能会警告
case 4:
std::cout << "Case 4 executed." << std::endl;
break;
default:
std::cout << "Default case." << std::endl;
}
}
int main() {
process_case(1);
std::cout << "---" << std::endl;
process_case(3);
return 0;
}
[[nodiscard]]
的函数的返回值,编译器通常会发出警告。可以提供一个可选的字符串字面量作为原因。#include <vector>
#include <iostream>
[[nodiscard]]
bool check_status() {
return true; // 假设这个状态很重要
}
[[nodiscard("Ignoring the error code can lead to issues.")]]
int get_error_code() {
return 0;
}
struct [[nodiscard]] ImportantHandle {
ImportantHandle(int id) : id_(id) {}
~ImportantHandle() { std::cout << "Handle " << id_ << " released." << std::endl; }
int id_;
};
ImportantHandle create_handle(int id) {
return ImportantHandle(id); // 构造函数返回类型是ImportantHandle,所以nodiscard生效
}
int main() {
check_status(); // 编译器通常会警告:忽略了[[nodiscard]]函数的返回值
int error = get_error_code(); // OK,返回值被使用
get_error_code(); // 编译器警告,并可能显示原因
create_handle(1); // 编译器警告:忽略了nodiscard类型的返回值
ImportantHandle h = create_handle(2); // OK
return 0;
}
#include <iostream>
void process_data(int data, [[maybe_unused]] bool debug_flag) {
// 在非调试构建中,debug_flag 可能不被使用
#ifdef ENABLE_DEBUG
if (debug_flag) {
std::cout << "Debug: Processing " << data << std::endl;
}
#endif
// ... 正常处理 data ...
}
int main() {
[[maybe_unused]] int unused_variable = 10; // 如果此变量后续未被使用,不会产生警告
process_data(100, true);
return 0;
}
[[likely]]
用于标记 if
或 switch
语句的某个分支(通常是 if (condition) [[likely]] { ... }
或 case label [[likely]]:
)更有可能被执行,而 [[unlikely]]
则标记不太可能被执行的分支。编译器可以利用这些信息优化代码布局,以改善指令缓存和分支预测的性能。if
和 switch
语句的条件部分,或者 case
标签)。#include <iostream>
void process_input(int input) {
if (input > 0) [[likely]] {
// 假设正数输入是常见情况
std::cout << "Processing positive input." << std::endl;
} else if (input == 0) {
std::cout << "Processing zero input." << std::endl;
} else [[unlikely]] {
// 假设负数输入是罕见情况
std::cout << "Processing negative input (error?)." << std::endl;
}
}
int main() {
process_input(10);
process_input(-5);
return 0;
}
#include <iostream>
struct Empty {}; // 空类
struct MyAllocator { // 另一个空类,可能用于元编程
void* allocate(size_t s) { return malloc(s); }
void deallocate(void* p) { free(p); }
};
template<typename T, typename Allocator = MyAllocator>
class MyVector {
T* data_;
size_t size_;
[[no_unique_address]] Allocator alloc_; // 如果Allocator是空类,alloc_可能不占空间
public:
MyVector() : data_(nullptr), size_(0), alloc_() {}
// ... 其他成员 ...
};
int main() {
MyVector<int> vec_default_alloc;
MyVector<int, Empty> vec_empty_alloc; // Empty是空类
// sizeof(vec_empty_alloc) 理论上可能等于 sizeof(int*) + sizeof(size_t)
// 而不是 sizeof(int*) + sizeof(size_t) + sizeof(Empty) (如果Empty占1字节)
std::cout << "sizeof(MyVector<int>): " << sizeof(vec_default_alloc) << std::endl;
std::cout << "sizeof(MyVector<int, Empty>): " << sizeof(vec_empty_alloc) << std::endl;
return 0;
}
C++23 标准继续扩展属性列表,但截至我知识更新时(2023年初),一些提案仍在最终确定阶段或刚被接纳,编译器支持可能尚不普遍。以下是一些被讨论或已采纳的:
expression
在该点始终为真。编译器可以使用这个信息进行更激进的优化,例如删除被认为不可能执行到的代码路径。如果运行时 expression
实际为假,则行为是未定义的。void process_array(int* arr, size_t size) {
if (size > 0) {
[[assume(arr != nullptr)]]; // 假设如果size > 0,则arr一定不是nullptr
for (size_t i = 0; i < size; ++i) {
arr[i] *= 2; // 编译器可能基于arr != nullptr优化这里的解引用
}
}
}
[[assume]]
可能导致难以调试的错误,因为它允许编译器基于可能不成立的假设进行优化。C++标准是一个持续演进的过程,新的属性提案会不断出现。例如,可能会有更多与契约式编程(Contracts)、反射、并发或特定硬件优化相关的属性。建议查阅最新的C++标准文档和编译器文档以获取最准确和最全面的信息。
std::
)。但允许实现定义带有命名空间的属性(例如 [[gnu::unused]]
)。C++属性提供了一种标准化的方式来向编译器传递元信息,以期改善代码质量、性能或开发体验。从C++11到C++23,属性的集合不断丰富,涵盖了从函数行为、弃用标记、分支预测到内存布局优化等多个方面。开发者应了解并适当使用这些属性,但也要注意它们并非万能药,特别是像 [[likely]]
/[[unlikely]]
和 [[assume]]
这样的优化提示,需要谨慎评估其效果。始终参考最新的C++标准和编译器文档是获取最准确信息的最佳途径。
奇异递归模板模式(Curiously Recurring Template Pattern, CRTP)是C++中一种著名且独特的惯用法。其核心思想在于基类是一个模板类,而这个模板类的模板参数恰好是继承自它的派生类本身。
让我们通过一个简单的代码示例来直观感受CRTP的结构:
// 基类模板
template <typename DerivedType>
class Base {
public:
void CommonOperation() {
// 将this指针安全地转换为派生类类型的引用或指针
DerivedType& derived = static_cast<DerivedType&>(*this);
// 或者使用指针:
// DerivedType* derived_ptr = static_cast<DerivedType*>(this);
// 调用派生类特有的方法
derived.SpecificImplementation();
}
};
// 派生类,将自身作为模板参数传递给基类
class Derived : public Base<Derived> {
public:
void SpecificImplementation() {
// ... 派生类特有的实现 ...
// std::cout << "Derived::SpecificImplementation called" << std::endl;
}
};
CRTP模式的关键特征:
Base
接受一个模板参数 DerivedType
。Derived
在继承 Base
时,将 Derived
自身作为模板参数 DerivedType
传递给 Base
,即 class Derived : public Base<Derived>
。Base
的成员函数(如 CommonOperation
)中,通过 static_cast
将 this
指针(或引用)转换为 DerivedType*
(或 DerivedType&
),从而能够调用派生类 DerivedType
中定义的成员函数(如 SpecificImplementation
)。CRTP的目的:
CRTP的主要目的是允许基类在编译期访问和使用其派生类的成员(通常是方法或类型别名),而无需依赖虚函数和动态多态。这使得基类可以提供通用的框架或行为,而将具体的实现细节委托给派生类。
CRTP的本质——“基类定义接口/框架,派生类提供具体实现”——使其在多种场景下都非常有用,主要优势在于静态多态带来的性能提升和代码复用的便利性。
传统的面向对象设计通过虚函数实现多态,这涉及到运行时的虚函数表查找(vtbl dispatch),会带来一定的性能开销。CRTP提供了一种实现多态行为的方式,但绑定在编译期完成,因此称为静态多态。
在CRTP中,基类的方法可以看作是“接口”定义,而派生类通过提供特定名称和签名的方法来实现这些“接口”。
示例:通用的 Writer
接口
template<typename ConcreteWriter>
class Writer {
public:
bool Write(int pos, void* data, int size) {
// 将this转换为具体的Writer类型,并调用其实现方法
ConcreteWriter* concrete_writer = static_cast<ConcreteWriter*>(this);
return concrete_writer->WriteImpl(pos, data, size);
}
};
class FileWriter : public Writer<FileWriter> {
public:
FileWriter(const std::string& filename) { /* ... 打开文件 ... */ }
private:
// 将WriteImpl声明为私有,并通过友元暴露给基类
// 这样可以防止外部直接调用WriteImpl,保持接口的一致性
friend class Writer<FileWriter>;
bool WriteImpl(int pos, void* data, int size) {
// ... 实际写入文件的逻辑 ...
// return true;
}
};
class SocketWriter : public Writer<SocketWriter> {
public:
SocketWriter(const std::string& ip, int port) { /* ... 连接socket ... */ }
private:
friend class Writer<SocketWriter>;
bool WriteImpl(int pos, void* data, int size) {
// ... 实际写入socket的逻辑 ...
// return true;
}
};
在这个例子中,Writer::Write
方法定义了一个稳定的接口,具体的写操作由派生类的 WriteImpl
实现。由于 static_cast
和模板的特性,调用在编译期就已经解析完成,没有虚函数开销。
与动态多态的对比:
CRTP实现的静态多态效率更高,但它不能像传统的动态多态那样轻易地将不同派生类对象存储在同一个基类指针容器中(例如 std::vector<Writer*>
). 因为 Writer<FileWriter>
和 Writer<SocketWriter>
是完全不同的类型。虽然可以通过引入共同的非模板抽象基类或使用 std::any
等技术来模拟异构容器,但这些方案通常不如动态多态直接。选择哪种多态方式取决于具体需求。
CRTP允许在基类中添加新的通用功能,这些功能可以利用派生类提供的基础操作。当基类增加新功能时,所有派生类自动“获得”这些新功能,而无需修改派生类代码。
示例:数值算法扩展
假设我们有一些基础算术操作类,希望在其上构建更高级的算法。
// 基类模板,提供高阶算法框架
template<typename ConcreteAlgorithm, typename NumericType>
class HighLevelAlgo {
public:
// 高阶功能:求和 (示例用变参模板,实际可能不同)
template<typename... Args>
NumericType Sum(NumericType first, Args... rest) {
ConcreteAlgorithm* concrete_algo = static_cast<ConcreteAlgorithm*>(this);
NumericType current_sum = first;
// 伪代码,实际需要递归或折叠表达式
// ((current_sum = concrete_algo->Add(current_sum, rest)), ...);
// 简化版:假设派生类有Add(NumericType, NumericType)
NumericType temp_args[] = {rest...};
for(const auto& arg : temp_args) {
current_sum = concrete_algo->Add(current_sum, arg);
}
return current_sum;
}
// 高阶功能:平方
NumericType Square(const NumericType& val) {
ConcreteAlgorithm* concrete_algo = static_cast<ConcreteAlgorithm*>(this);
return concrete_algo->Multiply(val, val); // 依赖派生类的Multiply
}
};
// 具体算法实现:整数运算
class IntAlgo : public HighLevelAlgo<IntAlgo, int> {
public:
int Add(int a, int b) { return a + b; }
int Multiply(int a, int b) { return a * b; }
// ... 其他基础运算:Subtract, Divide ...
};
// 具体算法实现:复数运算 (假设Complex类已定义)
class ComplexAlgo : public HighLevelAlgo<ComplexAlgo, Complex> {
public:
Complex Add(const Complex& a, const Complex& b) { return a + b; }
Complex Multiply(const Complex& a, const Complex& b) { return a * b; }
// ...
};
如果需要在 HighLevelAlgo
中增加一个新的高阶运算(如求模 Modulo
),只要这个运算能基于派生类已提供的基础操作(如加、减、乘、除)实现,那么所有派生类(IntAlgo
, ComplexAlgo
等)将自动拥有 Modulo
功能,无需任何修改。
设计模式中的“模板方法模式”定义了一个操作中的算法骨架,而将一些步骤延迟到子类中。子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。CRTP可以用于实现此模式的静态版本。
template<typename ConcreteWorker>
class WorkerFramework {
public:
void DoProcess() {
ConcreteWorker* concrete_worker = static_cast<ConcreteWorker*>(this);
concrete_worker->Step1_Initialize();
concrete_worker->Step2_ExecuteCoreLogic();
concrete_worker->Step3_Finalize();
}
};
class StrongWorker : public WorkerFramework<StrongWorker> {
public:
void Step1_Initialize() { /* ... StrongWorker的初始化 ... */ }
void Step2_ExecuteCoreLogic() { /* ... StrongWorker的核心逻辑 ... */ }
void Step3_Finalize() { /* ... StrongWorker的清理 ... */ }
};
class SmartWorker : public WorkerFramework<SmartWorker> {
public:
void Step1_Initialize() { /* ... SmartWorker的初始化 ... */ }
void Step2_ExecuteCoreLogic() { /* ... SmartWorker的核心逻辑 ... */ }
void Step3_Finalize() { /* ... SmartWorker的清理 ... */ }
};
WorkerFramework::DoProcess
定义了处理流程的骨架,而具体的步骤由 StrongWorker
和 SmartWorker
实现。同样,这里没有虚函数调用。
在传统的基于虚函数的继承体系中,复制一个通过基类指针指向的对象(即创建其实际派生类型的副本)是一个常见问题。通常的解决方案是在基类中声明一个纯虚的 clone()
方法,并由每个派生类重写。
class Shape {
public:
virtual ~Shape() = default;
virtual std::unique_ptr<Shape> clone() const = 0;
};
class Triangle : public Shape {
public:
std::unique_ptr<Shape> clone() const override {
return std::make_unique<Triangle>(*this); // 调用Triangle的拷贝构造
}
};
这种方法要求每个派生类都重复实现类似的 clone()
逻辑。CRTP可以帮助消除这种重复代码。
// 抽象基类 (可选,用于异构容器)
class AbstractShape {
public:
virtual ~AbstractShape() = default;
virtual std::unique_ptr<AbstractShape> clone() const = 0;
};
// CRTP基类,提供通用的clone实现
template <typename DerivedShapeType>
class ClonableShape : public AbstractShape { // 可选继承AbstractShape
public:
std::unique_ptr<AbstractShape> clone() const override {
// static_cast到派生类类型,然后调用派生类的拷贝构造函数
return std::make_unique<DerivedShapeType>(static_cast<const DerivedShapeType&>(*this));
}
protected:
// 防止直接实例化ClonableShape,并允许派生类构造
ClonableShape() = default;
ClonableShape(const ClonableShape&) = default;
ClonableShape(ClonableShape&&) = default; // C++11起通常也应提供移动
ClonableShape& operator=(const ClonableShape&) = default;
ClonableShape& operator=(ClonableShape&&) = default;
};
// 具体形状类,只需继承并传入自身类型
class Triangle : public ClonableShape<Triangle> { /* ... Triangle特有成员 ... */ };
class Rectangle : public ClonableShape<Rectangle> { /* ... */ };
class Circle : public ClonableShape<Circle> { /* ... */ };
通过这种方式,clone()
的实现被集中在 ClonableShape
基类中,派生类只需继承即可获得多态复制能力。protected
构造函数可以防止用户意外地创建 ClonableShape<SomeUnrelatedType>
实例。
如果不需要将不同形状存储在 AbstractShape*
的容器中,AbstractShape
基类可以省略,ClonableShape::clone()
的返回类型可以直接是 std::unique_ptr<DerivedShapeType>
。
静态成员变量常用于实现对象计数,但传统的单一计数器类无法为不同类型的对象分别计数。CRTP可以为每种类型生成一个独立的计数器。
template<typename CountedType>
class ObjectCounter {
public:
ObjectCounter() { ++count; }
ObjectCounter(const ObjectCounter&) { ++count; } // 拷贝也计数
~ObjectCounter() { --count; }
static size_t HowMany() { return count; }
private:
static size_t count; // 每个CountedType特化都有自己的count
};
// 静态成员定义
template<typename CountedType>
size_t ObjectCounter<CountedType>::count = 0;
// 使用CRTP进行计数
class Widget : private ObjectCounter<Widget> { // 私有继承,避免不当使用
public:
using ObjectCounter<Widget>::HowMany; // 将计数方法暴露出来
// ... Widget的其他成员 ...
};
class Gadget : private ObjectCounter<Gadget> {
public:
using ObjectCounter<Gadget>::HowMany;
// ...
};
int main() {
Widget w1, w2;
Gadget g1;
std::cout << "Widgets: " << Widget::HowMany() << std::endl; // 输出 2
std::cout << "Gadgets: " << Gadget::HowMany() << std::endl; // 输出 1
return 0;
}
使用私有继承是为了防止通过基类指针 ObjectCounter<Widget>*
意外地 delete
一个 Widget
对象(因为 ObjectCounter
的析构函数非虚),这会导致未定义行为。私有继承切断了 "is-a" 关系,使得这种指针转换非法。如果采用组合方式,C++20的 [[no_unique_address]]
属性可以帮助实现空基类优化(EBO)的效果,即使 ObjectCounter
作为成员变量。
CRTP的核心在于派生类将自身作为模板参数传递给基类。如果传递了错误的类型,可能会导致编译错误或难以察觉的运行时逻辑错误。
template <typename T> class Base { /* ... */ };
class Derived1 : public Base<Derived1> { /* 正确 */ };
// class Derived2 : public Base<Derived1> { /* 错误!Derived2的行为会像Derived1 */ };
规避方法:将基类的构造函数声明为 private
或 protected
,然后将模板参数 T
(即派生类)声明为基类的 friend
。
template <typename T>
class Base {
public:
// ... 公共接口 ...
protected: // 或 private
Base() = default; // 允许派生类调用
friend T; // 允许T(即正确的派生类)访问构造函数
};
class DerivedCorrect : public Base<DerivedCorrect> {}; // OK
// class DerivedWrong : public Base<DerivedCorrect> {};
// 编译错误: DerivedWrong 不是 Base<DerivedCorrect> 的友元,无法访问其构造函数
如果派生类中定义了一个与基类中非虚方法同名的方法,即使参数列表不同,派生类的方法也会隐藏(或称覆盖,尽管术语上更精确的是隐藏,因为基类方法非虚)基类的同名方法。这可能导致在CRTP基类中通过 static_cast
调用派生类方法时,实际调用的不是预期的版本。
template<typename T>
struct BaseWithFunc {
void Process() {
T* sub = static_cast<T*>(this);
// 期望调用 BaseWithFunc::Func(int, double) 或 T::Func(int, double)
sub->Func(6, 6.8);
}
void Func(int a, double b) { std::cout << "Base::Func(int, double)" << std::endl; }
};
struct DerivedWithHiding : public BaseWithFunc<DerivedWithHiding> {
// 这个Func隐藏了BaseWithFunc::Func(int, double)
void Func(int a, int b) { std::cout << "Derived::Func(int, int)" << std::endl; }
};
int main() {
DerivedWithHiding d;
d.Process(); // 调用会尝试匹配 DerivedWithHiding::Func(int, int)
// 如果参数不匹配,会导致编译错误,或者如果存在隐式转换则可能调用不期望的版本
}
规避方法:
using Base<DerivedType>::FuncName;
来将基类方法引入派生类的作用域,从而参与重载决议。CRTP是一种强大而灵活的C++技术,它通过一种“奇异的”递归模板继承结构,实现了编译期的静态多态和代码复用。它避免了虚函数的运行时开销,允许基类访问派生类的具体实现,并且是非侵入式的。然而,正确使用CRTP需要对模板和继承机制有深入的理解,并注意其潜在的陷阱,如模板参数的正确传递和方法隐藏问题。在合适的场景下,CRTP是实现高性能、高复用性代码的有效工具。
在现代 C++(C++11 及以上)中,应尽量避免使用 #define
宏来定义常量、函数或逻辑片段,因为宏是预处理器层面的文本替换,不受类型系统、作用域规则约束,容易引发错误。以下是推荐的替代方案及理由。
旧式宏用法 | 推荐替代 | 原因与说明 |
---|---|---|
#define PI 3.14159 |
constexpr double PI = 3.14159; |
类型安全、作用域明确、可调试、支持模板 |
#define SIZE 1024 |
inline constexpr std::size_t SIZE = 1024; |
防止重复定义冲突(ODR),支持头文件中使用 |
#define MAX(a,b) ((a)>(b)?(a):(b)) |
template<typename T> constexpr T max(T a, T b) |
避免副作用,支持类型推导,避免重复求值 |
#define SQUARE(x) ((x)*(x)) |
constexpr int square(int x) |
表达清晰,支持 constexpr 计算,避免副作用 |
#define VERSION "1.2.3" |
constexpr const char* VERSION = "1.2.3"; |
类型安全,易读,支持模板参数 |
#define LOG(x) std::cout << x << std::endl |
inline void log(auto x) |
类型推导安全,调试友好,可被 IDE 补全 |
#define FLAG_A 1 #define FLAG_B 2 |
enum class Flag { A = 1, B = 2 } |
命名空间封装,避免值冲突,强类型控制 |
#ifndef HEADER_H #define HEADER_H |
#pragma once |
编译器支持更好,减少宏污染,写法更简洁 |
场景 | 示例 | 原因 |
---|---|---|
编译开关控制 | #ifdef DEBUG / #if PLATFORM == WINDOWS |
constexpr 发生在编译阶段之后,无法控制是否参与编译 |
平台相关代码分支 | #ifdef _WIN32 |
C++ 语言本身无法识别平台宏,必须依赖预处理器 |
条件包含头文件 | #ifdef USE_MYLIB\n #include "mylib.h" |
语言层级无法控制是否包含头文件 |
C++ 版本特性适配 | #if __cplusplus >= 202002L |
识别语言版本必须用宏 |
兼容 C / 外部编译器行为 | #define EXPORT __declspec(dllexport) |
外部接口约定通常基于宏 |
技术 | 用途 | 替代宏的能力 |
---|---|---|
constexpr / const |
常量定义 | 值替换 |
inline |
函数替换 | 替代函数宏 |
模板函数 / 泛型函数 | 类型敏感的计算 | 避免类型错误 |
enum class |
标志常量 | 类型安全,作用域封装 |
if constexpr |
模板条件选择 | 替代部分条件逻辑(非编译开关) |
#pragma once |
防止重复包含 | 替代 include guard 宏(非标准但主流支持) |
constexpr
/ inline constexpr
定义常量enum class
替代裸整数标志#pragma once
替代传统 include guard#define
和 #ifdef
// config.hpp
#pragma once
inline constexpr int BUFFER_SIZE = 1024;
inline void log(auto&& msg) {
std::cout << "[LOG] " << msg << std::endl;
}
enum class Mode : int {
Read = 1,
Write = 2,
Exec = 4,
};
返回顶部