为什么允许字符串字面量的连接?

14

最近我被一个微妙的错误所困扰。

char ** int2str = {
   "zero", // 0
   "one",  // 1
   "two"   // 2
   "three",// 3
   nullptr };

assert( int2str[1] == std::string("one") ); // passes
assert( int2str[2] == std::string("two") ); // fails

如果您拥有神一般的代码审查能力,您会注意到我在"two"后面忘记了,

在花费了相当大的努力找到这个错误之后,我不得不问:为什么有人会想要这种行为呢?

我可以看出这可能对于宏魔法很有用,但是为什么这是Python这样一个现代语言的"特性"呢?

您曾经在生产代码中使用过字符串字面量连接吗?


3
我遇到了类似的问题,但我的数字分布在不同的行上,缺失逗号后的那一行有一个负号,因此我没有收到编译错误。 - Jared Updike
我猜测您在谈论C++0x的用户定义字面量:http://public.research.att.com/~bs/C++0xFAQ.html#UD-literals - Georg Fritzsche
同样的精神,如果您在字符串字面值后忘记了“_s”,会发生什么? - visitor
我能够挖掘出 C 和 C++ 以及 Python 中这个特性的实际原理,详情请见下面我的回答。 - Shafik Yaghmour
10个回答

23

当然,这是使你的代码看起来好看的简单方式:

char *someGlobalString = "very long "
                         "so broken "
                         "onto multiple "
                         "lines";

最好的原因是为了奇怪的printf格式,比如类型强制:

uint64_t num = 5;
printf("Here is a number:  %"PRIX64", what do you think of that?", num);

有很多这样的类型已经定义好了,如果你有类型大小的要求,它们可能会派上用场。请查看此链接来了解全部内容。以下是一些示例:

PRIo8 PRIoLEAST16 PRIoFAST32 PRIoMAX PRIoPTR

这解释了为什么它存在于C/C++中,但实际上表明在Python中类似的想法是不可能的。 - Ken Williams

17

这是一个很棒的功能,它允许您将预处理器字符串与您的字符串组合在一起。

// Here we define the correct printf modifier for time_t
#ifdef TIME_T_LONG
    #define TIME_T_MOD "l"
#elif defined(TIME_T_LONG_LONG)
    #define TIME_T_MOD "ll"
#else
    #define TIME_T_MOD ""
#endif

// And he we merge the modifier into the rest of our format string
printf("time is %" TIME_T_MOD "u\n", time(0));

1
@STingRaySC,那PRIx32或者PRIuLEAST32等相关内容怎么处理呢? http://www.opengroup.org/onlinepubs/9699919799/basedefs/inttypes.h.html - Carl Norum
2
@STingRaySC - 虽然我同意在C++中有更好的方法来做到这一点,但他的问题也被标记为C(在那里这非常有用)。 - R Samuel Klatchko
2
printf(以及其它类似函数)的格式字符串为什么要是编译时常量,这其中有一个非常重要的原因——编译器可以告诉你参数类型是否与格式字符串匹配。 - caf
1
@STingRaySC:可能并不是必要的,但这是一种常见的用法。我想看到一个指向简单替代解决方案示例的指针,该解决方案不使用预处理器进行比较。 - Michael Burr
2
@STingRaySC - printf不是作为单个库调用实现的,它可以被优化为一系列特定于该字符串中包含的格式的调用,这就是为什么它只需要一个常量字符串作为其第一个参数!如果您正在编译微型嵌入式平台,不需要具有大量代码的全能打印所有内容功能,那么这将是一个巨大的胜利(请记住,嵌入式空间是C仍然占主导地位的市场之一,因此有很多人认为这很重要)。 - Charles Duffy
显示剩余11条评论

5

这种技术的使用场景包括:

  • 生成包含预处理器定义组件的字符串(这可能是C语言中最常见的用例之一,我经常看到这种情况)。
  • 将字符串常量分成多行

为了更具体地说明前者,可以举个例子:

