前言
在 C++11 之前,往容器里填一批初始值是一件很啰嗦的事:要么先定义再逐条push_back,要么先建一个临时数组再整体拷贝。Boost.Assign 是 Boost 里一个很小的头文件库,目标就是让「一次性填充容器」写得像自然语言一样紧凑,例如v += 1, 2, 3;或者std::vector<int> v = list_of(1)(2)(3);。它在 2000 年代中后期的代码库中非常常见。
需要先明确:Boost.Assign 是第三方库,不是标准库;C++11 引入初始化列表(initializer list)之后,绝大多数场景已经被std::vector<int> v{1, 2, 3};取代。本文讲它,一是因为大量存量代码仍在用,二是它把「链式插入器」这个设计手法展示得很清楚,理解之后再看其他流式接口会轻松很多。文中涉及的 Boost 接口以 Boost 官方文档为准,不同版本的细节可能有差异。
本文的代码以 C++17 为基准,示例只做语义推演,未在本机编译,也不对任何输出作断言。
一、Boost.Assign 提供什么
Boost.Assign 的主要入口在头文件<boost/assign.hpp>中,它聚合了几个子头文件。核心设施可以分成三类。
第一类是list_of,用于顺序容器。它返回一个插入器对象,后面用函数调用运算符连续追加值,最后可以转换成目标容器:
// 适用于 C++17,需要 Boost
#include <boost/assign.hpp>
#include <vector>
std::vector<int> v = boost::assign::list_of(1)(2)(3)(4);第二类是map_list_of和pair_list_of,用于关联容器。前者的每个实参对是键和值,后者直接接收std::pair:
// 适用于 C++17,需要 Boost
#include <boost/assign.hpp>
#include <map>
#include <string>
std::map<int, std::string> m =
boost::assign::map_list_of(1, "one")(2, "two")(3, "three");第三类是作用于「已有容器」的插入器:push_back、push_front、insert,以及重载的operator+=和operator,。它们不创建新容器,而是把值追加到现有对象上:
// 适用于 C++17,需要 Boost
#include <boost/assign.hpp>
#include <map>
#include <set>
#include <vector>
using namespace boost::assign; // 让重载的 += 与逗号参与查找
std::vector<int> v;
v += 1, 2, 3; // 追加到尾部
std::set<int> s;
s += 10, 20, 30; // 对 set 而言是插入
std::map<int, std::string> m;
boost::assign::insert(m)(1, "one")(2, "two");insert(m)(key, value)的写法对std::map很友好,因为它把键和值放在同一对括号里,比m.insert(std::make_pair(k, v))短,也比m[k] = v更明确地表达「插入而非覆盖」。
二、链式插入器是怎么实现的
list_of(1)(2)(3)能成立,靠的是两层重载:函数调用运算符和逗号运算符。
list_of返回一个list_inserter模板对象,内部持有一个「插入策略」(inserter)和目标容器的引用或指针。每调用一次operator(),它就把值交给策略对象插入容器,然后返回自身的引用,于是可以继续(...)(...)。这跟流式输出std::cout << a << b的返回引用套路一致。
v += 1, 2, 3;则依赖运算符优先级:+=的优先级高于逗号,所以表达式先算v += 1。Boost 为容器重载的operator+=返回list_inserter,接着内建的逗号运算符被重载版本接管,把2、3依次插入并继续返回插入器。整条语句的最终结果被丢弃,副作用留在容器里。
理解这一点很重要:v += 1, 2, 3;不是「把逗号表达式的结果赋给 v」,而是「+=返回一个可以继续接收值的代理对象」。这也解释了为什么单独写v += 1;同样有效,以及为什么auto x = (v += 1);得到的是一个插入器而不是容器。
转换到容器这一步,由插入器的转换运算符完成:它按顺序把已经记录或已经插入的值交给目标容器。具体是「边调用边插入」还是「最后统一插入」,取决于所用的插入策略;对使用者而言,两种方式的可见结果一致。
三、与 C++11 初始化列表的对比
C++11 起,标准容器都接受std::initializer_list,直接写花括号即可:
// 适用于 C++17
#include <map>
#include <string>
#include <vector>
std::vector<int> v{1, 2, 3, 4};
std::map<int, std::string> m{{1, "one"}, {2, "two"}, {3, "three"}};对比之下,Boost.Assign 的优势只剩下几处:可以往已有容器追加而不构造临时容器;insert(m)(k, v)对 map 的写法紧凑;在老代码里保持风格统一。缺点也明显:需要第三方依赖、宏与模板错误信息不友好、using namespace boost::assign;容易污染命名空间。
| 维度 | Boost.Assign | C++11 初始化列表 |
|---|
| 是否标准 | 否,需要 Boost | 是,语言与标准库内建 |
| 填充新容器 | list_of(1)(2) | {1, 2} |
| 追加到已有容器 | v += 1, 2; | v.insert(v.end(), {1, 2}); |
| map 初始化 | map_list_of(1, "a") | {{1, "a"}} |
| 类型推导 | 依赖插入器模板 | 依赖initializer_list<T> |
| 编译错误可读性 | 较差 | 较好 |
| 适用标准 | C++03 起 | C++11 起 |
如果项目已经是 C++17,新代码应优先使用初始化列表;Boost.Assign 只在维护旧代码或确实需要「追加」语义时保留。
四、完整示例:配置表的两种写法
下面用一个「把配置项装进容器」的例子,把 Boost.Assign 与标准写法并排放。示例只展示接口用法,输出内容不作断言。
// 适用于 C++17,需要 Boost
#include <boost/assign.hpp>
#include <iostream>
#include <map>
#include <string>
#include <vector>
int main() {
using namespace boost::assign; // 仅限本函数作用域
// 1. 顺序容器:先构造,再追加
std::vector<std::string> hosts;
hosts += "127.0.0.1", "example.invalid";
// 2. 关联容器:一次性构造
std::map<std::string, int> ports =
boost::assign::map_list_of("http", 80)("https", 443);
// 3. 已有 map:插入而非覆盖
boost::assign::insert(ports)("ssh", 22)("dns", 53);
// 4. 标准写法对照
std::vector<std::string> hosts2{"127.0.0.1", "example.invalid"};
std::map<std::string, int> ports2{
{"http", 80}, {"https", 443}, {"ssh", 22}, {"dns", 53}};
for (const auto& h : hosts) {
std::cout << h << '\n';
}
for (const auto& kv : ports) {
std::cout << kv.first << "=" << kv.second << '\n';
}
for (const auto& kv : ports2) {
std::cout << kv.first << "=" << kv.second << '\n';
}
(void)hosts2;
return 0;
}这段代码里,hosts += "127.0.0.1", "example.invalid";依赖using namespace boost::assign;把重载的+=与逗号带入作用域。实参依赖查找不会把boost::assign的运算符关联到std::vector,所以只包含头文件而不引入命名空间是找不到这些重载的。实际项目中更稳妥的写法是显式限定命名空间,或者干脆用初始化列表;把using namespace放进函数作用域,至少不会污染头文件。
如果确实要保留 Boost.Assign,建议把它限制在.cpp文件里,公开头文件只暴露标准容器类型。这样下游使用者不需要安装 Boost 就能包含你的头文件,构建系统的传递依赖也更干净。
五、什么时候不该用它
第一,接口边界不要用。list_inserter是库内部的代理类型,不应该出现在函数参数或返回值里;一旦 Boost 升级改变了实现,接口就跟着变。
第二,需要严格控制分配次数时不要用。插入器可能引起多次push_back,而push_back可能触发多次扩容;标准初始化列表配合reserve更容易控制容量。至于具体分配几次,取决于实现与容器状态,不能凭印象给数字。
第三,跨团队的公共代码不要用。多一个第三方依赖就多一份构建与升级成本,而它带来的收益只是写法短一点。C++17 项目没有任何理由在新代码里引入它。
常见坑点
- ❌ 只包含
<boost/assign/list_of.hpp>,却使用map_list_of或重载的+=。✅ 直接包含<boost/assign.hpp>,或按官方文档分别包含对应的子头文件,避免「函数找不到」的编译错误。
- ❌ 在头文件里写
using namespace boost::assign;,把重载的逗号与+=暴露给所有包含者。✅ 只在实现文件的函数作用域内使用,或改为完全限定的调用。
- ❌ 写
v += 1, 2, 3;时以为逗号只是分隔符,把语句拆成三行后行为不变。✅ 拆开后每行都要写v += ...;链式写法依赖+=返回的插入器,不能随意拆。
- ❌ 对
std::map写m += 1, "one";期望插入键值对。✅ map 用insert(m)(1, "one")或map_list_of(1, "one"),因为+=的实参是单个元素,不是两个独立值。
- ❌
auto it = (v += 1);之后把it当容器使用,或让它活得比v更久。✅ 插入器只是代理,立即用完即弃;不要保存、不要跨作用域传递。
- ❌
list_of(1)(2.5)(3)混合类型后赋给std::vector<int>,依赖隐式截断。✅ 保持元素类型一致,需要转换时显式写static_cast<int>(2.5),让意图可见。
- ❌ 用
list_of("a")("b")构造std::vector<std::string>时依赖元素类型自动变成std::string。✅ 需要精确类型时写list_of(std::string("a"))(std::string("b")),避免推导出const char*带来的意外。
总结
| 关注点 | 结论 |
|---|
| 定位 | Boost 的容器填充辅助库,非标准组件 |
| 核心机制 | 插入器代理 + 重载()、,、+= |
| 主要优势 | 追加语义紧凑、map 插入写法短 |
| 主要劣势 | 第三方依赖、错误信息差、命名空间污染 |
| 现代替代 | C++11 初始化列表,C++17 项目优先使用 |
| 使用建议 | 仅用于维护存量代码,且限制在实现文件内 |
Boost.Assign 是一个「用运算符重载换取书写便利」的典型例子:它没有引入新的运行期机制,只是把插入动作包装成可链式调用的代理对象。理解了代理与重载逗号的关系,就能准确判断哪些写法成立、哪些只是碰巧能编译。新项目请直接用初始化列表;老项目迁移时,把list_of换成花括号、把map_list_of换成嵌套花括号,通常是最省事的路径。