我在理解 D 中的模板时遇到了一些麻烦。
我了解类或函数的struct Foo(T) { }
或等效项的作用,但是什么是template Bar(T) { }
? 它与类、结构或函数模板有何不同,何时使用?
当你看到template bar(T)
时,你可以把它想象成一个命名空间 - 有点像一个结构或类。就像struct Foo(T)
一样,内容当然是在模板参数上模板化的,并且通常只能通过bar!T.memberName
访问。
我说一般,因为有一些特殊的规则。首先,我们有同名模板。如果template foo(T)
有一个与模板同名的成员,那么foo!T
就是foo!T.foo
的同义词,整个命名空间的想法就会消失。foo!T
内的其他成员是隐藏的,无法访问。实际上,当你编写struct Foo(T) {}
时,编译器会将其转换为template Foo(T) { struct Foo {} }
。
另一种特殊情况是 mixin 模板,它们基本上是当您实例化它们时将逐字删除的片段。也就是说,这段代码(注意template
前面的mixin
关键字):
mixin template Foo() {
int i;
}
struct Bar {
mixin Foo!();
}
在功能上等效于以下代码:
struct Bar {
int i;
}
现在,你为什么要只使用template
?首先是模板元编程。如果您查看std.traits
或std.meta
,这些模块都充满了模板。不是mixin模板,不是模板化的结构,类或函数,而只是模板。它们对传递给它们的值和类型进行操作,并返回某种值或类型。
像这样使用的模板的一个非常简单的例子是std.meta.Reverse
.它获取参数列表并反转它们,如下所示:
template Reverse(TList...)
{
static if (TList.length <= 1)
{
alias Reverse = TList;
}
else
{
alias Reverse =
AliasSeq!(
Reverse!(TList[$/2 .. $ ]),
Reverse!(TList[ 0 .. $/2]));
}
}
想要使用模板的另一种情况是,在某些情况下应省略模板化类型。假设你正在制作自己的Nullable(T)
,并且您希望Nullable!(Nullable!T)
永远Nullable!T
。如果您只是编写struct Nullable(T) {}
,则不会获得此行为,并且最终会得到双可为空的类型。解决方案是使用模板约束:
struct Nullable(T) if (!isNullable!T) {}
以及处理退化情况的模板:
template Nullable(T) if (isNullable!T) {
alias Nullable = T;
}
我希望这有所帮助,如果有什么不清楚的地方,请问。 :)
嗯,从技术上讲,
struct S(T)
{
...
}
相当于
template S(T)
{
struct S
{
...
}
}
和
auto foo(T)(T t)
{
...
}
相当于
template foo(T)
{
auto foo(T t)
{
...
}
}
只是提供了较短的语法,以使常见用例的内容更清晰。template
创建用于代码生成的模板。在使用参数实例化模板之前,该模板中的任何内容都不会作为实际代码存在,并且生成的代码取决于模板参数。因此,模板的语义分析要等到实例化后才会完成。
结构、类和函数将模板作为其声明的一部分,而不是显式声明模板来包装它们,这部分情况就是所谓的同名模板。使用模板时,任何包含与模板同名的符号的模板都将替换为该符号。例如
template isInt(T)
{
enum isInt = is(T == int);
}
然后可以在表达式中使用,例如
auto foo = isInt!int;
枚举isInt.isInt
的值用于表达式中代替模板。此技术与模板约束的帮助程序模板大量使用。例如isInputRange
在
auto foo(R)(R range)
if(isInputRange!R)
{...}
isInputRange
定义为同名模板,如果给定类型是输入范围,则会导致true
,否则false
。在某种程度上,这有点像有一个对类型进行操作的函数,尽管它也可以对值进行操作,并且结果不必bool
。例如
template count(Args...)
{
enum count = Args.length;
}
或
template ArrayOf(T)
{
alias ArrayOf = T[];
}
还有一个用于同名模板的快捷方式语法,如果它们没有任何其他成员,则它们不是用户定义的类型或函数。
例如enum count(Args...) = Args.length;
alias ArrayOf(T) = T[];
正如我所说,同名模板可能有点像具有对类型进行操作的函数,这就是它们在需要对类型执行复杂操作时的作用。例如,将该ArrayOf
模板与std.meta.staticMap
一起使用,您可以执行以下操作
alias Arrays = staticMap!(ArrayOf, int, float, byte, bool);
static assert(is(Arrays == AliasSeq!(int[], float[], byte[], bool[])));
这在模板约束(或模板约束中使用的其他同名模板)中非常有用,或者可以与 static foreach 之类的东西一起使用以更明确地生成代码。 例如,如果我想测试所有字符串类型的一些代码,我可以写类似的东西
alias Arrays(T) = AliasSeq!(T[],
const(T)[],
const(T[]),
immutable(T)[],
immutable(T[]));
unittest
{
import std.conv : to;
static foreach(S; AliasSeq(Arrays!char, Arrays!wchar, Arrays!dchar))
{{
auto s = to!S("foo");
...
}}
}
这些技术通常在元编程中大量使用。除了类型之外,它们还可以用于值,但通常对值使用 CTFE 更有效,而不是将它们放在AliasSeq
中并在其上使用各种同名模板。例如,几年前,火卫一曾经有同名模板Format
,用于在编译时生成字符串,类似于 std.format.format 在运行时的方式,但是一旦 CTFE 改进到可以在编译时使用format
,使用Format
而不是format
就没有意义了, 因为相比之下Format
速度非常慢(做很多递归模板实例化可能会很昂贵)。因此,在对类型进行操作时仍然需要使用模板元编程,但是如果您可以执行CTFE需要做的事情,那通常更好。
如果你正在Phobos中寻找元编程工具,std.traits和std.meta是主要的地方,尽管有些分散在整个火卫一中,这取决于它们的用途(例如,特定于范围的工具在std.range.primitives中)。
此外,与如何混合字符串类似,您可以混合模板。
template foo(T)
{
T i;
}
void main()
{
mixin foo!int;
auto a = i;
}
因此,模板生成的代码基本上被复制粘贴到您混合它的位置。或者,您可以在模板声明上放置mixin
,使其作为 mixin 以外的任何内容使用都是非法的。例如
mixin template foo(T)
{
T i;
}
就个人而言,我通常只使用字符串混音来做这种事情,但有些人更喜欢模板混音。