为什么 "transform(s.begin(),s.end(),s.begin(),tolower)" 不能成功编译?

35

给定代码:

#include <iostream>
#include <cctype>
#include <string>
#include <algorithm>
using namespace std;

int main()
{
     string s("ABCDEFGHIJKL");
     transform(s.begin(),s.end(),s.begin(),tolower);
     cout<<s<<endl;
}

我遇到了以下错误:

无法匹配函数调用transform(__gnu_cxx::__normal_iterator<char*, std::basic_string<char, std::char_traits<char>, std::allocator<char> > >, __gnu_cxx::__normal_iterator<char*, std::basic_string<char, std::char_traits<char>, std::allocator<char> > >, __gnu_cxx::__normal_iterator<char*, std::basic_string<char, std::char_traits<char>, std::allocator<char> > >, <未解决的重载函数类型>)

"未解决的重载函数类型"是什么意思?

如果我将tolower替换为我编写的一个函数,就不会出现错误了。


3
main函数的返回类型是int,在C++中返回类型必须显式声明。一些编译器可以通过发布的代码,但这是不标准的,可能会因为新的编译器版本或其他编译器而失效。 - David Rodríguez - dribeas
1
@DavidRodríguez-dribeas 在 C 或 C++ 中不需要从 main 返回,它会隐式返回 0。请参见此答案的评论:https://dev59.com/g1wX5IYBdhLWcg3wvRkf#33442842 - Jonathan Mee
5个回答

31
让我们看一下选项列表,从最差到最好进行排序。我们将在此列出它们并在下面进行讨论:
  1. transform(cbegin(s), cend(s), begin(s), ::tolower)
  2. transform(cbegin(s), cend(s), begin(s), static_cast<int(*)(int)>(tolower))
  3. transform(cbegin(s), cend(s), begin(s), [](const unsigned char i){ return tolower(i); })

您问题中的代码transform(s.begin(), s.end(), s.begin(), tolower)将会产生错误,例如:

调用transform(std::basic_string<char>::iterator, std::basic_string<char>::iterator, std::basic_string<char>::iterator, <unresolved overloaded function type>)时没有匹配的函数

您得到“未解决的重载函数类型”的原因是在std命名空间中有2个tolower:
  1. locale库定义了template <typename T> T tolower(T, const locale&)
  2. cctype库定义了int tolower(int)

1davka提供的解决方案。它通过利用localetolower不在全局命名空间中定义的事实来解决您的错误。

根据您的情况,localetolower可能值得考虑。您可以在这里找到tolower的比较:C++中哪个tolower更好?


很不幸,1 依赖于 cctypetolower 在全局命名空间中被定义。让我们看看为什么可能不是这种情况:
你正在正确地使用 #include <cctype>,因为在 C++ 中,#include <ctype.h> 已被弃用:http://en.cppreference.com/w/cpp/header 但是 C++ 标准在头文件的 D.3[depr.c.headers]2 中声明:

未指定这些名称是否首先在命名空间 std 的命名空间范围(3.3.6)内被声明或定义,然后通过显式 using 声明(7.3.3)注入到全局命名空间范围内。

因此,我们保证代码实现独立的唯一方法是使用来自命名空间stdtolowerDavid Rodríguez - dribeas提供的解决方案static_cast可以利用以下事实:

通过执行函数到特定类型的指针转换来消除函数重载的歧义。

在我们继续之前,请允许我评论一下,如果你觉得int (*)(int)有点令人困惑,你可以在这里阅读有关函数指针语法的更多信息。
很遗憾,tolower的输入参数存在另一个问题,如果它:

不能表示为无符号字符并且不等于EOF,则行为未定义

您正在使用char类型的元素的string。标准特别说明了char,即7.1.6.2[dcl.type.simple]3:

实现定义了char类型对象是表示为有符号还是无符号数量。 signed强制将char对象设为有符号。

因此,如果实现定义char表示signed char,则对于所有对应于负数的字符,12都会导致未定义的行为。(如果使用ASCII字符编码,则对应于负数的字符为扩展ASCII。)
如果在传递给tolower之前将输入转换为unsigned char,就可以避免未定义的行为。使用一个按值接受unsigned char的lambda表达式,再将它隐式转换为int并传递给tolower3就能实现这一点。
为了确保在所有符合规范的实现中保证定义良好的行为,独立于字符编码,您需要使用transform(cbegin(s), cend(s), begin(s), [](const unsigned char i){ return tolower(i); })或类似的东西。

2
我也想知道。 - exilit
7
我认为这是最好的答案。尽管我知道我的回答永远不会被采纳,但我还是花了时间回答这个问题,因为我觉得其他答案都有所欠缺。而有人仅仅为了点个踩,真是让人感到悲哀。无论如何,感谢你的确认。知道有人关心我努力提高答案质量的努力,真的很好。 - Jonathan Mee
1
@TonvandenHeuvel 谢谢,我也认为这是最完整的。但我不认为它会被接受,因为OP自2014年以来就没有出现过。 - Jonathan Mee

28
尝试使用::tolower。 这对我来说解决了问题。

4
@liu:就像@David所写的那样 - "::"从全局命名空间中选择tolower函数。 - davka
2
这个答案没有回答问题——为什么它不起作用? - tambre
取决于您的编译器。MSVC 19似乎没有问题。如果您查看cppreference的注释,它显示了如何正确使用std::tolower进行转换的具体示例。话虽如此,cppreference似乎表示函数签名是相同的(cctype, ctype),因此我实际上不确定为什么例如clang无法编译。 - xoorath
2
好的,我做了一些调查。在标准库中有一个带有 const locale& 参数的 tolower 版本,因此 transform 无法推断要使用哪个重载版本。由于 tolower 的全局命名空间版本没有其他重载版本,所以它可以正常工作。话虽如此,似乎 MSVC 没有这个问题——除非您包括 <locale>。 - xoorath

