std::string 和字符串文字之间的不一致

Posted

技术标签:

【中文标题】std::string 和字符串文字之间的不一致【英文标题】:Inconsistency between std::string and string literals 【发布时间】:2011-10-07 07:56:49 【问题描述】:

我发现std::string 和 C++0x 中的字符串文字之间存在令人不安的不一致:

#include <iostream>
#include <string>

int main()

    int i = 0;
    for (auto e : "hello")
        ++i;
    std::cout << "Number of elements: " << i << '\n';

    i = 0;
    for (auto e : std::string("hello"))
        ++i;
    std::cout << "Number of elements: " << i << '\n';

    return 0;

输出是:

Number of elements: 6
Number of elements: 5

我理解为什么会发生这种情况的机制:字符串文字实际上是一个包含空字符的字符数组,当基于范围的 for 循环在字符数组上调用 std::end() 时,它会得到一个指针过去数组的结尾;由于空字符是数组的一部分,因此它会获得一个越过空字符的指针。

但是,我认为这是非常不可取的:当涉及到与长度一样基本的属性时,std::string 和字符串文字肯定应该表现相同?

有没有办法解决这种不一致?例如,可以为字符数组重载std::begin()std::end(),以使它们分隔的范围不包括终止的空字符吗?如果是,为什么没有这样做?

编辑:为了向那些说我只是在遭受使用“遗留功能”的 C 风格字符串的后果的人证明我的愤慨,请考虑以下代码以下:

template <typename Range>
void f(Range&& r)

    for (auto e : r)
    
        ...
    

您是否希望 f("hello")f(std::string("hello")) 做一些不同的事情?

【问题讨论】:

这是一个真实的问题吗?它更像是关于标准应该是什么而不是它是什么的个人意见。 基于一些答案和 cmets,我现在想知道负责确定未来 C++ 版本功能的人员是否考虑为 std::string 字符串添加新的字符串文字语法。我的意思是,Objective-C 和 C# 都使用 @"" 来表示非 C 风格的字符串文字,即使在 C 和 C++ 中,您也可以使用 L"" 语法来表示宽字符串文字。 (而且似乎L'' 可以用来表示文字wchars?) @JAB: 字符串字面量到底有什么问题,需要另一种内置类型? @Gene:当整数类型完美地满足目的时,为什么 C 实现了布尔类型? @JAB:在 C++0x 中,您将能够通过用户定义的文字为std::string创建一个新的字符串文字语法。 【参考方案1】:

如果我们为 const char 数组重载 std::begin()std::end() 以返回比数组大小小 1,那么以下代码将输出 4 而不是预期的 5:

#include <iostream>

int main()

    const char s[5] = 'h', 'e', 'l', 'l', 'o';
    int i = 0;
    for (auto e : s)
        ++i;
    std::cout << "Number of elements: " << i << '\n';

【讨论】:

也许有一种方法可以将定义为字符串文字的字符数组与正常定义的字符数组区分开来?我们只想为前者重载。 我不知道在图书馆有什么方法可以做到这一点。您必须更改语言,而该更改会破坏代码。窄字符串字面量定义为由 n 个 const char 组成的数组,其中 n 是字符数加一表示终止的 null。 任何解决方案都需要解决如何处理const char s[6] = 'h', 'e', 'l', 'l', 'o', '\0';。我在这里支持霍华德,C++ 程序员应该知道sizeof("Hello")==6 @HighCommander4:我使用sizeof("Hello")==6 作为在 C 和 C++ 中编写它的快速方法,字符串文字 长度为 N+1 的常量字符数组,包括一个终止 \0。编译器在进行参数重载时不需要,也可能不会区分这两者。这意味着您将强制为次要功能重新设计主要的编译器。 我刚刚意识到情况比这更糟。一个翻译单元可以定义char const s[6]="Hello";,另一个可以调用end(s)-begin(s)。这意味着字符串文字和字符串数组之间的差异需要更改 ABI。抱歉,这不会发生。【参考方案2】:

但是,我认为这是非常不可取的:当涉及到与长度一样基本的属性时,std::string 和字符串文字肯定应该表现相同吗?

根据定义,字符串文字在字符串末尾有一个(隐藏的)空字符。 Std::strings 没有。因为 std::strings 有一个长度,所以那个空字符有点多余。字符串库的标准部分明确允许非空终止的字符串。

编辑 在大量赞成和大量反对的意义上,我认为我从未给出过更具争议性的答案。

auto 迭代器在应用于 C 样式数组时会迭代数组的每个元素。范围的确定是在编译时而不是运行时进行的。这是格式错误的,例如:

char * str;
for (auto c : str) 
   do_something_with (c);

