38

我发现std::stringC++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"))做一些不同的事情吗?

4

6 回答 6

29

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

#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';
}
于 2011-07-17T23:23:38.423 回答
22

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

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

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

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

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

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

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

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

于 2011-07-17T23:21:29.420 回答
19

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

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

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

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

于 2011-07-17T23:25:42.623 回答
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")) ...
于 2011-07-18T07:55:17.487 回答
4

如果你想要长度,你应该使用strlen()C 字符串和.length()C++ 字符串。您不能以相同的方式对待 C 字符串和 C++ 字符串——它们具有不同的行为。

于 2011-07-17T23:23:04.493 回答
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 风格的字符串文字了。

于 2011-07-19T04:07:01.947 回答