23

问题很可能与tolower有多个重载版本有关,编译器无法为您选择一个适当的版本。您可以尝试限定它以选择一个特定版本,或者您可能需要提供一个函数指针转换来消除歧义。 tolower函数可以存在于<locale>头文件中(具有多个不同的重载版本),也可以存在于<cctype>中。

尝试:

int (*tl)(int) = tolower; // Select that particular overload
transform(s.begin(),s.end(),s.begin(),tl );

这可以通过一行代码完成类型转换,但可能会更难阅读:

transform(s.begin(),s.end(),s.begin(),(int (*)(int))tolower );

6
但请注意,如果上述字符串中的任何 char 值为负数(在大多数现代系统上可能会出现,例如如果存在任何带重音的字符),则使用这个版本的 tolower 函数是未定义的行为。 - James Kanze
1
@James Kanze:好观点,我决定从阅读原始帖子中的过载来实现它(在那里明确包含了cctype而没有包含locale)。此外,locale中的函数需要不止一个参数,这意味着代码将通过bindbind2nd添加无关复杂性以提供默认的locale... - David Rodríguez - dribeas
3
请注意,使用::tolower虽然在不同编译器中可以工作,但它不是标准的。基本上,当您包含cctype时,大多数编译器都需要提供int std :: tolower(int),但不需要添加int :: tolower(int),不同的编译器将提供具有相同实现的两个函数(其中一个将转发到另一个),但这不是必需的,并且可能会随着下一次编译器发布(或更改编译器)而发生变化。 - David Rodríguez - dribeas
1
@liu 使用::tolower并不能解决问题。使用char参数调用::tolower是未定义的行为。你需要将其包装在一个函数对象中,将char转换为unsigned char。或者接受David Rodríguez在<locale>版本方面提到的所有复杂性。 - James Kanze
1
@davka 标准明确说明对于任何tolower参数:“不能表示为无符号字符且不等于EOF的行为未定义”,因此将扩展ASCII传递给 tolower 可能会出现未定义的行为:https://dev59.com/zm035IYBdhLWcg3wPdcS#37438120 - Jonathan Mee
显示剩余10条评论

7

David已经找到了问题所在,即以下两个函数之间的冲突:

  • <cctype>中的int tolower(int c)
  • <locale>中的template <typename charT> charT tolower(charT c, locale const& loc)

使用第一个函数更容易,但是只要处理的不是小写ascii(0-127)中的有符号字符,就会出现未定义的行为(不幸的是)。顺便说一下,我建议将char定义为无符号的。

模板版本很好,但是你必须使用bind来提供第二个参数,这肯定很丑陋...

因此,我可以介绍一下Boost String Algorithm库

更重要的是:boost::to_lower :)

boost::to_lower(s);

表达能力是非常重要的。


@davka:1)C++标准没有明确规定char是有符号还是无符号的。如果想要确定,可以进行限定。但是一些函数(例如int tolower(int))在使用负数char时会产生未定义行为...请查看您的编译器,可能有一个开关或合理的默认值。2)boost::to_lower基于C++的tolower函数,因此依赖于已注入的std::localectypefacet。请注意,这些facet无法处理多字符编码... - Matthieu M.
1
@davka:不,这是不合法的。编译器通常有开关让你决定,例如gcc-fsigned-char-funsigned-char - Matthieu M.
2
@davka:正确。而且你需要注意第三方库:为了让第三方库之间以及与你的程序顺畅地交互,所有的库都应该给char赋予相同的含义。你需要查看它们的文档以了解它们是否明确说明了这一点,或者检查代码以了解它们是否做出了假设:while((c = getchar()) == EOF)在使用-funsigned-char时会成为一个无限循环,因为getchar()返回的是一个int而不是一个char(而EOF-1)。 - Matthieu M.
@Matthieu M,你的原则是正确的,所有库都应该使用相同的“语言”进行编译,而改变普通字符的符号会改变语言。然而,在实践中,至少在2的补码机器上,改变char的符号并不会有效地改变接口,而且可以使用-funsigned-char编译,然后链接到不使用此选项的库。 - James Kanze
@James:我同意,链接将会顺利传递 :) - Matthieu M.
显示剩余5条评论

4
浏览gcc 4.2.1中的<ctype>头文件时,我看到了这个:
// -*- C++ -*- forwarding header.

// Copyright (C) 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005
// Free Software Foundation, Inc.

...

#ifndef _GLIBCXX_CCTYPE
#define _GLIBCXX_CCTYPE 1

#pragma GCC system_header

#include <bits/c++config.h>
#include <ctype.h>

// Get rid of those macros defined in <ctype.h> in lieu of real functions.
#undef isalnum
#undef isalpha

...

#undef tolower
#undef toupper

_GLIBCXX_BEGIN_NAMESPACE(std)

  using ::isalnum;
  using ::isalpha;

...

  using ::tolower;
  using ::toupper;

_GLIBCXX_END_NAMESPACE

#endif

看起来 tolower 函数同时存在于 <cctype> 头文件的 std 命名空间和 <ctype.h> 头文件的根命名空间中。我不确定 #pragma 的作用。


1
#pragma 指示 GCC 此文件是系统头文件。这通常会影响诊断,因为编译器发出警告对于与之捆绑且不应被更改的头文件被认为是不好的风格。 - Matthieu M.

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接