有些人使用 char 类型的数组来保存任意数据。是的,这是一种老式的 C 思维方式,也许他们应该使用 C++ 风格的 std::array,但该构造非常有效且非常有用。如果他们的 char buffer[1024]; 上的自动迭代器在元素 15 处停止,只是因为该元素恰好与空字符具有相同的值,那些人会相当沮丧。 Type buffer[1024]; 上的自动迭代器将一直运行到最后。是什么让 char 数组如此值得完全不同的实现?

请注意,如果您希望对字符数组的自动迭代器提前停止,有一种简单的机制可以做到这一点:在循环体中添加 if (c == '0') break; 语句。

底线:这里没有矛盾。 char[] 数组上的 auto 迭代器与任何其他 C 样式数组的自动迭代器工作方式一致。

【讨论】:

这个答案只是重复了提问者在问题中所说的话,它根本没有解决问题(见最后一段)【参考方案3】:

您在第一种情况下得到6 是在C 中无法避免的抽象泄漏。std::string“修复”了它。为了兼容性,C 风格的字符串文字的行为在 C++ 中没有改变。

例如,std::begin() 和 std::end() 可以重载吗? 字符数组,以便它们分隔的范围不包括 终止空字符?如果是,为什么没有这样做?

假设通过指针访问(而不是char[N]),只需在包含字符数的字符串中嵌入一个变量,这样就不再需要寻找NULL。哎呀!那是std::string

“解决不一致”的方法是根本不使用遗留功能

【讨论】:

“根本不使用旧功能。”不使用字符串文字似乎是一项艰巨的任务(并且必须记住字符串文字是一项“遗留”功能也可能是一项艰巨的任务)。 @Suma:嗯,我说的是传递char const*char[N]。字符串文字本身当然仍然是完全合理的。诚然,OP 在他的问题中使用的是字符串文字;我猜for (auto c : "literal") 有点棘手。无论如何,std::string OP 不喜欢的行为的“修复”。【参考方案4】:

根据 N3290 6.5.4,如果范围是数组,则边界值为 在没有begin/end函数调度的情况下自动初始化。 那么,像下面这样准备一些包装器怎么样?

struct literal_t 
    char const *b, *e;
    literal_t( char const* b, char const* e ) : b( b ), e( e ) 
    char const* begin() const  return b; 
    char const* end  () const  return e; 
;

template< int N >
literal_t literal( char const (&a)[N] ) 
    return literal_t( a, a + N - 1 );
;

那么下面的代码就是有效的:

for (auto e : literal("hello")) ...

如果您的编译器提供用户定义的文字,缩写可能会有所帮助:

literal operator"" _l( char const* p, std::size_t l ) 
    return literal_t( p, p + l ); // l excludes '\0'


for (auto e : "hello"_l) ...

编辑:以下开销较小 (但用户定义的文字将不可用)。

template< size_t N >
char const (&literal( char const (&x)[ N ] ))[ N - 1 ] 
    return (char const(&)[ N - 1 ]) x;


for (auto e : literal("hello")) ...

【讨论】:

我有一个文字实现:std::string。使用手头的工具。每个人都知道 C 字符串有一个终止 NULL。 感谢您的指出。尽管上述方式可能会简化用户定义的文字,但它有开销,并且似乎比std::string 没有太多优势。我应该提到一个数组的明显方式。我编辑了答案。【参考方案5】:

如果你想要长度,你应该使用 strlen() 来表示 C 字符串,.length() 来表示 C++ 字符串。您不能对 C 字符串和 C++ 字符串一视同仁——它们有不同的行为。

【讨论】:

问题与更新后的 C++ 标准 (C++0x) 如何定义 for (auto e: someexp) 以及当表达式是字符串而不是字符数组或 std:: 时有何不同有关字符串——因此它与strlen 或获取长度的正确方法无关。 @Soren 原发帖人明确指出长度是他认为这种行为错误的原因之一。【参考方案6】:

可以使用 C++0x 工具箱中的另一个工具解决不一致问题:用户定义的文字。使用适当定义的用户定义文字:

std::string operator""s(const char* p, size_t n)

    return string(p, n);

我们可以写:

int i = 0;     
for (auto e : "hello"s)         
    ++i;     
std::cout << "Number of elements: " << i << '\n';

现在输出预期的数字:

Number of elements: 5

有了这些新的 std::string 文字,可以说再也没有理由使用 C 风格的字符串文字了。

【讨论】:

注意:用户定义的文字必须以下划线开头。此外,另一个答案已经建议使用文字 - 为什么不接受那个?

以上是关于std::string 和字符串文字之间的不一致的主要内容,如果未能解决你的问题,请参考以下文章

C# 中插入符号位置、字符串长度和匹配索引的不一致

字符串文字匹配 bool 重载而不是 std::string

不区分大小写的 std::string.find()

不区分大小写的 std::string.find()

函数模板和“正常”函数之间奇怪的不一致

SwiftUI - 文本和形状位置之间的不一致