1 设计模式视角:适配器模式

适配器模式(Adapter Pattern) 是一种结构型设计模式,其核心思想是:将一个类的接口转换成客户希望的另一个接口,使得原本因接口不兼容而不能一起工作的类可以协同工作。

在 C++ 中,适配器模式通常有两种实现方式:

  • 对象适配器:通过组合(持有被适配对象)实现。

  • 类适配器:通过多重继承(私有继承被适配类)实现。

C++11 标准库中的“包装器”概念,本质上是适配器模式的泛化与扩展。它不仅限于类的接口适配,还扩展到了:

  • 容器接口适配stack/queue 封装 deque

  • 可调用对象接口统一std::function

  • 函数参数适配std::bind

这些工具共同构成了 C++ 中强大的接口适配体系。


2 STL 中的容器适配器

STL 提供了三种经典的容器适配器:stackqueuepriority_queue。它们并非独立的容器,而是对其他底层容器(如 dequevectorlist)的接口封装。

2.1 std::stack

std::stack 提供 LIFO(后进先出)语义。默认底层容器为 std::deque

cpp

#include <stack>
#include <vector>
#include <list>

std::stack<int> s1;                     // 使用 deque
std::stack<int, std::vector<int>> s2;   // 使用 vector
std::stack<int, std::list<int>> s3;     // 使用 list

关键成员函数

  • push() / emplace():入栈

  • pop():出栈(无返回值)

  • top():访问栈顶元素

  • empty() / size()

实现要点
适配器通过 protected 继承或组合(标准未规定)持有底层容器,所有操作均转发给底层容器的对应方法。例如 push 调用 c.push_back()pop 调用 c.pop_back()

2.2 std::queue

std::queue 提供 FIFO(先进先出)语义。默认底层容器为 std::deque

cpp

std::queue<int> q1;
std::queue<int, std::list<int>> q2;

关键成员函数

  • push() / emplace():队尾入队

  • pop():队首出队

  • front() / back():访问队首/队尾元素

限制std::vector 不能作为 queue 的底层容器,因为它不支持 pop_front()

2.3 std::priority_queue

std::priority_queue 提供优先队列语义,默认使用最大堆(std::vector 作为底层,std::less 作为比较器)。

cpp

std::priority_queue<int> pq1;                      // 最大堆
std::priority_queue<int, std::vector<int>, std::greater<int>> pq2; // 最小堆

关键成员函数

  • push() / emplace():插入元素,内部调用 push_heap

  • pop():弹出堆顶,内部调用 pop_heap

  • top():访问堆顶

底层实现:通过 <algorithm> 中的 push_heappop_heap 维护堆结构。

2.4 底层容器选择与性能分析

适配器 默认容器 可选容器 要求
stack deque vectorlistdeque 支持 back()push_back()pop_back()
queue deque listdeque 支持 front()back()push_back()pop_front()
priority_queue vector vectordeque 支持随机访问迭代器

性能考量

  • deque:在两端插入/删除为 O(1),内存分配策略优于 vector(分段连续),适合作为 stack/queue 的默认选择。

  • vector:仅当需要极致内存紧凑性时用于 stack,但 vector 在重新分配时会复制所有元素。

  • list:节点独立分配,缓存不友好,但能在 O(1) 合并两个队列。

C++11 的改进

  • 引入 emplace 系列方法,减少临时对象构造。

  • 移动语义使容器适配器在返回大对象时效率更高(底层容器支持移动构造)。


3 C++11 函数包装器:std::function

std::function 是 C++11 引入的通用多态函数包装器,它可以存储、复制和调用任何可调用对象(函数、函数指针、Lambda、函数对象、成员函数指针等)。

3.1 可调用对象类型统一

cpp

#include <functional>

int add(int a, int b) { return a + b; }

struct Multiply {
    int operator()(int a, int b) const { return a * b; }
};

int main() {
    std::function<int(int, int)> func;

    func = add;                     // 函数指针
    std::cout << func(2, 3) << '\n';

    func = Multiply();              // 函数对象
    std::cout << func(2, 3) << '\n';

    func = [](int a, int b) { return a - b; }; // Lambda
    std::cout << func(2, 3) << '\n';

    return 0;
}

成员函数指针的特殊处理:需要借助 std::mem_fn 或 std::bind,或使用 Lambda 捕获对象。

cpp

struct Foo {
    int value;
    int add(int x) const { return value + x; }
};

std::function<int(const Foo&, int)> f = &Foo::add;
Foo foo{10};
std::cout << f(foo, 5) << '\n'; // 15

// 或者通过 std::bind 绑定对象
std::function<int(int)> g = std::bind(&Foo::add, foo, std::placeholders::_1);

3.2 实现原理与类型擦除

std::function 的核心是 类型擦除(Type Erasure)。其内部通常采用“小对象优化”(Small Object Optimization)来避免堆分配。

简化实现思路

  1. std::function 内部持有一个抽象基类指针 _CallableBase

  2. 对于每个具体的可调用对象类型 T,派生一个模板类 _CallableImpl<T>,存储 T 的实例,并实现 operator() 的虚函数调用。

  3. 构造函数模板根据实际类型创建对应的派生类对象。

  4. 当可调用对象较小时(如函数指针、小 Lambda),可以存储在内部缓冲区(如 void* buf[16])中,避免堆分配。

伪代码示例

cpp

template <typename Signature>
class function;

template <typename Ret, typename... Args>
class function<Ret(Args...)> {
    struct CallableBase {
        virtual ~CallableBase() = default;
        virtual Ret invoke(Args... args) = 0;
        virtual CallableBase* clone() const = 0;
    };

    template <typename F>
    struct CallableImpl : CallableBase {
        F f;
        CallableImpl(F&& f) : f(std::forward<F>(f)) {}
        Ret invoke(Args... args) override { return f(std::forward<Args>(args)...); }
        CallableBase* clone() const override { return new CallableImpl<F>(f); }
    };

    CallableBase* ptr;
    // ... 小对象优化缓冲区
};

C++11 的移动语义std::function 的移动构造和移动赋值避免了不必要的复制,尤其对于大型函数对象。

3.3 性能开销与优化技巧

开销来源

  • 类型擦除虚函数调用:每次调用 operator() 都经过虚函数间接调用。

  • 可能的堆分配:当可调用对象大于内部缓冲区时,会进行动态内存分配。

  • 复制成本std::function 复制时会复制内部的可调用对象(如果未启用小对象优化则可能复制堆数据)。

优化建议

  1. 优先使用 Lambda 表达式:如果不需要类型擦除,直接使用 Lambda(每个 Lambda 有唯一类型,编译器可内联)。

  2. 使用 std::function 存储小对象:函数指针、捕获少量变量的 Lambda 通常可触发小对象优化。

  3. 移动而非复制:传递 std::function 时使用 std::move

  4. 避免频繁构造:重复使用同一个 std::function 对象,避免在循环中构造临时对象。

基准测试示意

cpp

// 直接调用函数指针
int (*fp)(int, int) = add;
for (int i = 0; i < 1e8; ++i) fp(i, i);  // 极快

// 通过 std::function 调用
std::function<int(int,int)> f = add;
for (int i = 0; i < 1e8; ++i) f(i, i);    // 慢约 2-5 倍(取决于编译器优化)

4 绑定器:std::bind

std::bind 是一种函数适配器,它可以将可调用对象与其部分参数绑定,生成一个新的可调用对象。它支持占位符,允许延迟指定参数。

4.1 参数绑定与占位符

cpp

#include <functional>
using namespace std::placeholders;

int f(int a, int b, int c) { return a + b + c; }

auto g = std::bind(f, 1, _2, _1);   // 绑定第一参数为1,第二参数取占位符2,第三参数取占位符1
std::cout << g(10, 20);              // 等价于 f(1, 20, 10) -> 31

占位符_1_2、...、_N 定义在 std::placeholders 命名空间中,数量最多可达 20(标准未限制,但实现通常支持 20 或更多)。

嵌套绑定std::bind 可以嵌套,内层 bind 的结果在调用时会被求值。

cpp

auto h = std::bind(f, std::bind(g, _1, _2), 100, 200);

4.2 嵌套绑定与函数组合

通过 std::bind 可以实现简单的函数组合:

cpp

auto add1 = std::bind(std::plus<int>(), _1, 1);
auto mul2 = std::bind(std::multiplies<int>(), _1, 2);
auto add1_then_mul2 = std::bind(mul2, std::bind(add1, _1));

std::cout << add1_then_mul2(5); // (5+1)*2 = 12

4.3 与 Lambda 表达式的对比

C++11 引入了 Lambda,很多场景下 Lambda 比 std::bind 更清晰、更高效。

