为什么std::tolower不能用于std::transform

在C++中,std::tolower和std::toupper函数(在本文中都用std::tolower指代两者)用于对一个字符进行大小写转换,将其与std::transform函数结合则可以对整个字符串进行大小写转换,如下所示:

1
2
3
4
5
6
7
std::string string = "ABCDEF";
std::transform(
string.begin(),
string.end(),
string.begin(),
std::tolower
);

以上代码在Visual C++下能编译成功,但是在XCode下却编译失败,错误信息为No matching function for call to 'transform'。这是因为std::tolower函数存在两个重载,第一个定义于头文件cctype,声明如下:

1
int tolower(int ch);

第二个定义于头文件locale,声明如下:

1
2
template<class charT>
charT tolower(charT ch, const locale& loc);

cctype和locale都是很基础的模块,会被其它头文件引用,因此这两个重载通常都会同时出现。XCode的编译器由于不知道该选择哪个重载而报错。

然而,从理论上来说,编译器是可以知道如何选择的。std::transform在XCode中的源码如下:

1
2
3
4
5
6
7
8
9
template <class _InputIterator, class _OutputIterator, class _UnaryOperation>
inline _LIBCPP_INLINE_VISIBILITY
_OutputIterator
transform(_InputIterator __first, _InputIterator __last, _OutputIterator __result, _UnaryOperation __op)
{
for (; __first != __last; ++__first, (void) ++__result)
*__result = __op(*__first);
return __result;
}

在第七行,可以看到调用__op的时候只传了一个参数,因此无论如何只能选择std::tolower的第一个重载,编译器完全有能力做出这个推导。如果把上述std::transform的源码转移到Visual C++并且以同样的方式来调用,可以编译成功,可见这个推导是可行的。那为什么XCode的编译器没有这么做呢?一个可能的原因是效率问题,类型推导越精确会耗费越多编译时间。从这一点或许可以解释为什么Visual C++的编译速度这么慢。

那么,应该如何解决这个问题呢?有两种方法,第一种方法是把std::tolower换成::tolower,如下所示:

1
2
3
4
5
6
std::transform(
string.begin(),
string.end(),
string.begin(),
::tolower
);

默认情况下,在全局名称空间中,tolower只有一种声明形式,因此没有问题。但前提是没有使用using namespace语句把std名称空间的内容导入全局名称空间——然而这种用法很常见,因此使用::tolower并不一定有效。

第二种方法是显式指定使用std::tolower的第一个重载,如下所示:

1
2
3
4
5
6
std::transform(
string.begin(),
string.end(),
string.begin(),
static_cast<int(*)(int)>(std::tolower)
);

这种做法带来了编码负担,因为每次使用的时候都要回忆一下std::tolower的声明形式,并且要输入更多字符。所以最好用一个函数把它封装起来,一劳永逸。