C++的manipulator(控制符)的工作原理
如果要简单打印一个bool值:
std::cout << true;
结果是1,也就是true这个literal的字节存储表示。
但是有时候我们希望打印出来“true”,可以这么写:
std::cout << std::boolalpha << true;
其中std::boolalpha属于一类叫做manipulator(控制符)的函数,原型是:
ios_base& std::boolalpha(ios_base& str);
这个函数的作用是改变输入输出流str的格式属性,使其之后在遇到bool参数时以文本方式处理。注意这个状态的变更是持久的,之后所有std::cout的输出都会受到影响,直到再调用std::noboolalpha()恢复。
这里最让人困惑的,就是这个函数在上面使用的时候,看起来并不像一个函数,因为没有括号,而且参数也没看到。
先看看正常的写法:
std::boolalpha(std::cout); std::cout << true;
这么写很好理解,先改变状态,然后打印。结果是一致的。
那最初的写法是什么原理呢?
在Visual Studio里面跟踪操作符<<的定义,发现它是basic_ostream类的这么一个重载方法:
basic_ostream& __CLR_OR_THIS_CALL operator<<(ios_base&(__cdecl* _Pfn)(ios_base&) ) { // call ios_base manipulator _Pfn(*this); return *this; }
原来<<接受的是一个函数指针,这个函数的参数和返回值都是ios_base的引用。里面只是简单地调用了一下这个函数而已。
我们可以换个写法验证一下:
std::ios_base& (*func)(std::ios_base&) = std::boolalpha; std::cout << func << true;
这里我们先存储函数指针到func,然后再传进来,最终结果也是一样的。
另外,std::endl也是一种manipulator,甚至我们还能定义自己的manipulator,只要符合同样的函数声明。
扩展:
上述的manipulator是无参的,实际上还有带参的,例如常见的std::setprecision(n),这个似乎没法通过函数指针来解释了。
首先看它的声明:
_Smanip<long long> setprecision(long long n);
和之前不太一样,参数只有一个n,返回的是这个东西:
// STRUCT TEMPLATE _Smanip template <class _Arg> struct _Smanip { // store function pointer and argument value _Smanip(void(__cdecl* _Left)(ios_base&, _Arg), _Arg _Val) : _Pfun(_Left), _Manarg(_Val) {} void(__cdecl* _Pfun)(ios_base&, _Arg); // the function pointer _Arg _Manarg; // the argument value };
简单来说,就是返回了两个信息:一个函数指针,一个参数n。
最后再看看<<操作符:
template <class _Elem, class _Traits, class _Arg> basic_ostream<_Elem, _Traits>& operator<<(basic_ostream<_Elem, _Traits>& _Ostr, const _Smanip<_Arg>& _Manip) { // insert by calling function with output stream and argument (*_Manip._Pfun)(_Ostr, _Manip._Manarg); return _Ostr; }
和之前无参的<<不同,这个<<是全局函数,而不是某个类的成员函数,因此有两个参数:第一个参数对应我们的std::cout,第二个参数就是setprecision()的返回值。
里面实现很简单:调用相应函数,将流和n作为参数传递进去。
其实大体上还是类似,仍然是基于函数指针,只是将参数封装了一下。
总结一下:manipulator是C++IO标准库里面的一类特殊函数(不属于C++语言特性),作用是修改输入输出流的状态,这类函数可以通过<<操作符传递函数指针的方式来简化语法。
参考: