在 D 语言中使用重载集创建特定于类型的模板 API
(Ab)Using Overload Sets to Create Ad-Hoc Template APIs in D

原始链接: https://blog.dlang.org/2026/07/19/abusing-overload-sets-to-create-ad-hoc-template-apis/

在 D 语言中,“重载集”(Overload sets)——即共享同一名称的函数或模板集合——是泛型编程的强大工具。通过利用模板特化,开发者可以超越僵化、单一的定义,从而创建“特定接口”(ad-hoc APIs)。 与定义单一主模板不同,特定接口表现为一组特化匹配的集合。这种方法具有以下优势: * **维度无关的代码**:通过对整数值(如向量维度)进行模板特化,您可以编写通用的逻辑,在无需重复代码的情况下操作不同的结构。 * **编译时元编程**:您可以像处理数组一样处理重载集,从而实现编译时查找、代码生成和灵活的分发。 * **可扩展的 API**:通过将函数设计为接受重载集作为别名,您可以允许用户注入他们自己的实现(如自定义序列化逻辑),从而有效地“修复”或扩展 API 的行为。 归根结底,重载集允许您根据成功匹配的逻辑而非固定的定义来进行编程。通过专注于设计良好的模板头文件,开发者可以构建高度灵活的元编程 API,在保持简洁和可维护性的同时,支持多种访问模式。

```Hacker News 最新 | 过往 | 评论 | 提问 | 展示 | 招聘 | 提交 登录 (Ab) 在 D 语言中使用重载集创建即席模板 API (dlang.org) 6 分 | WalterBright | 1 小时前 | 隐藏 | 过往 | 收藏 | 2 条评论 | 帮助 Kapendev | 13 分钟前 | 下一条 [–] 非常有意思的文章。我该如何将其用于 Web 开发?我的主管让我问这个问题。 回复 WalterBright | 1 小时前 | 上一条 [–] “重载集”是一个术语,它似乎源于多年来重载规则的复用。重载集在背后支撑着 D 语言中许多泛型编程的实现,一旦你明确理解了它们,就能开启一类全新的 API 设计方式。 回复 考虑申请 YC 2026 年秋季批次!申请截止日期为 7 月 27 日。 准则 | 常见问题 | 列表 | API | 安全 | 法律 | 申请 YC | 联系 搜索: ```
相关文章

原文

“Overload set” is a term of art that seemingly arises from the reuse of overload rules across the years. Overload sets quietly power a lot of D’s generic programming, and once you understand them explicitly, a whole class of API design opens up.

import std;

void bar(int)   { "int".writeln;   }
void bar(float) { "float".writeln; }
void bar(bool)  { "bool".writeln;  }

void useIt(alias F)() {
    F(1);      // prints "int"
    F(13.37);  // prints "float"  
    F(true);   // prints "bool"
}

unittest { useIt!bar; }

What exactly is bar when passed to useIt? Wait, does it survive as useIt.F? (Yes, it compiles as is.)

What Are Overload Sets?

The specification:

An Overload Set is the set of functions with the same name declared in the same scope that participate in overload resolution.

And from the alias spec:

Aliases can also ‘import’ a set of overloaded functions that can be overloaded with functions in the current scope…

The behavior of overload sets is scattered across the spec. A few sentences here, a few more tucked away in the template section. Template specialization lets you pattern-match against the whole set; pick the right implementation based on compile-time arguments.

Overload sets can work with several very different things. I use the following definition:

An overload set is a collection of things that share a name and a closely related template header.

You may not know everything that can be templatized, and real-world template usage is extremely concerned about making only one declaration match at a time.

Specialization on Integer Values

alias typeAt(int I : 0) = int;
alias typeAt(int I : 1) = float;
alias typeAt(int I : 2) = string;
enum typeAtLength = Length!typeAt;

template Length(alias A, int acc=0){ // returns the pseudo-length of an overloadset
    static if( ! __traits(compiles,A!acc) ){
        enum Length=acc;
    } else {
        enum Length=Length!(A,acc+1);
    }
}

static foreach (i; 0 .. typeAtLength) {
    pragma(msg, typeAt!i.stringof);
}
// prints: int, float, string

By treating value specialization of ints as an array, you can make an overload set that is foreachable. You can build compile-time lookup tables, generate code for each type in the set, and dispatch based on integer constants, etc.

Real-World Pattern: Unified Vector Interface

In Kap’s joka library, GVec2, GVec3, GVec4 are all parameterized by type, but what if I want dimension-generic code?

It’s an incomplete templatization:

// Separate types for each dimension
alias BVec2 = GVec2!byte;
alias IVec2 = GVec2!int;
// ... more aliases

struct GVec2(T) { T x, y; }
struct GVec3(T) { T x, y, z; }
struct GVec4(T) { T x, y, z, w; }

You can’t write “works on any N-dimensional vector”; the dimension is stuck in the type name. To specialize on dimension, you can expand the shared template header to include an int parameter.

alias GVec2(T = float) = GVec!(2, T); // (optional, allows backwards compatibility)
alias GVec3(T = float) = GVec!(3, T);
alias GVec4(T = float) = GVec!(4, T);

// struct GVec(int N, T); // implicit, no primary definition exists

struct GVec(int N : 2, T) { T x = 0; T y = 0; }
struct GVec(int N : 3, T) { T x = 0; T y = 0; T z = 0; }
struct GVec(int N : 4, T) { T x = 0; T y = 0; T z = 0; T w = 0; }

Now you can write functions that take GVec!(N, T) and work across any dimension. Pattern-matching on N at compile time allows you to easily access N in trivial meta-code:

void print(int N, T)(GVec!(N, T) v) {
    import std;
    writeln("Vector of dimension ", N, " with type ", T.stringof);
    static foreach (c; "xyzw"[0 .. N]) {
        writeln(c, ": ", mixin("v." ~ c));
    }
}

unittest {
    IVec3(1, 4, 7).print;
    DVec4(2, 6, 0, 0).print;
}

The Ad-hoc Template API

The above is an Ad-hoc Template API. Note that no primary struct GVec(int N, T) exists in the source code. The symbol GVec exists only as a collection of specializations. The API is a coordinate map of successful matches. You are programming against the existence of a match in the resolution logic rather than a central definition.

The API that can be named is not the immortal API. -monkyyy-tzu

Swapping an Overload Set

std.conv.to is an overload set with the implied syntax of .to!T. You can let users pass their own instead for serialization.

template someSerializeFunc(alias TO_ = void, Args...)(Args args) {
    static if (is(TO_ == void)) {
        import std.conv;
        alias TO = std.conv.to; // Successful partial symbol resolution
    } else {
        alias TO = TO_;
    }
    // ... use args with .TO!T for conversions
}

This code uses the default std.conv.to unless the user provides their own. So long as the user matches the ad-hoc API of to, they can extend it (and fix its edge cases). This flexible API, with an overloadable-overload-set, lets users fix behavior.

enum foo(int I : 0) = 0;
int foo(int I : 1)(int) => 1;
template foo(int I : 2) {
    int foo = 2;
}
struct foo(int I : 3) {
    int myint = 3;
}

The overload set resolution and specialization mechanisms work on all types of templates in D. The compiler only cares about something matching the template header; you can define it however you want and make ad-hoc APIs.

With type and value specialization shared across at least four declaration patterns, which of your problems could be approached by asking, “How can I define a good template header”?

Conclusion

Overload sets aren’t exotic. Combine them with template specialization and you get ad-hoc APIs that support multiple access patterns without duplicating code. Build your types to play nice with meta-programs, design your APIs to take overload sets, and let users swap in their own implementations. This works even for more complex APIs such as std.conv.to.


Based on 2 chapters from my book, ‘Black Magic in D’.

https://crazymonkyyy.github.io/

联系我们 contact @ memedata.com