// in version.h
#define MYPROG_NAME "FOO"
#define MYPROG_VERSION "0.1.2"

// in main.c
puts("Welcome to " MYPROG_NAME " version " MYPROG_VERSION ".");

5
我看到了几个有关C和C++的答案,但它们没有真正回答“为什么”或者这个特性的理由是什么。在C++中,这种特性来自于C99,我们可以通过去查看“Programming Languages—C”的第6.4.5节“字符串文字”的理由来找到这个特性(重点是我的):

可以使用反斜杠-换行符行继续将字符串延续到多行,但这要求字符串的继续必须从下一行的第一个位置开始。为了允许更灵活的布局,并解决一些预处理问题(见§6.10.3),C89委员会引入了字符串字面值连接。两个连续的字符串字面值被粘贴在一起,中间没有空字符,组成一个组合的字符串字面值。这个添加到C语言中的特性允许程序员在不使用反斜杠-换行机制并破坏程序的缩进方案的情况下扩展字符串字面值超出物理行的末尾。因为连接是词法结构而不是运行时操作,所以没有引入显式的连接运算符。

像Python一样,它似乎有同样的原因,这减少了使用丑陋的\继续长字符串文字的需要。这在Python语言参考的2.4.2字符串文字连接一节中有所涉及。

这似乎是真正的原因:“没有引入显式连接运算符,因为连接是一个词法结构而不是运行时操作。”其他人只是效仿而已。 - deft_code
如果你需要在C语言中表示大量文本,这将非常有用。例如,CLI程序的冗长的“使用”消息,或者如果你不幸地在C语言中编写CGI程序。 - Brian McFarland

3

我不确定其他编程语言是否允许这样做,但例如C#是不允许的(我认为这是一件好事)。据我所知,大多数展示C++中此方法有用的例子,如果你可以使用一些特殊的运算符进行字符串连接,仍然可以工作:

string someGlobalString = "very long " +
                          "so broken " +
                          "onto multiple " +
                          "lines"; 

这样做可能不太舒适,但肯定更安全。在你举的例子中,除非添加,来分隔元素或者+来连接字符串,否则代码将无效...


那是无效的。在编译之前,这些字符串中至少有一个必须转换为std :: string。此外,该问题标记为C。 - Billy ONeal
@BillyONeal:这个问题标记了Python/C++,并且问为什么“像Python这样的现代语言”允许这样做,所以我想发表一个反例。我想展示的是,你不需要这个特性(通常情况下)来支持诸如换行和宏扩展之类的东西。 - Tomas Petricek
这是一个有用的答案 - 它展示了为什么在 Python 中它是一个不必要的问题,会导致问题,并且(在我看来)实际上是一个错误的特性。 - Ken Williams
你为什么要在C#中这样做,而不是使用@"very long so broken onto multiple lines",或者更进一步允许插值并使用$@"very long so broken onto multiple lines"呢?我的方法允许用户直接从SSMS中将他们的SQL复制到一个空字符串中,并忘记它。无需逐行添加,等等。 - Krausladen
花了我一些时间才理解这个12年前的问题 :) 当然,今天你可能不会在C#中这样做(除非你想避免字符串中的换行符?)但问题是,为什么你可以在C++中这样做而不需要显式的“+” - 这是C#已经消除的东西。 - Tomas Petricek

3

来自Python词法分析参考文献第2.4.2节:

该功能可用于减少所需反斜杠数量,方便地跨越长行方便地拆分长字符串,甚至可以向字符串的部分添加注释。


