Skip to content

Instantly share code, notes, and snippets.

@KuRRe8
Last active May 16, 2025 15:15
Show Gist options
  • Save KuRRe8/bdf62eb11a81ddc37010072fe6bacee8 to your computer and use it in GitHub Desktop.
Save KuRRe8/bdf62eb11a81ddc37010072fe6bacee8 to your computer and use it in GitHub Desktop.
现代C++的一些新特性,以17、20、23版本为例

现代C++

C++ 11/14作为一个奠基版本,构造了近年来编写C++的新范式。

本人熟悉的主要语言技术栈有C/C++, Python, Matlab, C#, 相比之下,C++的变化是最频繁的,也是最有趣的

多数人已然熟悉C++11/14的用法,本Gist仓库旨在总结一些17及以后版本的特性。

欢迎在讨论区发表相应见解。

C++17 语言与标准库特性概览


一、语言核心特性

1. 结构化绑定(Structured Bindings)

引入语法糖,解构类型为 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)

2. if / switch 初始化语句

允许在条件判断前进行局部变量定义:

if (int x = f(); x > 0) {
    // 使用 x
}

用于限制作用域并提升代码清晰性。

3. 折叠表达式(Fold Expressions)

针对可变参数模板提供简洁聚合运算方式。

template<typename... Args>
auto sum(Args... args) {
    return (... + args); // 从左到右相加
}

形式支持:

  • (... op pack)
  • (pack op ...)
  • (pack op ... op init)

4. constexpr 语义增强

在 constexpr 函数体中允许使用:

  • 条件语句(if、switch)
  • 循环(for、while)
  • 局部变量定义
constexpr int factorial(int n) {
    int result = 1;
    for (int i = 1; i <= n; ++i) result *= i;
    return result;
}

5. 内联变量(inline variables)

用于头文件中全局 constexpr 定义,避免多重定义。

inline constexpr int version = 1;

6. [[nodiscard]] 属性

用于警告被忽略的返回值(避免遗漏重要调用结果)。

[[nodiscard]] int compute() { return 42; }

compute(); // 可能被编译器警告

7. constexpr if 条件分支

用于模板上下文下的条件编译:

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";
}

二、标准库增强特性

1. std::optional

可选值容器,可能有值也可能无值。

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;
};

2. std::variant

类型安全的联合体,支持多种类型之一。

std::variant<int, std::string> v = "text";
v = 10;

std::visit([](auto&& x) { std::cout << x; }, v);

使用索引或 std::get_if<T> 来访问成员。

3. std::any

可存放任意类型对象(运行时类型擦除)。

std::any a = 1;
a = std::string("test");

if (a.type() == typeid(std::string))
    std::cout << std::any_cast<std::string>(a);

本质上封装了类型信息和析构函数指针。

4. std::string_view

非拥有、只读的字符串视图(避免复制)。

void log(std::string_view msg) {
    std::cout << msg;
}

log("hello"); // 允许字面量

std::string 不兼容构造(不拥有内存)。

5. std::filesystem

跨平台文件与路径处理 API。

#include <filesystem>
namespace fs = std::filesystem;

if (fs::exists("data.txt")) {
    auto size = fs::file_size("data.txt");
}

常用操作:

  • 路径拼接:path / "file"
  • 遍历目录:directory_iterator

6. 并发相关改进

scoped_lock

用于一次加锁多个 mutex,避免死锁。

std::scoped_lock lock(m1, m2);  // 原子加锁

shared_mutex

提供多个读者 / 单个写者访问:

std::shared_mutex mutex;
{
    std::shared_lock lock(mutex);  // 多读
}
{
    std::unique_lock lock(mutex);  // 独写
}

三、实用工具类与函数

std::invoke

统一调用普通函数、成员函数、函数对象等:

std::invoke(f, args...);

std::apply

将 tuple 参数展开用于函数调用:

auto args = std::make_tuple(1, 2);
auto result = std::apply([](int a, int b) { return a + b; }, args);

四、总结

C++17 是一个“可用性增强”版本,重点改进包括:

  • 更简洁的语法(结构化绑定、折叠表达式)
  • 更强的模板条件表达能力(if constexpr)
  • 更高效的值类型容器(optional、variant)
  • 非拥有引用视图(string_view)
  • 正式的文件系统支持(filesystem)
  • 并发控制更安全(scoped_lock, shared_mutex)

相比 C++11/14,C++17 在实践中大幅降低了模板复杂度和常见代码样板,是现代 C++ 编程的推荐入门版本之一。

C++20 语言与标准库特性概览


一、语言核心特性

1. Concepts(概念)

对模板参数施加约束,提高模板可读性和错误提示质量。

template<typename T>
concept Number = std::is_arithmetic_v<T>;

template<Number T>
T add(T a, T b) { return a + b; }

2. 三向比较 <=>(Spaceship Operator)

统一实现 <, ==, > 等比较操作。

#include <compare>

struct Point {
    int x, y;
    auto operator<=>(const Point&) const = default;
};

3. Range-based 操作库(Ranges)

views:: 管道操作表达序列变换:

#include <ranges>

for (int i : std::views::iota(1, 10) | std::views::filter([](int x){ return x % 2 == 0; }))
    std::cout << i << " ";

4. 协程(Coroutines)

通过 co_await, co_yield, co_return 实现非阻塞协程。

task<int> foo() {
    co_return 42;
}

(完整协程需要协程 promise 和协程句柄封装)

5. 模块化(Modules)

替代头文件的模块系统(编译器支持有限)。

// math.ixx
export module math;
export int square(int x) { return x * x; }

使用:

import math;
int y = square(5);

6. constevalconstinit

consteval:必须在编译期求值

consteval int cube(int x) { return x * x * x; }

constinit:避免静态初始化顺序问题

constinit int global = 42;

7. 模板参数自动类型推导增强

支持 template<auto> 等:

template<auto N>
void printNTimes() { ... }

二、标准库增强特性

1. std::span

轻量非拥有数组视图,不拷贝数据。

void print(std::span<int> s) {
    for (int i : s) std::cout << i;
}

2. std::format

类似 Python f-string 的格式化输出:

#include <format>
std::cout << std::format("value = {}", 42);

3. std::ranges 模块

范围处理功能分为:

  • views::(惰性生成、变换)
  • actions::(修改容器)
  • ranges::(通用接口)
auto evens = vec | std::views::filter([](int x){ return x % 2 == 0; });

4. std::concepts(标准概念)

如:

  • std::same_as<T, U>
  • std::convertible_to<T, U>
  • std::invocable<F, Args...>

示例:

template<std::integral T>
void f(T x);

5. std::bit_cast

类型安全的强制转换(要求类型大小相同):

float f = 3.14f;
uint32_t u = std::bit_cast<uint32_t>(f);

6. std::jthread

自动 join 的线程:

std::jthread t([] { do_work(); });  // 析构时自动 join

7. std::stop_token / stop_source

用于线程取消协作机制:

void run(std::stop_token st) {
    while (!st.stop_requested()) { ... }
}

8. std::move_only_function

仅可移动的泛型可调用封装器:

std::move_only_function<void()> f = [] { ... };

三、总结

C++20 是继 C++11 之后又一次大的语言升级,核心目标包括:

  • 提升模板约束与泛型表达力(Concepts)
  • 引入现代并发与异步控制(Coroutines、jthread、stop_token)
  • 引入模块系统,重构头文件机制(Modules)
  • 扩展标准库以支持更现代的开发风格(ranges、format、span)
  • 提供更强的编译期工具(consteval、constinit、bit_cast)

C++20 被广泛认为是现代 C++ 成熟阶段的重要标志。

C++23 语言与标准库特性概览


一、语言核心特性

