std::string和字符串字面值之间的不一致性

38

我发现了C++0x中std::string和字符串字面量之间的一个令人不安的不一致性:

#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

我理解这种情况发生的原因:字符串字面值实际上是一个包括空字符在内的字符数组,当使用range-based 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"))执行不同的操作吗?


6
这是一个真正的问题吗?它更像是对标准应该是什么的个人看法,而不是现实是什么样子。 - Gene Bushuyev
@JAB:那么,字符串字面值到底有什么问题,需要再添加一个内置类型呢? - Gene Bushuyev
1
@Gene:为什么C语言要实现布尔类型,整数类型完全可以胜任这个任务? - JAB
1
@JAB:在C++0x中,您将能够通过用户定义的字面量为std::string创建新的字符串字面量语法。 - HighCommander4
@HighCommander4:嗯,那很不错。 - JAB
显示剩余2条评论
6个回答

29
如果我们为 const char 数组重载 std::begin() 和 std::end(),让它们返回数组大小减一的值,那么以下代码将输出 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';
}

3
也许有一种方法可以区分以字符串字面值定义的字符数组和普通定义的字符数组?我们只想为前者重载。 - HighCommander4
1
它不一定会破坏代码……它可以保持类型不变,只需让编译器记住字符数组的来源,并通过内置函数(如__is_string_literal(char_array))报告它。但是,在库中实现这个功能会更好…… - HighCommander4
9
任何解决方案都需要考虑如何处理 const char s[6] = {'h', 'e', 'l', 'l', 'o', '\0'};。我在这里支持Howard的观点,C++程序员应该知道 sizeof("Hello")==6 - MSalters
2
@HighCommander4:我用 sizeof("Hello")==6 作为一个快速的方法在C和C++中编写,字符串字面值是带有长度N+1的常量字符数组,包括终止符\0。编译器不需要,也可能不区分这两个,在它们执行参数重载时。这意味着你会因为一个小特性而迫使编译器进行重大重新设计。 - MSalters
2
我刚意识到情况比那还要糟糕。一个翻译单元可以定义 char const s[6]="Hello";,而另一个则可以调用 end(s)-begin(s)。这意味着字符串字面值和字符串数组之间的差异需要进行ABI更改。抱歉,这是不可能发生的。 - MSalters
显示剩余7条评论

22

然而,我认为这非常不可取:当涉及到像长度这样基本的属性时,std::string和字符串字面量应该表现一致吧?

字符串字面量在定义时会在字符串末尾(隐藏地)添加一个空字符。而 std::string 则没有这个规定。由于 std::string 有一个长度,因此那个空字符有点多余。关于字符串库的标准部分明确允许使用非以 null 结尾的字符串。

编辑
我想在回答问题上,这可能是我给出的最具争议性的答案之一,一方面获得了大量的赞成,另一方面也收到了大量的反对意见。

auto 迭代器应用于 C 风格数组时,它会迭代数组的每个元素。范围的确定是在编译时而不是运行时进行的。例如,以下代码是不合法的:

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

有些人使用char类型的数组来存储任意数据。是的,这是一种旧式的C方式思考,也许他们应该使用C++风格的std::array,但这种构造非常有效和有用。如果一个auto迭代器遍历一个char buffer[1024];数组时停在第15个元素,只因为那个元素恰好具有null字符的值,那么这些人会感到相当沮丧。而对于一个Type buffer[1024];数组,auto迭代器会一直运行到最后。什么使得char数组如此值得拥有完全不同的实现呢?

请注意,如果您想要让auto迭代器在字符数组上提前停止,有一种简单的机制可以实现:在循环体内添加一个if (c == '0') break;语句。

底线:这里没有任何不一致之处。char[]数组上的auto迭代器与任何其他C风格数组上的auto迭代器的工作方式是一致的。


6
这个回答只是重复了提问者在问题中所说的话,完全没有回答问题(请参见最后一段)。 - BlueRaja - Danny Pflughoeft

19

在第一种情况下得到的是6,这是在C语言中无法避免的抽象泄漏。 std::string“修复”了这个问题。为了兼容性,C++中的C风格字符串字面值的行为不会改变。

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

假设通过指针访问(而不是char[N]),唯一的方法是将一个变量嵌入到字符串中以包含字符数量,这样就不再需要寻找NULL了。糟糕!这就是std::string的用途。

“解决不一致性”的方法是根本不使用旧特性


6
"不要使用任何遗留功能。" 不使用字符串字面量似乎是一项困难的任务(而且必须记住字符串字面量是一种“遗留”功能也可能很困难)。 - Suma
@Suma:嗯,我说的是传递char const*char[N]。字符串字面量本身当然仍然是完全合理的。不可否认,OP在他的问题中使用了字符串字面量;我猜for (auto c : "literal")确实有点棘手。无论如何,std::string确实是解决OP不喜欢的行为的“修复”方法。 - Lightness Races in Orbit

6
根据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。 - emsr
感谢您指出。 虽然上述方法可能会给用户定义的文字提供简洁性, 但它有额外的开销,似乎没有比 std::string 更多的优势。 我应该提及一种使用数组的显而易见的方法。我已经编辑了答案。 - Ise Wisteria

4

如果您需要获取长度,对于C字符串应使用strlen(),对于C++字符串应使用.length()。不能将C字符串和C++字符串视为相同--它们具有不同的行为。


1
这个问题与更新的C++标准(C++0x)如何定义 for (auto e: someexp) {} 有关,以及当表达式是字符串文字而不是char数组或std :: string时的区别 - 因此它与 strlen 或获取长度的正确方法无关。 - Soren
@Soren,原帖作者明确指出长度是他认为这种行为不正确的原因之一。 - robert

3

不一致性可以通过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风格的字符串字面量了。


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

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