一个 ''' 字符串不是也能达到同样的效果吗? - deft_code
@Caspin - 原始字符串(r'',或三引号)将包含所有换行符和空格。 单独的字符串文本只会被连接起来。 - JimB

2

为了解释,扩展和简化Shafik Yaghmour的答案:字符串字面值连接起源于C语言(因此被C++继承),同样也是由于两个原因产生的(参考自ANSI C编程语言的原理):

  • 对于格式化:允许长字符串字面值跨越多行并保持适当的缩进 - 这与行续行符相反,后者会破坏缩进方案(3.1.4字符串字面值);以及
  • 对于宏魔术:通过宏(通过字符串化)构造字符串字面值(3.8.3.2 #操作符)。
它被包含在现代编程语言Python和D中,因为它们从C语言中复制了它。尽管在这两种语言中都已经提议废弃它,因为它容易出错(正如您所指出的)并且不必要(因为可以使用连接运算符和常量折叠进行编译时评估;在C语言中无法做到这一点,因为字符串是指针,所以无法将它们相加)。
由于这会破坏兼容性,并且需要小心处理优先级(隐式连接发生在词法分析期间,在运算符之前,但用运算符替换它意味着您需要小心处理优先级),因此删除它并不简单,这就是为什么它仍然存在的原因。
是的,它在生产代码中得到了应用。Google Python Style Guide行长度 指定:

当一个字面上的字符串不能放在一行上时,使用括号进行隐式行连接。

x = ('This will build a very long long '
     'long long long long long long string')

请参阅维基百科上的“字符串字面值连接”获取更多详细信息和参考资料。

很好的观点,常量折叠会消除所有优势。如果仅限于字符串字面值,即使在C/C++中也可以使用。 - deft_code
谢谢! 字符串文字可以特殊处理,但这也是一种hack,并且以自己的方式令人困惑:为什么 "foo" + "bar" 可以工作,但是 string s ="bar"; "foo" + s不能工作呢?我认为推理是字符串字面值明确是类型为char [](C)/const char[](C++)。然而,在C ++14中使用新字符串标准字面(带有s后缀)是有效的: "foo"s + "bar"s是合法的,并且可以被折叠。 - Nils von Barth

2

这样你就可以将长字符串字面值跨行拆分。

而且,我在生产代码中也看到过它的使用。


1

虽然有人已经谈到了这个功能的实际用途,但是迄今为止还没有人试图为语法的选择进行辩护。

据我所知,可能会因此而漏掉的打字错误可能只是被忽视了。毕竟,正如Dennis所展示的那样,兼容打字错误显然不是他首要考虑的问题:

if (a = b);
{
    printf("%d", a);
}

此外,还有一种可能的观点认为,为了连接字符串字面量而使用额外的符号并不值得——毕竟,除此之外,它们几乎没有其他用途,并且在那里放置一个符号可能会引起尝试在运行时进行字符串连接的诱惑,这超出了 C 内置功能的范畴。
一些基于 C 语法的现代高级语言已经放弃了这种表示法,可能是因为容易出错。但是这些语言都有一个字符串连接运算符,例如 +JavaScriptC#),.PerlPHP),~D,虽然它也保留了 C 的并置语法),并且常量折叠(在编译语言中)意味着没有任何运行时性能开销。

VB.NET(以及其他语言)中用于字符串连接的运算符是& - Peter Mortensen
@PeterMortensen VB.NET 不是基于 C 语法的语言,但确实如此。我相信 VB 也支持 +。Excel 也使用 & - Stewart
继续之前的评论,我记得GW-BASIC手册中有一节关于将其他BASIC语言的程序转换的附录,告诉你将字符串连接的&改为+。当时,我从未见过其他使用&进行字符串连接的BASIC语言。你可能会问,为什么微软不直接设计GW-BASIC来支持这种语法呢?也许在当时,微软的理念是避免不必要的臃肿。而在VB中,&被重新引入。(另一方面,在QBasic中,&是一个可选的符号,我想是用于长整型?) - undefined

-2

另一个我在实际编程中经常看到的错误是人们认为两个单引号可以用来转义引号(例如,在 CSV 文件中通常用双引号),因此他们会在 Python 中写出以下语句:

print('Beggars can''t be choosers')

这段代码输出的是Beggars cant be choosers而不是程序员想要的Beggars can't be choosers

至于最初的“为什么”问题:为什么在像Python这样的现代语言中,这会成为一个“特性”?——我个人认为,我同意OP的观点;它不应该存在。


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