Report a bug
If you spot a problem with this page, click here to create a Bugzilla issue.
Improve this page
Quickly fork, edit online, and submit a pull request for this page. Requires a signed-in GitHub account. This works well for small changes. If you'd like to make larger changes you may want to consider using a local clone.

Operator overloading for raw templates

Version 1
Created 2014-06-15
StatusDraft
Last modified 2013-06-15
Author Михаил Сташун (Dicebot)

Abstract

This DIP proposes solution for user-defined types with semantics similar to those of built-in template argument lists. It can be achieved by overloading operators inside template symbols that don’t resolve to any valid type.

Rationale

During discussion and implementation of DIP54 it was figured out that we are currently lacking any hygienic way to defined packed argument lists. Existing solutions degrade into unpacked lists on any operation and are thus not robust enough to fit into standard library. This can be achieved by allowing overloads for static opSlice and similar operators. To avoid ambiguity with definition of array types those should be applicable to something that is not a valid type on its own - template symbol.

Description

On the origin of proposal

Most simple implementation of packed argument list looks like this:

template Pack(T...)
{
    alias expand = T;
}

It is commonly used as a quick solution but lacks any convenience. Packing is achieved only for passing as template argument, any actual inter-operation requires expansion. This is both error-prone and pollutes code with lot of otherwise unneeded .expand suffixes. Somewhat more robust implementation uses aggregate and alias this:

struct Pack(T...)
{
    alias expand = T;
    alias expand this;
}

This makes it possible to use template argument list operations on a pack without explicit .expand which is a big advantage. However, any such operation (like slicing) will return expanded template argument list which needs to be explicitly packed again to preserve hygiene. In practice such solution is also hardly acceptable for Phobos.

Obvious fix is to allow template operator overloads:

struct Pack(T...)
{
    alias expand = T;

    alias opSlice(size_t lower, size_t upper) = Pack!(T[lower..upper]);
}

However this does not work for all desired operators because of ambiguity with array declaration syntax:

struct Pack(T...)
{
    alias expand = T;

    alias opIndex(size_t index) = T[index];
}

// Pack!(int, int)[1] -> integer or array of structs?

To address that we can benefit from the fact that non-eponymous template symbols are not a valid types in D:

template SomeTemplate(T...)
{
}

static assert (!is(SomeTemplate!int));

So final solution may look something like this:

template Pack(T...)
{
    alias expand = T;

    alias opIndex(size_t index) = T[index];
    alias opSlice(size_t lower, size_t upper) = Pack!(T[lower..upper]);
    alias opDollar = T.length;
}

alias element = Pack!(int, int)[1]; // no ambiguity as Pack!(int, int) is not a valid type

Instead of defining opApply for such type compiler can detect if opIndex and opDollar are defined and generate static foreach code same as it is done for built-in template argument lists.

Summary of proposed semantics

  1. allow defining opIndex, opSlice and opDollar as aliases / enums inside templates
  2. check for presence of those when matching operation is attempted on template symbol that does not resolve to a type
  3. if such non-type template is used inside foreach loop, rewrite it as iteration from opIndex[0] to opIndex[$]
  4. template symbol itself is still not considered proper type by is expression

Additional use cases

Suggested by Timon Gehr, hygienic tuple slicing:

struct Tuple(T...) {
    T expand;
    template Pack() {
        auto opSlice(size_t lower, size_t upper) {
            return tuple(expand[lower..upper]);
        }
    }
    alias Pack!() this;
}
auto tuple(T...)(T args) { return Tuple!T(args); }

void main(){
    Tuple!(double,int,string) t1 = tuple(1.0, 2, "three");
    auto t2 = t1[1..$];
    static assert (is(typeof(t2)==Tuple!(int,string)));
    foreach (i, v; t1) writeln(i,": ",v);
}

Backwards compatibility

Almost perfect, no semantics of currently working code are changed. Only way to be influenced by the change is to query for specific semantics of non-type template symbols via is expression, which is extremely uncommon thing to do.

QA

Q: It seems inconsistent that the operators are defined as templated aliases instead of functions as anywhere else.

A: To be able to mimic semantics of template argument lists such overloaded operators need to be able to return types and symbols which is not possible with function. However, planned implementation should accept functions too, as well as anything that fits `Symbol.opSlice!(a, b)` call pattern.

Q: Why these operators only?

A: Because author needs it implemented for one specific task and any other applications are likely to not get enough testing / attention. If concept will work good, other operators can be added on per-need basis.

This document has been placed in the Public Domain.