用户定义文字的每个"normal"使用都是未定义的行为吗?

Is every "normal" use of user-defined literals undefined behavior?

本文关键字:未定义 文字 定义 用户 normal      更新时间:2023-10-16

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

这是一条或多或少广为人知的规则,你可以在每一个谈论用户文字的外行网站上找到它。这也是一条规则,从那以后,我(可能还有其他人?)一直在"胡说八道"的基础上公然无视这条规则。当然,这绝对是不对的。从最严格的意义上讲,这使用了一个保留的标识符,从而调用了Undefined Behavior(尽管实际上编译器不会对此不屑一顾)。

因此,在思考我是否应该继续故意忽略标准中的这一部分(在我看来是无用的)时,我决定看看实际写的是什么。因为,你知道,重要的是什么每个人都知道。重要的是标准中写了什么。

[over.literal]声明保留"某些"文字后缀标识符,链接到[usrlit.suffix]。后者表示所有都是保留的,以下划线开头的除外。好吧,这几乎正是我们已经知道的,明确地写的(或者更确切地说,向后写的)。

此外,[over.literal]包含一个注释,它暗示了一个明显但令人不安的事情:

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

当然是。没有任何地方说它们不是,那么你还能指望它们是什么呢?

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

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

您是否确实希望将用户定义的文字放入命名空间,因为"正常"用法(下划线或非下划线)使用的是保留名称?

是:禁止使用_作为全局标识符的开头,再加上要求非标准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),用户可以提前了解他们想要在代码的哪些部分中使用哪些文字集。

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

显然不是。

以下是UDL的惯用用法(因此肯定是"正常"的),它是根据您刚才列出的规则定义的:

namespace si {
struct metre { … };
constexpr metre operator ""_m(long double value) { return metre{value}; }
}

您列出了有问题的案例,我同意您对其有效性的评估,但它们在惯用C++代码中很容易避免,所以我不完全认为当前的措辞有问题,即使这可能是偶然的。

根据[over.tliteral]/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名称之间的空白变得重要。

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

[lex.name]/3.2读取:

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

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

"每个以下划线开头的标识符都保留给实现,并且实现可以在全局名称空间中使用这些标识符作为名称">

而是

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

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

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

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

我自己没有遇到过这种情况,因为我试图遵循规则:

不要在全局名称空间中放置任何东西(除了main、名称空间和用于ABI稳定性的extern "C"之外)。

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库处理文字的方式,因此您的代码用户应该很熟悉。

给定后缀为_X的文字,语法将_X称为"标识符"。

因此,是的:该标准可能无意中使其无法在定义明确的程序中创建全球范围的UDT,或以大写字母开头的UDT。(注意,前者不是你通常想做的事情!)

这无法通过编辑解决:用户定义的文字的名称必须有自己的词法"命名空间",以防止与(例如)实现提供的函数的名称发生冲突。不过,在我看来,如果在某个地方有一份非规范性的说明,指出这些规则的后果,并指出它们是经过深思熟虑的,那就太好了。