Skip to content

Forcing Named Arguments in D

Problem

D's named arguments (DIP1030) are convenient but optional — callers can always fall back to positional arguments. For APIs with multiple parameters of the same type (e.g., int x, int y, int width, int height), positional calls are error-prone because swapping arguments compiles silently.

There is no built-in keyword-only parameter mechanism (unlike Python's * separator).

Solution: Private Sentinel Type

Place a private-typed parameter with a default value as the first parameter. External callers cannot construct or match the private type positionally, so they must use named arguments for the remaining parameters.

Functions

d
module mylib.drawing;

/// Sentinel type — private to this module, impossible to name or
/// construct from the outside.
private struct NamedOnly {}

/// Draws a rectangle. External callers must use named arguments:
///     draw(x: 10, y: 20, width: 100, height: 200);
void draw(NamedOnly _ = NamedOnly.init, int x = 0, int y = 0, int width = 0, int height = 0)
{
    // ...
}
d
import mylib.drawing;

draw(x: 10, y: 20, width: 100, height: 200);  // ✅ compiles
draw(width: 50, height: 50);                    // ✅ partial — rest get defaults
draw(10, 20, 100, 200);                         // ❌ Error: cannot pass `int` to parameter `NamedOnly`

Structs

The same trick works for struct initialization — place the sentinel as the first field:

d
module mylib.config;

struct ServerConfig
{
    private struct NamedOnly {}

    NamedOnly _ = NamedOnly.init;
    string host;
    ushort port;
    uint maxConnections = 100;
}
d
import mylib.config;

auto cfg = ServerConfig(host: "localhost", port: 8080);  // ✅ compiles
auto bad = ServerConfig("localhost", 8080);               // ❌ Error: cannot convert `string` to `NamedOnly`

Guidelines

  • Name the sentinel _ so it is clearly not a real parameter
  • Always give it = NamedOnly.init so callers never need to mention it
  • All real parameters should also have defaults when the sentinel is used, since named arguments allow skipping parameters (e.g., draw(width: 50, height: 50))
  • Use this idiom for APIs where positional argument confusion would be a source of bugs (e.g., multiple parameters of the same type like coordinates)
  • Prefer the function-parameter variant over the struct-field variant for zero-overhead guarantees (see ABI Impact below)

ABI Impact

The sentinel is a zero-sized struct. Its impact depends on where it is used:

VariantOverheadWhy
Function parameterNoneZero-sized struct occupies no register and no stack slot — identical codegen
Struct field4 bytesD structs have minimum sizeof == 1; alignment padding inflates the struct

Compiler Output

With LDC at -O2 -release, the function-parameter variant produces byte-identical assembly to a plain function (full source: abi_comparison.d):

d
export Rect withSentinel(
    NamedOnly _ = NamedOnly.init,
    int x = 0, int y = 0, int width = 0, int height = 0, int margin = 0,
)
{
    return Rect(x - margin, y - margin, width + 2 * margin, height + 2 * margin);
}
d
export Rect withoutSentinel(int x, int y, int width, int height, int margin)
{
    return Rect(x - margin, y - margin, width + 2 * margin, height + 2 * margin);
}
asm
subl    %r8d, %edi
subl    %r8d, %esi
leal    (%rdx,%r8,2), %edx
leal    (%rcx,%r8,2), %ecx
shlq    $32, %rsi
leaq    (%rdi,%rsi), %rax
shlq    $32, %rcx
orq     %rcx, %rdx
retq

Alternative Techniques Considered

None of the following enforce named-only arguments:

TechniqueIdeaWhy it doesn't work
@disable this(int, int, …)Disable the positional constructor on a structNamed args resolve to the same constructor signature, so @disable blocks both positional and named calls
Distinct wrapper types (struct X { int v; })Each parameter gets its own type, preventing accidental swapsProvides type safety but doesn't force naming — callers can still write draw(X(10), Y(20)) positionally
All-default parametersGive every parameter a default so callers can skip freelyPositional calls still compile — draw(10, 20, 100, 200) is accepted without names
Struct parameter wrappervoid draw(DrawOpts opts)Encourages naming at the struct literal site, but DrawOpts(10, 20, 100, 200) still compiles positionally
static opCall with @disable this()Disable default constructor and route through static opCall@disable this() interferes with opCall — the compiler tries the disabled constructor first and rejects the call

The experiments for each technique are in this directory — alternatives.d collects all failing approaches, and abi_comparison.d is the ABI snippet shown above. The multi-module enforcement test:

d
module lib;

private struct NamedOnly {}

struct Rect
{
    int x, y, width, height;
}

Rect inflateRect(NamedOnly _ = NamedOnly.init, int x = 0, int y = 0, int width = 0, int height = 0, int margin = 0)
{
    return Rect(x - margin, y - margin, width + 2 * margin, height + 2 * margin);
}

struct RectOpts
{
    private struct NamedOnly {}

    NamedOnly _ = NamedOnly.init;
    int x, y, width, height;
}

Rect makeRect(RectOpts o)
{
    return Rect(o.x, o.y, o.width, o.height);
}
d
/// Run: dmd -i -run test_positive.d
import lib;

void main()
{
    import std.stdio : writefln;

    auto r1 = lib.inflateRect(x: 10, y: 20, width: 100, height: 200, margin: 5);
    assert(r1 == lib.Rect(5, 15, 110, 210));

    auto r2 = lib.inflateRect(width: 50, height: 50);
    assert(r2 == lib.Rect(0, 0, 50, 50));

    auto r3 = lib.makeRect(lib.RectOpts(x: 10, y: 20, width: 100, height: 200));
    assert(r3 == lib.Rect(10, 20, 100, 200));

    writefln("All positive tests passed.");
}
d
/// Run: dmd -i -c test_negative.d
/// Expected: compilation errors for every call below.
import lib;

void main()
{
    auto r1 = lib.inflateRect(10, 20, 100, 200, 5);  // ❌ cannot pass `int` as `NamedOnly`
    auto r2 = lib.RectOpts(10, 20, 100, 200);         // ❌ cannot convert `int` to `NamedOnly`
}