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[] 可 constexprstatic 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_typehandle_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++中实现类型擦除的常用且高效的方法,它能有效避免传统继承体系的弊端,如侵入性、接口膨胀和编译时依赖过重。结合小对象优化、概念等技术,可以构建出既灵活又高效的类型擦除解决方案。
返回顶部