特性 std::bind Lambda
语法简洁性 复杂,需要占位符 直观,捕获列表清晰
编译优化 难以内联,类型擦除 每个 Lambda 是独立类型,易内联
成员函数绑定 需使用 &Class::method 和对象指针 可直接捕获对象后调用
重载函数绑定 需要显式转型 可直接在 Lambda 内调用,重载决议正常

推荐:C++11 之后,除非需要与旧代码兼容或实现高阶函数组合(如 std::bind(std::less<int>(), _1, 0) 用于过滤),否则优先使用 Lambda。

示例对比

cpp

// 使用 bind
auto isPositive = std::bind(std::greater<int>(), _1, 0);

// 使用 Lambda
auto isPositive = [](int x) { return x > 0; };

5 其他包装器工具

5.1 std::ref 与 std::cref

std::ref 和 std::cref 用于在函数对象中按引用传递参数,避免复制。它们生成 std::reference_wrapper 对象,隐式转换为引用类型。

典型应用std::bind 默认按值传递参数,使用 std::ref 可以传递引用。

cpp

void increment(int& x) { ++x; }

int main() {
    int n = 0;
    auto bound = std::bind(increment, std::ref(n));
    bound();
    std::cout << n; // 1
}

与 Lambda 对比:Lambda 可直接捕获引用 [&n],更直观。

5.2 std::mem_fn

std::mem_fn 用于将成员函数包装为可调用对象,类似于 std::function 的轻量版本,但不需要显式指定函数签名。

cpp

struct Point {
    int x, y;
    void print() const { std::cout << x << ',' << y; }
};

auto printFn = std::mem_fn(&Point::print);
Point p{1,2};
printFn(p);      // 通过对象
printFn(&p);     // 通过指针

std::mem_fn 生成的包装器支持通过对象、引用、指针调用,非常灵活。


6 综合应用与最佳实践

6.1 设计模式中的适配器实现

对象适配器示例:将旧版图形库适配到新版接口。

cpp

// 旧接口
class LegacyRectangle {
public:
    void draw(int x1, int y1, int x2, int y2) {
        std::cout << "Legacy draw\n";
    }
};

// 新接口
class Shape {
public:
    virtual void draw() = 0;
    virtual ~Shape() = default;
};

// 适配器
class RectangleAdapter : public Shape {
    LegacyRectangle adaptee;
public:
    void draw() override {
        adaptee.draw(0, 0, 10, 10);
    }
};

使用 std::function 实现更灵活的适配

cpp

using DrawCallback = std::function<void()>;

class FlexibleAdapter : public Shape {
    DrawCallback drawImpl;
public:
    FlexibleAdapter(DrawCallback cb) : drawImpl(std::move(cb)) {}
    void draw() override { drawImpl(); }
};

// 使用
LegacyRectangle rect;
FlexibleAdapter adapter([&rect](){ rect.draw(0,0,10,10); });

6.2 回调系统与事件驱动架构

std::function 与 std::bind 常被用于回调系统。

cpp

class Button {
public:
    using ClickHandler = std::function<void()>;
    void setClickHandler(ClickHandler handler) { clickHandler = std::move(handler); }
    void click() { if (clickHandler) clickHandler(); }
private:
    ClickHandler clickHandler;
};

struct Logger {
    void log(const std::string& msg) { std::cout << msg << '\n'; }
};

int main() {
    Button btn;
    Logger logger;
    btn.setClickHandler(std::bind(&Logger::log, &logger, "Button clicked"));
    btn.click();
}

6.3 性能关键代码的权衡

在性能敏感场景(如游戏引擎、高频交易):

  • 避免在热路径使用 std::function,改用模板或直接调用。

  • 使用 Lambda 捕获局部变量,让编译器充分内联。

  • 若必须类型擦除,考虑自定义小对象优化或使用函数指针数组。

示例:策略模式优化

cpp

// 低性能(虚函数)
class Strategy { public: virtual int execute(int) = 0; };
// 高性能(模板策略)
template <typename F>
int compute(int x, F&& strategy) { return strategy(x); }

7 总结

C++11 通过 std::functionstd::bindstd::ref 以及容器适配器,为开发者提供了一套完整的接口适配工具:

  1. 容器适配器:快速将底层容器转换为特定数据结构(栈、队列、优先队列),体现了对象适配器模式。

  2. 函数包装器std::function 统一了所有可调用对象类型,但伴随一定的运行时开销。

  3. 绑定器std::bind 实现了参数适配,但 Lambda 在多数场景下更优。

  4. 其他辅助工具std::ref 实现引用语义,std::mem_fn 适配成员函数。

更多推荐