1. 显式 this 参数

允许将 this 作为显式形参,支持更灵活的成员函数调用。

struct S {
    void func(this S& self, int x) {
        self.value = x;
    }
    int value;
};

2. 多维下标运算符

用户自定义类型支持 operator[](...) 形式的多维下标。

struct Matrix {
    int operator[](size_t i, size_t j) const;
};

3. if consteval

用于判断当前是否处于 consteval 上下文。

consteval int always_constexpr() { return 1; }

constexpr int f() {
    if consteval {
        return always_constexpr();
    } else {
        return 0;
    }
}

4. 模板函数形参列表中的别名声明

可直接引入类型别名:

template<typename T>
void f(alias A = typename T::value_type);

5. lambda 表达式支持属性与显式模板

auto l = []<typename T>(T x) [[nodiscard]] { return x + 1; };

6. 默认构造函数推导改进(CTAD)

类模板参数推导支持更多构造情况,提升推导准确性。

7. 推导指南中支持 requires

template<typename T>
requires std::integral<T>
struct X { T value; };

X x = X{42};  // 仅当 T 满足 integral

二、标准库增强特性

1. std::expected<T, E>

用于替代 std::optional 表达错误信息:

std::expected<int, std::string> parse(std::string_view s);

if (res) {
    int val = *res;
} else {
    std::cerr << res.error();
}

2. std::print, std::println

std::format 结合的便捷输出:

std::print("value: {}
", 42);
std::println("hello {}", "world");

3. std::generator

轻量协程生成器:

std::generator<int> gen() {
    for (int i = 0; i < 3; ++i)
        co_yield i;
}

4. std::move_only_function

泛型回调对象,移动语义支持:

std::move_only_function<void()> f = [] { do_something(); };

5. std::flat_map, std::flat_set(尚未进入主线实现,但已提案接受)

排序向量构造的 map/set,插入慢、查找快、占用小。

6. std::stacktrace

用于捕获运行时调用栈信息:

auto st = std::stacktrace::current();
std::cout << st;

三、语言细节改进

  • constexpr 支持更多 STL 类型(如 std::vector 部分操作)
  • operator[]constexpr
  • static operator() / static [](实验性提案)
  • UTF-8 字符串视为 portable 源代码字符集

四、总结

C++23 是对 C++20 的迭代补强版本,主要特点是:

  • 拓展已有语言特性表达力(lambda、requires、模板)
  • 提供更强的工具类型支持(expected、generator)
  • 增强标准库的诊断与调试能力(stacktrace)
  • 强化 constexpr 在语言与标准库中的一致性

C++23 兼容性强,适用于需要现代语法同时强调编译期控制与运行期效率的项目。

C++20 协程与可等待对象


一、协程基本概念

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_alwaysstd::suspend_never 以控制。


四、可等待对象(Awaitable)

满足以下接口即可被 co_await

  • await_ready() → bool:是否立即完成(返回 true 则不挂起)
  • await_suspend(std::coroutine_handle<>):挂起行为,传入当前协程句柄
  • await_resume():恢复后返回值

示例:自定义 awaitable

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 << "
";
}

五、生成器示例(使用 co_yield)

#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::jthread + 协程挂起

结合 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
";
}

七、标准库协程类介绍(C++23 起)

  • std::generator<T>:标准生成器
  • std::task<T>(未来 TS):表示可等待任务
  • std::sync_wait, std::when_all 等协程组合工具(需配合执行器实现)

八、总结

C++ 协程机制高度底层,编译器将其转换为状态机,通过 promise_typecoroutine_handle 控制生命周期。

使用协程应理解:

  • 协程自身是惰性的(不会自动运行)
  • co_await 本质是协程挂起点,依赖被等待对象是否决定挂起
  • 可构建生成器、异步调度器、future 框架等多种抽象

建议实践中使用封装好的框架如 cppcorolibunifex,以避免手动实现完整的状态管理。

