用户定义文字的每个"normal"使用都是未定义的行为吗?
Is every "normal" use of user-defined literals undefined behavior?
用户定义的文字必须以下划线开头。
这是一条或多或少广为人知的规则,你可以在每一个谈论用户文字的外行网站上找到它。这也是一条规则,从那以后,我(可能还有其他人?)一直在"胡说八道"的基础上公然无视这条规则。当然,这绝对是不对的。从最严格的意义上讲,这使用了一个保留的标识符,从而调用了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_literals
和std::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。(注意,前者不是你通常想做的事情!)
这无法通过编辑解决:用户定义的文字的名称必须有自己的词法"命名空间",以防止与(例如)实现提供的函数的名称发生冲突。不过,在我看来,如果在某个地方有一份非规范性的说明,指出这些规则的后果,并指出它们是经过深思熟虑的,那就太好了。
- 编译C++时未定义的引用
- vscode g++链路故障:体系结构x86_64的未定义符号
- 如何修复此错误:未定义对"距离(浮点数,浮点数,浮点数,浮点数,浮点数)"的引用
- 我的项目不会像"undefined reference to `grpc::g_core_codegen_interface'"那样使用未定义的引用错误进行编译
- 不知道某个东西是否被忽略会引入未定义的行为吗
- 对C宏的未定义引用,但在定义它时会出现重新定义错误
- 未定义的引用在哪里
- 编译时的 CImg 库返回对"__imp_SetDIBitsToDevice"的未定义引用
- 对Py_Initialize()的未定义引用
- c++11评估顺序(未定义的行为)
- 使用mysql c++连接器的未定义引用
- 从python调用openMP共享库时,未定义opnMP函数
- 在 Mac 上使用 CMAKE 将 FFTW 和 FFTWPP 链接到项目中时未定义的符号
- Cmake 链接问题:未定义对 Button::mousePressEvent(QGraphicsSceneMouseE
- 未定义的引用 .. 使用 OpenCV 编译 C++ 代码时,从命令行
- 具有外部"c"和程序集的未定义函数
- 此增量后语句是否会导致未定义的行为?
- 尝试调用 .h 文件中定义的变量时出现变量未定义错误
- 在C++中使用内联方法时出现未定义的符号错误
- 用户定义文字的每个"normal"使用都是未定义的行为吗?