为什么字符串字面量不被作为数组的引用而是作为不透明指针传递?

4
在C++中,字符串字面值的类型是const char [N],其中N作为std::size_t表示字符数加一(零字节终止符)。它们驻留在静态存储区,并从程序初始化到终止都可用。
通常,接受常量字符串的函数不需要std::basic_string的接口,或者更愿意避免动态分配;它们可能只需要字符串本身及其长度。特别地,std::basic_string提供了一种从语言本身的字符串字面值构建对象的方法。此类函数提供了一个接受C风格字符串的变体:
void function_that_takes_a_constant_string ( const char * /*const*/ s );

// Array-to-pointer decay happens, and takes away the string's length
function_that_takes_a_constant_string( "Hello, World!" );

这个答案所解释的那样,数组会退化为指针,但它们的维度会被去除。对于字符串字面量来说,这意味着它们在编译时已知的长度会丢失,必须通过迭代指向内存,直到找到一个零字节来重新计算它们的长度在运行时。 这不是最优的。
然而,使用模板参数推导将字符串字面量和一般的数组作为引用传递,可以保留它们的大小。
template<std::size_t N>
void function_that_takes_a_constant_string ( const char (& s)[N] );

// Transparent, and the string's length is kept
function_that_takes_a_constant_string( "Hello, World!" );

模板函数可以作为另一个函数的代理,真正的函数将接受指向字符串和其长度的指针,以避免代码暴露并保持长度不变。
// Calling the wrapped function directly would be cumbersome.
// This wrapper is transparent and preserves the string's length.
template<std::size_t N> inline auto
function_that_takes_a_constant_string
( const char (& s)[N] )
{
    // `s` decays to a pointer
    // `N-1` is the length of the string
    return function_that_takes_a_constant_string_private_impl( s , N-1 );
}

// Isn't everyone happy now?
function_that_takes_a_constant_string( "Hello, World!" );

为什么这种方法没有更广泛的应用?特别是,为什么std::basic_string没有使用建议签名的构造函数?
注意:我不知道建议参数的名称;如果您知道,请建议修改问题标题。

2
如果将std::string作为参数,会引入char arr[100]; fillFirst20Chars(arr); std::string s(arr);的实例。 - chris
1
@chris 对不起,我不明白你的意思。你是说创建的 std::string 太长了吗?如果是这样,可以使用 std::string s(arr, 20); 来缓解。 - djsp
1
我猜问题在于std::string的实现者和使用者都假设字符串中的所有字符都是非空的,并且字符串后的第一个字符为空。但如果一些开发人员粗心大意,这种假设就是错误的。我猜可以很容易地解决这个问题,即确定当\0的放置假设被打破时,行为是未定义的。 - Aaron McDaid
4
请在Library Fundamentals TS中寻找string_view - Kerrek SB
@Kalrish,是的,你可以这样做,但是a)现有代码不支持,b)这可能是意外和未检测到的。 - chris
4个回答

3

添加这样一个模板化的重载函数的问题很简单:

每当使用静态char类型的缓冲区调用该函数时,它都会被使用,即使缓冲区不是整体上的字符串,而你真正想传递的只是初始字符串(嵌入零比终止零要少得多,而使用缓冲区的一部分非常普遍):当前代码很少包含从数组到指向第一个元素的指针的显式衰减,使用强制转换或函数调用。

演示代码 (在coliru上)

#include <stdio.h>
#include <string.h>

auto f(const char* s, size_t n) {
    printf("char* size_t %u\n", (unsigned)n);
    (void)s;
}
auto f(const char* s) {
    printf("char*\n");
    return f(s, strlen(s));
}
template<size_t N> inline auto
f( const char (& s)[N] ) {
    printf("char[&u]\n");
    return f(s, N-1);
}

int main() {
    char buffer[] = "Hello World";
    f(buffer);
    f(+buffer);
    buffer[5] = 0;
    f(buffer);
    f(+buffer);
}

记住:如果你在C语言中谈论字符串,它总是表示一个以0结尾的字符串,而在C++中它也可以表示一个计数的std::string

虽然您提到的(有时通过插入零字节来截断缓冲区)是正确的,但是依靠缓冲区内容来表示其大小就像是在向(堆栈)溢出的神明献祭自己一样。如果您已经知道要传递的区域的大小,为什么不明确说明呢? - djsp
你为什么认为你知道它?你将一个缓冲区传递给一个函数,该函数填充了一个字符串,然后您进一步处理该字符串。直到最后一步,您才知道字符串的长度,而不是缓冲区的长度。此外,我只是在中间某个地方设置了0字节,以证明这并不意味着所有操作都是这样完成的。 - Deduplicator
许多函数返回写入的字节数(例如请参见http://en.cppreference.com/w/cpp/io/c/fprintf),即使它们没有返回,仍建议扫描零字节:因为您知道缓冲区的大小,所以您知道何时停止,但是一个天真的函数(您传递给它)不知道(它只接收指针),可能会越界。 - djsp
你应该自然而然地了解被调用函数的契约(并利用任何辅助信息):那么,它是否保证成功时的0终止,还是随机的? - Deduplicator
我一直在为语法+buffer苦恼。这个运算符被称为“一元加号”,可以执行数组到指针的转换,以及其他操作。请在http://en.cppreference.com/w/cpp/language/operator_arithmetic中搜索“一元算术运算符”。 - djsp

3

从某种意义上来说,这在很大程度上是历史的遗留问题。虽然你是对的,没有什么实质性的原因不能这样做(如果你不想使用整个缓冲区,请传递一个长度参数,对吧?),但是事实仍然如此,如果你有一个字符数组,通常情况下,它是一个缓冲区,并非你随时都在使用:

char buf[MAX_LEN];

由于这通常是它们的使用方式,因此添加一个新的basic_string构造函数模板来处理const CharT(&)[N]似乎是不必要的甚至是有风险的。

整个事情都很边缘化。


3
我相信这个问题正在被解决,C++14将基于用户定义字符串字面量进行构建。 http://en.cppreference.com/w/cpp/string/basic_string/operator%22%22s
#include <string>

int main()
{
    //no need to write 'using namespace std::literals::string_literals'
    using namespace std::string_literals;

    std::string s2 = "abc\0\0def"; // forms the string "abc"
    std::string s1 = "abc\0\0def"s; // form the string "abc\0\0def"
}

0

您可以创建一个辅助类,而无需为每个函数使用重载来解决这个问题。

struct string_view
{
    const char* ptr;
    size_t size;
    template<size_t N>
    string_view(const char (&s)[N])
    {
        ptr = s;
        size = N;
    }
    string_view(const std::string& s)
    {
        ptr = s.data();
        size = s.size() + 1; // for '\0' at end
    }
};
void f(string_view);
main()
{
    string_view s { "Hello world!" };
    f("test");
}

你应该扩展这个类以添加辅助函数(例如beginend),以简化程序中的使用。


这是一个相对简单的string_view类版本,正在图书馆基础TS中考虑(感谢Kerrek SB指出,这正是我所要求的!)。 - djsp
@kalrish,你可以将这个简单版本改成更通用的array_view(支持std::arraystd::stringstd::vector输入以及原始C数组)。通常我会自己给它一个beginend指针,而不是一个ptrsize - Yakk - Adam Nevraumont

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