模板和宏中的类型 vs 非类型 vs expr vs stmt

typed vs untyped vs expr vs stmt in templates and macros

我最近一直在使用模板和宏,但我不得不说我几乎没有找到关于这些重要类型的信息。这是我粗浅的理解:

这是一个非常模糊的类型概念。我想对它们有更好的解释,包括哪些类型应该用作 return.

这些不同参数类型的目的是让您在指定编译器应该接受什么作为宏参数时更加精确。

让我们想象一个可以求解数学方程式的假设宏。它将像这样使用:

solve(x + 10 = 25) # figures out that the correct value for x is 15

这里,宏只关心提供的 AST 树的结构。它不要求同一棵树是当前范围内的有效表达式(即 x 已定义等)。该宏只是利用了已经可以解码大部分数学方程式的 Nim 解析器,将它们变成更易于处理的 AST 树。这就是 untyped 参数的用途。他们没有得到语义检查,你得到的是原始 AST。

精度阶梯的下一步是 typed 参数。它们允许我们编写一种通用的宏,它将接受任何表达式,只要它在当前范围内具有适当的含义(即可以确定其类型)。除了更早地捕获错误之外,这还有一个好处,即我们现在可以在宏体内处理表达式的类型(使用 macros.getType proc)。

我们可以通过要求特定类型的表达式(具体类型或类型 class/concept)来获得更精确的结果。宏现在可以像常规过程一样参与重载决策。重要的是要了解该宏仍将接收 AST 树,因为它将接受可以在编译时求值的表达式和只能在 运行 时求值的表达式。

最后,我们可以要求宏接收在编译时提供的特定类型的值。宏可以使用此值来参数化代码生成。这是static parameters的境界。在宏的主体内,它们不再是 AST 树,而是普通的类型良好的值。

到目前为止,我们只讨论了表达式,但 Nim 的宏也接受和生成块,这是我们可以控制的第二个轴。 expr一般表示单个表达式,而stmt表示表达式列表(历史上,它的名字来源于StatementList,在Nim统一表达式和语句之前,它作为一个单独的概念存在)。

使用 return 类型的模板最容易说明区别。考虑系统模块中的 newException 模板:

template newException*(exceptn: typedesc, message: string): expr =
  ## creates an exception object of type ``exceptn`` and sets its ``msg`` field
  ## to `message`. Returns the new exception object.
  var
    e: ref exceptn
  new(e)
  e.msg = message
  e

即使认为构建异常需要几个步骤,通过将 expr 指定为模板的 return 类型,我们告诉编译器只有最后一个表达式将被视为 return模板的值。其余语句将被内联,但巧妙地隐藏在调用代码中。

作为另一个例子,让我们定义一个特殊的赋值运算符,它可以模拟 C/C++ 的语义,允许在 if 语句中赋值:

template `:=` (a: untyped, b: typed): bool =
  var a = b
  a != nil

if f := open("foo"):
  ...

指定具体类型与使用 expr 具有相同的语义。如果我们使用默认的 stmt return 类型,编译器将不允许我们传递 "list of expressions",因为 if 语句显然需要一个表达式。

.immediate. 是很久以前的遗产,当时模板和宏不参与重载决策。当我们第一次让他们了解类型系统时,大量代码需要当前的 untyped 参数,但是重构编译器很难从一开始就引入它们,因此我们添加了 .immediate. 编译指示作为强制整个 macro/template 向后兼容行为的一种方式。

使用 typed/untyped,您可以更精细地控制宏的各个参数,并且 .immediate. pragma 将逐渐被淘汰和弃用。