c++模板中使用尖括号会带来哪些语法问题?

What are all the syntax problems introduced by the usage of angle brackets in C++ templates?

本文关键字:语法 问题 c++      更新时间:2023-10-16

在c++中,模板是用尖括号vector<int>实例化的,Java和c#语言对它们的泛型采用了相同的语法。

D语言的创建者对尖括号带来的问题直言不讳,他们创造了一个新的语法foo!(int) —但我从来没有见过太多关于尖括号带来的问题的细节。

其中之一是当用另一个模板vector<vector<int>>实例化模板时,这会导致一些(旧的?)编译器将尾部的'>> '与位移位或流操作符混淆。解决方案是在两个尖括号之间插入一个空格,但是现在编译器还不能解析这种语法吗?

另一个问题是当使用大于运算符foo<3 > 2>时。解析器会认为操作符实际上关闭了模板实例化;修复方法是引入括号foo<(3 > 2)>。但我不认为在很多情况下你需要这样做,无论如何,我宁愿在需要的时候输入额外的括号,而不是引入新的语法和总是不得不输入感叹号。

还有什么问题,使D开发人员创建一个新的语法尖括号?

就我个人而言,我见过的最可怕的问题是在依赖上下文中调用模板函数:

template <typename T>
void foo(T t) {
  t.bar<3>();
}

这看起来很简单,但实际上是不正确的。c++标准要求引入template关键字来消除t.bar < 3与方法调用的歧义,产生:

t.template bar<3>(); // iirk

litb发表了一些关于编译器可能给出的解释的非常有趣的帖子。

关于>>问题,它在c++ 0x中得到了修复,但需要更聪明的编译器。

但是编译器现在还不能解析该语法吗?

当然

。但这绝不是微不足道的。特别是,它使您无法在上下文无关的词法分析器和解析器之间实现清晰的分离。对于语法高亮显示和其他需要解析c++的支持工具来说,这尤其令人讨厌,但不想/不能实现一个完全成熟的语法分析器。

这使得c++很难解析,以至于很多工具都懒得解析。这是生态系统的净损失。换句话说:它使开发解析工具的成本大大增加。

例如,ctags在某些模板定义中失败,这使得它无法在当前的c++项目中使用。很烦人。

但是我不认为在很多情况下你需要[区分尖括号和小于]

你需要多长时间这样做并不重要。解析器仍然需要处理这个

D决定放弃角度背球是不需要动脑筋的。考虑到这是一个净收益,任何一个理由都足够了。

问题是使语言语法与上下文无关。当一个程序被词法分析器标记时,它使用一种称为maximum munch的技术,这意味着它总是使用可能指定标记的最长字符串。这意味着>>被视为右位移操作符。因此,如果您有类似vector<pair<int, int>>的内容,则末尾的>>被视为右位移操作符,而不是模板实例化的一部分。为了使它在此上下文中以不同的方式对待>>,它必须是上下文敏感的而不是上下文自由的—也就是说,它必须实际关心正在解析的令牌的上下文。这使词法分析器和解析器相当复杂。词法分析器和解析器越复杂,出现错误的风险就越高——更重要的是,工具实现它们就越困难,这意味着工具越少。当像IDE或代码编辑器中的语法高亮这样的东西变得复杂时,这是一个问题。

通过使用!()(对于相同的声明会导致vector!(pair!(int, int))), D避免了上下文敏感性问题。D在其语法中明确地做出了许多这样的选择,目的是使工具在需要时更容易地实现词法分析或解析,以便完成它们所做的工作。因为在模板中使用!()并没有什么不好的地方,除了对于那些在使用<>的其他语言中使用模板或泛型的程序员来说有点陌生之外,这是一个合理的语言设计选择。

当使用尖括号语法时,你使用或不使用模板的频率会产生歧义-例如vector<pair<int, int>> -与语言无关。无论如何,工具必须实现它。决定使用!()而不是<>完全是为了简化工具语言,而不是为了程序员。虽然您可能特别喜欢!()语法,也可能不是特别喜欢,但它非常容易使用,因此除了学习它之外,它最终不会给程序员带来任何问题,而且它可能违背他们的个人偏好。

在c++中另一个问题是预处理器不理解尖括号,所以这失败了:

#define FOO(X) typename something<X>::type
FOO(std::map<int, int>)

问题是预处理器认为FOO被调用时带有两个参数:std::map<intint>。这是一个更广泛的问题的例子,即符号是操作符还是括号通常是不明确的。

看看这是怎么回事:

bool b = A< B>::C == D<E >::F();
bool b = A<B>::C == D<E>::F();

上次我检查过了,你可以通过改变作用域中的内容来使它解析。

使用<>作为匹配和非匹配令牌是一场灾难。至于!()使D的使用更长:对于具有单个参数的常见情况,()是可选的,例如:

Set!int foo;

我相信这是唯一的情况。

然而,不是用户问题,而是实现者问题。这个看似微不足道的差异使得更难以为c++构建正确的解析器(与D相比)。D也被设计为对实现友好,因此他们尽最大努力避免产生歧义代码。

(旁注:我确实发现shift-感叹号组合有点尴尬…尖括号的一个优点是很容易输入!)

>=大于或等于歧义是另一个没有提到的情况:

失败:

template <int>
using A = int;
void f(A<0>=0);

:

void f(A<0> =0);

我认为这在c++ 11中没有像>>那样改变。

参见这个问题了解更多细节:为什么"A<0>=0"由于大于或等于比运算符&;>=&;?

最终,任何编译器要做的就是把你的半英文源代码——不管用什么语言——翻译成计算机可以实际操作的真正的机器码。这最终是一系列难以置信的复杂数学变换。

嗯,数学告诉我们编译需要的映射是"映上"或"满射"。这意味着每一个合法的程序都可以毫不含糊地映射到汇编程序。这就是语言关键字和像";"这样的标点符号存在的原因,也是每种语言都有它们的原因。然而,像c++这样的语言对多个事物使用相同的符号,如"{}"answers"<>",因此编译器必须添加额外的步骤来生成它需要的整体转换(这就是您在线性代数中对矩阵进行乘法时所做的事情)。这增加了编译时间,引入了显著的复杂性,其本身可能包含bug,并且可能限制编译器优化输出的能力。

例如,Strousoup可以使用'@'作为模板参数——这是一个未使用的字符,它可以完美地让编译器知道"这是,而且只会是某种模板"。这实际上是一个1到1的变换,对于分析工具来说是完美的。但他没有;他使用了已经映射到大于和小于的符号。仅这一点就会立即引入歧义,并且只会变得更糟。

听起来像"D"决定将序列"!()"作为一个特殊的符号,仅用于模板,就像上面的"@"示例一样。我愿意猜测,其高度模板化的代码编译速度更快,bug也更少。