用户定义文字的每次“正常”使用都会导致行为未定义吗?

时间:2019-12-04 16:15:58

标签: c++ language-lawyer

用户定义的文字必须以下划线开头。

这是一个或多或少众所周知的规则,您可以在每个谈论用户文字的外行词站点上找到。自从“胡说八道”以来,我(甚至可能是其他人)一直公然忽略了这条规则。当然,现在这绝对是不正确的。从最严格的意义上讲,它使用保留的标识符,从而调用未定义的行为(尽管实际上,编译器不会给您耸耸肩的麻烦。)

因此,考虑是否应该继续故意忽略标准的那部分(我认为无用),我决定查看实际编写的内容。因为,每个人都知道,这有什么关系。重要的是标准中写的是什么。

[over.literal]声明保留了“一些”文字后缀标识符,它们链接到[usrlit.suffix]。后者指出全部保留,但下划线开头的保留。好的,这几乎就是我们已经知道的,明确编写的(或者相反编写的)。

此外,[over.literal]包含一个 Note ,它暗示了明显但令人困扰的事情:

  

除上述约束外,它们是普通的命名空间范围函数和函数模板

好吧,确定是。它无处表明它们不是,所以您还希望它们成为什么。

但是请稍等。 [lex.name]明确指出,每个在全局命名空间中以下划线开头的标识符都是保留的。

现在,通常是文字运算符,除非您将其显式地放入全局名称空间中的名称空间(我相信没有人这样做!?)。因此,该名称(必须以下划线开头)必须保留。没有提及特殊例外。因此,每个名称(带下划线或不带下划线的名称)都是保留名称。

您是否真的希望将用户定义的文字放到命名空间中,因为“普通”用法(是否带有下划线)使用的是保留名称?

5 个答案:

答案 0 :(得分:6)

是:禁止将_用作全局标识符的开头,再加上要求非标准UDL以_开头的组合意味着您不能将它们放在全局标识符中命名空间。但是您不应该使用尤其是 UDL来弄脏全局命名空间,所以这不是什么大问题。

标准使用的传统习惯用法是将UDL放在literals命名空间中(如果您有不同的UDL集,则将它们放在该命名空间下的不同inline namespaces中) 。 literals名称空间通常位于您的主要名称空间之下。当您要使用一组特定的UDL时,可以调用using namespace my_namespace::literals或包含您选择的字面量的任何子命名空间。

这很重要,因为UDL往往大量缩写。例如,标准使用s表示std::string,但也使用std::chrono::duration秒。尽管它们确实适用于不同种类的文字(适用于字符串的s是字符串,而适用于数字的s则是持续时间),但有时阅读使用缩写文字的代码可能会造成混淆。因此,您不应该对库的所有用户都使用文字。他们应该选择使用它们。

通过为这些名称空间使用不同的命名空间(std::literals::string_literalsstd::literals::chrono_literals),用户可以预先了解他们希望在代码的哪些部分中使用哪些文字集。

答案 1 :(得分:3)

这是一个很好的问题,我不确定答案,但根据标准的特定阅读,我认为答案是“不,不是UB”。

[lex.name] /3.2读取:

  

以下划线开头的每个标识符都保留给实现,以用作全局命名空间中的名称。

现在,很显然,“作为全局命名空间中的名称”的限制应理解为适用于整个规则,而不仅仅是实现可能如何使用该名称。也就是说,它的含义不是

“每个以下划线开头的标识符都保留给实现,并且实现可以使用诸如全局名称空间中的名称之类的标识符”

但是,

“使用以下划线开头的任何标识符作为全局命名空间中的名称,都保留给实现”。

(如果我们相信第一种解释,那么这意味着没有人可以声明一个名为my_namespace::_foo的函数。)

在第二种解释下,类似于operator""_foo的全局声明(在全局范围内)是合法的,因为这样的声明不使用_foo作为名称。相反,标识符只是实际名称的一部分,operator""_foo不是以下划线开头)。

答案 2 :(得分:2)

  

用户定义文字的每次“正常”使用都是未定义的行为吗?

显然不是。

以下是UDL的惯用(因此也绝对是“正常”)使用,并且根据您刚刚列出的规则进行了明确定义:

namespace si {
    struct metre { … };

    constexpr metre operator ""_m(long double value) { return metre{value}; }
}

您已经列出了有问题的案例,我同意您对它们的有效性的评估,但是在惯用的C ++代码中很容易避免它们,因此即使当前措辞可能是偶然的,我也无法完全看到其问题。< / p>

根据[over.literal] / 8中的示例,我们甚至可以在下划线后使用大写字母:

float operator ""E(const char*);    // error: reserved literal suffix (20.5.4.3.5, 5.13.8)
double operator""_Bq(long double);  // OK: does not use the reserved identifier _Bq (5.10)
double operator"" _Bq(long double); // uses the reserved identifier _Bq (5.10)

因此,唯一有问题的事情似乎是该标准使得""与UDL名称之间的空白很重要。

答案 3 :(得分:1)

是的,在全局名称空间中定义自己的用户定义文字会导致程序格式错误。

我自己没有遇到这个问题,因为我尝试遵循以下规则:

请勿在全局名称空间中放置任何内容(除了main,名称空间和extern "C"东西以确保ABI稳定性)。

namespace Mine {
  struct meter { double value; };
  inline namespace literals {
    meter operator ""_m( double v ) { return {v}; }
  }
}

int main() {
  using namespace Mine::literals;
  std::cout << 15_m.value << "\n";
}

这也意味着即使在命名空间中也不能使用_CAPS作为文字名称。

称为literals的内联名称空间是打包用户定义的文字运算符的好方法。可以将它们导入到要使用它的位置,而不必确切命名所需的文字,或者如果导入整个命名空间,您也可以获取这些文字。

这遵循std库如何处理文字的情况,因此代码用户应该熟悉。

答案 4 :(得分:0)

给出带有后缀_Xthe grammar calls _X an "identifier"的文字。

因此,是的:大概是由于疏忽,该标准使得无法在定义良好的程序中在全局范围内创建UDT或以大写字母开头的UDT。 (请注意,前者通常不是您想做的事情!)

这不能用编辑方式解决:用户定义文字的名称必须具有自己的词法“名称空间”,以防止与(例如)实现提供的功能名称发生冲突。不过,我认为,在某处出现非规范性注释,指出这些规则的后果,并指出它们是故意的,将是很好的。