C++23新特性:智能指针与C风格出参的优雅交互 - out_ptrinout_ptr详解

前言

在C++编程中,智能指针(如 std::unique_ptrstd::shared_ptr)是管理动态内存、防止资源泄漏的重要工具。然而,当C++代码需要与传统的C风格API或一些期望通过输出参数(通常是 T**T*&)来分配或修改指针的库交互时,智能指针的非侵入式特性会带来一些不便和潜在风险。C++23引入的 std::out_ptrstd::inout_ptr 适配器旨在优雅地解决这一问题。

1. 问题的提出:智能指针与C风格出参的矛盾

1.1 C风格接口的挑战

当我们使用智能指针时,获取其管理的裸指针通常通过 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 接管,从而导致泄漏。

1.2 传统的代理类方案及其局限性

为了解决上述问题,一种常见的模式是使用一个临时的代理(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 块永远不会执行。

2. C++23 智能指针适配器

C++23标准库提供了官方的解决方案:std::out_ptr_tstd::inout_ptr_t 适配器类,以及对应的辅助函数 std::out_ptrstd::inout_ptr

2.1 std::out_ptr_tstd::inout_ptr_t (适配器类)

  • 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
        }
    }

2.2 辅助函数 std::out_ptrstd::inout_ptr

为了简化 std::out_ptr_tstd::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) { /* ... */ }
}

2.3 使用注意事项

  1. 临时对象的生命周期: 与传统的代理类方案类似,std::out_ptrstd::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已被正确更新
        // ...
    }
  2. 避免延长临时适配器对象的生命周期: 不要使用 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才被更新
  3. std::inout_ptr_tstd::shared_ptrstd::inout_ptr_t 的行为是先释放智能指针原有的所有权,然后用C API返回的指针重新初始化。这种操作模式与 std::shared_ptr 的共享所有权语义不兼容,因此 std::inout_ptr_t (及 std::inout_ptr) 不能用于 std::shared_ptrstd::out_ptr_t 可以用于空的 std::shared_ptr

  4. C API的指针处理语义: 使用这些适配器前,必须清楚C API对于传入的 T**T*& 参数是如何操作的:

    • 对于 out_ptr:API是否总是分配新内存?API是否会处理(如 delete)原先通过 T** 传入的非空指针?标准 out_ptr 的行为是先 reset() 智能指针,所以如果C API不处理传入的指针,而智能指针原来管理着一个对象,该对象会被释放。
    • 对于 inout_ptr:API是否会 deletefree 传入的指针所指向的对象?如果API不释放,适配器在 release() 后将所有权交给临时裸指针,C API操作后,适配器析构时会用新指针 reset 智能指针。如果C API没有释放原对象且也没有返回新对象(而是修改了原对象),则需要确保所有权被正确传递。

3. 核心优势与总结

std::out_ptrstd::inout_ptr 适配器的引入,为C++开发者带来了诸多好处:

  • 安全性增强:通过RAII机制,确保从C API获取的资源能够被智能指针正确接管,极大地降低了因异常或编码疏忽导致的资源泄漏风险。
  • 代码简洁性:相比手动管理裸指针或编写自定义代理类,使用标准库提供的适配器使得与C风格API交互的代码更加简洁明了。
  • 标准化:提供了一套标准的、通用的解决方案,提高了代码的可移植性和可维护性。
  • 与C++生态的融合:更好地将C风格的资源获取/释放模式整合到现代C++的RAII和智能指针体系中。

4. 示例代码片段回顾

使用 std::out_ptr (配合 T** 参数的C API)

// 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;
    }
}

使用 std::inout_ptr (配合 T** 修改型参数的C API)

假设 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;
        }
    }
}

5. 结语

