【问题标题】:Why aren't string literals passed as references to arrays instead of opaque pointers?为什么不将字符串文字作为对数组的引用而不是不透明的指针传递?
【发布时间】:2014-10-31 06:57:51
【问题描述】:

在 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!" );

正如this answer 中所解释的,数组衰减为指针,但它们的维度被带走了。在字符串文字的情况下,这意味着它们的长度(在编译时已知)会丢失,并且必须在 运行时通过遍历指向的内存直到找到零字节来重新计算。这不是最优的。

但是,字符串字面量,通常是数组,可以使用模板参数推导作为引用传递以保持其大小:

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 没有带有建议签名的构造函数?


注意:我不知道建议的参数是如何命名的;如果您知道如何,请建议问题标题的版本。

【问题讨论】:

  • std::string 将其作为参数将引入 char arr[100]; fillFirst20Chars(arr); std::string s(arr); 的实例。
  • @chris 很抱歉,但我不明白你的意思。您的意思是创建的std::string 会太长吗?如果是这样,可以使用std::string s(arr, 20); 来缓解这种情况。
  • 我想问题是std:: string 的实现者和用户假设字符串中的所有字符都是非空的,并且字符串后面的第一个字符是空的。但是,如果某些开发人员粗心,这种假设将是不正确的。我想这可以通过确定行为未定义来轻松解决,因为关于 \0 的位置的假设被打破了。
  • 在 Library Fundamentals TS 中查找 string_view
  • @Kalrish,是的,你可以,但是 a) 现有代码没有,并且 b) 它很可能是意外且未被检测到的。

标签: c++ string c++11 c++14


【解决方案1】:

从某种意义上说,这主要是历史性的。虽然您是正确的,但没有真正的理由不能这样做(如果您不想使用整个缓冲区,请传递一个长度参数,对吗?)如果您有一个字符数组,它仍然是正确的 通常一个缓冲区,不是你在任何时候都在使用的:

char buf[MAX_LEN];

由于这是通常它们的使用方式,因此为@987654323 添加新的basic_string 构造函数模板似乎没有必要甚至冒险 @。

不过,整个事情都非常边缘化。

【讨论】:

    【解决方案2】:

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

    每当使用char-type 的静态缓冲区调用函数时都会使用它,即使缓冲区作为一个整体不是一个字符串,并且您真的只想传递初始字符串(嵌入零远不如终止零常见,并且使用缓冲区的一部分很常见):当前代码很少包含从数组到指向 first 的指针的显式衰减元素,使用强制转换或函数调用。

    演示代码(On 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,这是计数的。

    【讨论】:

    • 虽然您的观点(有时通过插入零字节来剪切缓冲区)是正确的,但根据缓冲区的内容来表示它们的大小是为(堆栈)溢出之神提供的牺牲品.如果您已经知道要通过的区域的大小,为什么不明确说明呢?
    • 你为什么认为你知道它?您将缓冲区传递给一个函数,该函数填充了一个字符串,您可以进一步处理该字符串。直到最后一步,您才知道字符串的长度,只知道缓冲区的长度。另外,我只是在中间的某个地方设置了那个 0 字节来演示,这并不意味着它完全是这样做的。
    • 许多函数返回写入的字节数(例如,参见en.cppreference.com/w/cpp/io/c/fprintf),即使没有,仍然建议扫描零字节:你知道缓冲区的大小,所以你知道什么时候停止,但是你传递给它的一个简单的函数不会(它只是接收一个指针),并且可能会超出范围。
    • 好吧,你自然应该知道被调用函数的合约(并且还可以利用任何辅助信息):那么,它是否保证成功时 0 终止,或者它是一个废话?
    • 我一直对+buffer的语法摸不着头脑。该运算符称为“一元加法”,除其他外,还可以执行数组到指针的衰减。在en.cppreference.com/w/cpp/language/operator_arithmetic 中搜索“一元算术运算符”。
    【解决方案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"
    }
    

    【讨论】:

      【解决方案4】:

      您可以创建帮助类来解决这个问题,而无需对每个函数都使用重载

      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");
      }
      

      您应该将此类扩展为辅助函数(如begineend)以简化程序中的使用。

      【讨论】:

      • 这是库基础 TS 中正在考虑的 string_view 类的一个有点幼稚的版本(感谢 Kerrek SB 指出这一点,这正是我所要求的!)。
      • @kalrish 你可以让这个幼稚的版本更通用一点,就像array_view 一样(支持std::arraystd::stringstd::vector 输入以及原始C 数组)。通常我给它一个 beginend 指针而不是 ptrsize 我自己。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-07-11
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多