c++ stream manipulator 的实现机制
C++ stream manipulator,是 stream 的一种特殊的操作方式。manipulator 中文也翻译作运算子或者操作符。只要用到 stream,就必然会涉及到 manipulator。
比如下面的代码:
std::cout << "hello world!" << std::endl;
这里的 std::endl 就是一个最常用的 manipulator,类似的还有 std::hex,std::setw 等。那么他们的实现机理是怎样的呢?
首先看 std::endl, 它被定义为一个函数。参数就是 ostream& (即 cout 的类型),返回值也是 ostream&:
ostream& endl(ostream& obj);
所以对于 ostream manipulator 可以这样定义:返回值和第一个参数都是 ostream 的函数.
下面的代码是一个较为完整的模拟 manipulator 的例子:
#include <iostream> std::ostream& space(std::ostream& os) { os << ' '; return os; } int main(int, char*[]) { std::cout << "[" << space << "]" << std::endl; return 0; }
当然,目前我们模拟的manipulator 函数也有限制: 它不能带参数。
比如,我们想实现一个新的manipulator,命名为 spaces,它可以接受一个参数 int count,输出 count 个空格,使用起来是下面的样子:
std::cout << "[" << spaces(5) << "]" << std::endl;
则刚才的定义函数的思路,定义一个新的函数 spaces,让他除了 ostream&,还可以多接收一个 int 参数:
std::ostream& spaces(std::ostream& os, int count) { for (int i = 0; i < count; i++) os << '.'; return os; };
std::cout << "[" << spaces(5) << "]" << std::endl; // compile error
但调用 spaces 的程序没法写,写成上面的样子编译器会报错。
那调用的时候再如果加上一个参数呢? std::cout << "[" << spaces(std::cout, 5) << "]" << std::endl;
这样也行不通。因为按照C++语法,编译器会先计算这个函数的返回值,则这行代码最终会生成类似
spaces(std::cout, 5); std::cout << "[" << (void*)std::cout << "]" << std::endl;
的代码。运行结果有可能会是这个样子: .....[0x601088]
所以,这里必须存一个函数地址,或者是看起来像函数的东西:它只能先存下参数,但不能马上调用。
那么是不是可以就要用到仿函数的概念,把 spaces 实现为一个 functor,这样在调用 << spaces(5) 的时候,先生成一个 spaces 对象,把参数保存好,但先不调用功能。
为了能够接收 ostream& 参数,我们还需要为 spaces 类型定义一个 ostream operator << 的重载: std::ostream& operator << (std::ostream& os, spaces f) 这样在真正需要调用功能的时候才执行:
#include <iostream>
struct spaces
{ int _count;
spaces(int count) : _count(count) { } std::ostream& operator()(std::ostream& os)
{ for (int i = 0; i < _count; i++) os << ' '; return os; } }; std::ostream& operator << (std::ostream& os, spaces f)
{ return f(os); } int main(int, char*[])
{ std::cout << "[" << spaces(5) << "]" << std::endl; return 0; }
这样的实现已经可以达到我们的要求,运行结果可以正确输出: [ ]
如果作为库的实现者,需要考虑的要更多:
如果为每一个 manipulator 都实现了一个 functor,这就需要为每一种 functor都实现一遍 std::ostream& operator << (std::ostream& os, functor f) ,但内部实现都一样。
如果 manipulator 特别多,则带来的弊端是大量的 manipulator 类定义,且每种 operator<< 的重载实现都看似重复。
vs2010 的 C++ 库实现是这样的:
定义一个通用的 functor 模板 _Smanip,通过它把参数和调用函数地址存起来,这里用 setbase 举例:
// ------------ iomanip -------------------- template<class _Arg> struct _Smanip { // store function pointer and argument value _Smanip(void (__cdecl *_Left)(ios_base&, _Arg), _Arg _Val) : _Pfun(_Left), _Manarg(_Val) { // construct from function pointer and argument value } void (__cdecl *_Pfun)(ios_base&, _Arg); // the function pointer _Arg _Manarg; // the argument value }; _MRTIMP2 _Smanip<int> __cdecl setbase(int); // ---------- iomanip.cpp ------------------ static void __cdecl sbfun(ios_base& iostr, int base) { // set base iostr.setf(base == 8 ? ios_base::oct : base == 10 ? ios_base::dec : base == 16 ? ios_base::hex : ios_base::_Fmtzero, ios_base::basefield); } _MRTIMP2 _Smanip<int> __cdecl setbase(int base) { // manipulator to set base return (_Smanip<int>(&sbfun, base)); }
代码不难理解:manipulator 的形式是 setbase(int),有一个实现其功能的函数 sbfun(ios_base&, int),通过 _Smanip<int> 将二者联系起来。
当然各家的实现方法都不一样:
我的 linux-mint 上带的是 gcc 4.8.2-19,里面的 glibc 没有用 functor,每种 manipulator 只定义一个结构保存参数,真正的功能实现在 operator << 的重载函数里。
struct _Setbase { int _M_base; }; inline _Setbase setbase(int __base) { return { __base }; } template<typename _CharT, typename _Traits> inline basic_ostream<_CharT, _Traits>& operator<<(basic_ostream<_CharT, _Traits>& __os, _Setbase __f) { __os.setf(__f._M_base == 8 ? ios_base::oct : __f._M_base == 10 ? ios_base::dec : __f._M_base == 16 ? ios_base::hex : ios_base::fmtflags(0), ios_base::basefield); return __os; }
到目前为止,C++ stream manipulator 的实现机制就已经很清楚了。文中只涉及到了一个参数的 manipulator,没有参数或者两个或者多个参数的原理也是一样的。
基本思路就是:
1. 利用一个类 manip,把参数存起来(如果没有参数这一步就省了)。
2. 通过重载 operator << (ostream&, manip) 响应 << 的操作。
如果 manipulator 没有参数,第1步就退化为直接定义一个参数为 stream 对象引用的函数。
如果不想把实现写在 operator << 的重载函数里,而是单独提出来放在一个函数 mani_impl 里,则第1步还要将 mani_impl 的地址存在 manip 对象里,以便在 operator << 的重载函数里调用。

浙公网安备 33010602011771号