std::out_ptrstd::inout_ptr 是C++23中非常实用的工具,它们弥合了现代C++智能指针与传统C风格API在指针管理上的鸿沟。正确理解和使用这些适配器,能够显著提升代码的健壮性和简洁性,尤其是在与大量遗留C代码或底层库交互的场景中。务必注意其生命周期和与特定智能指针(如 shared_ptr)的兼容性问题,以及C API的具体行为,以发挥其最大效用。

C++类型擦除技术深度解析:概念、实践与优化

1. 理解类型擦除(Type Erasure)

1.1 核心概念

C++作为一种静态强类型语言,在编译期就确定了大部分类型信息,这带来了性能和安全上的优势。然而,在某些设计场景下,过于严格的类型约束反而会限制代码的灵活性和通用性。类型擦除(Type Erasure)技术应运而生,它允许我们编写能够操作多种不同具体类型的通用代码,而这些通用代码仅关注这些类型共有的、符合某种抽象定义的“特定行为”,仿佛这些类型的其他特有部分被“擦除”了一样。

实现类型擦除的两个关键要素:

  1. 特定行为的抽象:定义一组操作或接口,这些是通用代码所依赖的。
  2. 具体类型的隐藏:通用代码不直接依赖于某个具体类型,而是通过上述抽象接口与之交互,从而隐藏了底层的具体实现类型。

1.2 优缺点分析

优点:

  • 通用性与灵活性:允许使用统一的接口处理不同类型的对象,增强代码的复用性和适应性。
  • 封装与解耦:隐藏具体类型的实现细节,减少公有接口的类型暴露,使得代码更简洁,依赖关系更清晰,提高可维护性。
  • 支持多态:无论是运行时多态(通过虚函数)还是静态多态(通过模板),类型擦除都能提供一种实现方式,使得存储和操作异构对象集合成为可能。

缺点:

  • 运行时开销:通常需要额外的间接层(如虚函数调用、指针解引用、模板实例化)来实现类型封装和行为转发,可能引入性能开销。
  • 类型安全性的妥协:编译期类型检查的能力减弱。虽然我们试图通过抽象来约束行为,但有时仍需在运行时进行类型检查(如 std::any_cast)以确保操作的正确性,这可能导致运行时错误。
  • 调试困难:类型信息的隐藏可能使得调试过程更加复杂,因为在通用代码层面不易直接观察到具体对象的内部状态。

1.3 适用场景与设计特点

类型擦除技术特别适用于以下场景:

  • 代码逻辑依赖于对象的一组特定行为,而非其完整的类型信息。
  • 存在多种具体类型,它们都实现了这组特定的行为。
  • 希望隐藏这些具体类型中与特定行为无关的其他部分。

采用类型擦除技术的设计通常具备以下特点:

  • 通用代码不依赖于被擦除的具体类型
  • 在执行特定行为的通用代码中,具体类型被隐藏
  • 特定行为通过一个类型不可知(Type-Agnostic)的接口被调用。
  • 调用点是最后知道具体类型的地方,在该点,具体类型被转换为抽象接口。
  • 当需要访问具体类型的非特定行为时,可能需要类型具化(Type Recovery)操作。

1.4 C语言中的经典案例:qsort()

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() 的设计思想体现了类型擦除的基本原理。

2. C++中的类型擦除实践

2.1 传统面向对象方法:“接口+实现”

这是最常见的基于继承和虚函数的多态实现,也可以看作一种类型擦除。

// 抽象基类(接口)
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); // 通过基类指针调用虚函数,具体类型被"擦除"
        }
    }
}

存在的问题与反思

  1. 设计耦合:将 Draw 方法作为 Shape 的一部分,使得所有形状都必须依赖 Context。如果图形绘制逻辑并非形状的核心职责,这种设计可能不佳。
  2. 侵入性:外部类型(如隔壁老王写的 Triangle 类)必须继承自 Shape 才能被 DrawAllShapes 处理,这限制了代码复用。
  3. 接口膨胀:若要为 Shape 增加新行为(如 Serialize),就需要修改基类 Shape,违反了开闭原则(OCP),并可能影响整个继承体系。
  4. 值语义支持不佳:通常需要通过指针或引用来操作对象以实现多态,避免对象切片。返回类型擦除后的对象也往往只能是指针或引用,带来生命周期管理的复杂性。

2.2 基于模板的外部多态 (Non-intrusive Type Erasure)

现代C++更倾向于使用模板来实现非侵入式的类型擦除,它不要求具体类型继承自某个公共基类。

2.2.1 简单的模板泛化(非多态容器)

// 具体类型,无需共同基类
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),因此不是真正的运行时多态容器。

2.2.2 类型擦除容器 (Type Erasure Idiom / Wrapper)

这是一种更强大的技术,它创建一个包装类(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 无需继承共同基类。
  • OCP友好:易于扩展以支持新的形状类型,只需确保存在对应的 DrawShape 函数。
  • 值语义ShapeWrapper 可以(如果实现了 Clone)支持值拷贝,避免了裸指针管理。
  • 编译防火墙:增加新形状类型通常不需重新编译大量依赖 ShapeWrapper 的代码。

潜在问题与改进

  1. 行为硬编码ShapeModel 中对 DrawShape(ctx, m_shape_object) 的调用是硬编码的。如果不同类型有不同名称的绘制方法,或参数略有差异,就需要更复杂的适配。
    • 解决方案:可以使用 std::function 存储具体行为,并在 ShapeWrapper 构造时传入可调用对象。或者使用更高级的元编程技巧。
  2. 接口扩展:若要 ShapeWrapper 支持更多行为(如 Serialize),仍需修改 ShapeConcept 和所有 ShapeModel 特化。
    • 解决方案:可以设计更通用的 ShapeConcept,或者如原文提及,通过多重继承等方式扩展 ShapeConcept 的能力。

2.3 标准库中的类型擦除工具

  • 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;
}
  • Boost.TypeErasure: 一个功能更强大、更完善的第三方类型擦除库,提供了更灵活的概念定义和模型生成机制。

3. 类型擦除的相关议题

3.1 对象复制

类型擦除后的对象复制是个挑战,因为构造函数不能是虚的。

  • 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) {}

3.2 鸭子类型 (Duck Typing)

“如果它像鸭子一样走路,像鸭子一样嘎嘎叫,那它可能就是一只鸭子。” —— 鸭子类型关注对象的行为而非其继承关系。C++模板使得静态鸭子类型成为可能,结合类型擦除,可以实现运行时的外部多态。

3.3 优化

类型擦除容器通常基于值语义设计,其拷贝、移动和构造的性能至关重要。

  • 小对象优化 (Small Object Optimization, SOO):也称本地缓冲区优化 (Small Buffer Optimization, SBO)。当被包装的对象体积较小时,直接存储在包装类内部的缓冲区中,避免动态内存分配。std::functionstd::string (在某些实现中) 都使用了此技术。
  • 写时拷贝 (Copy-On-Write, COW):延迟实际的拷贝操作直到对象被修改时。在多线程环境下实现复杂,且现代C++中因移动语义的普及已较少推荐。
  • C++20 Concepts:可以用来约束模板参数,确保传入类型满足特定行为的编译期要求,增强类型安全性和代码可读性。
    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保证了其存在
        }
        // ...
    };

4. 总结

类型擦除是一种强大的设计技术,通过在适当的抽象层级隔离依赖关系,来应对软件设计中的变化。它允许我们创建灵活的、可扩展的系统,能够处理异构类型的对象集合,而无需强制它们继承自共同的基类。基于模板的外部多态是现代C++中实现类型擦除的常用且高效的方法,它能有效避免传统继承体系的弊端,如侵入性、接口膨胀和编译时依赖过重。结合小对象优化、概念等技术,可以构建出既灵活又高效的类型擦除解决方案。

@KuRRe8
Copy link
Author

KuRRe8 commented May 10, 2025

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment