Previous Entry Share Next Entry
Explaining Language Design (Part 2: Const Inference)
anime
he_the_great

The next part of "The Last Thing D Needs" goes into type inference with auto and templates. The rule here is very simple in D, the inferred type will be the same as the type providing the inference.

dlang
void main() {
    const int cx = 0;
    auto my_cx1 = cx;

C++ will infer the type of my_cx1 to be an int. This is because it is a new variable being declared so it does not need to maintain the same constness as cx. D doesn't try to guess desire and will maintain const on my_cx1.

dlang
    static assert(is(typeof(my_cx1) == const int));

    int my_cx1prime = cx;

    import std.traits : Unqual;
    Unqual!(typeof(cx)) my_cx1doublePrime = cx;
    static assert(is(typeof(my_cx1doublePrime) == int));

Taking a closer look at the options D provides. Since int is a value type D allows for the const attribute to be dropped when assigning to a new variable. This works if you know the type your working with. If inside generic code the same thing can be achieved utilizing std.traits.Unqual (unqualify). I'm not quite done.

dlang
    const my_cx1pointer = &cx;
    static assert(is(typeof(my_cx1pointer) == const(int*)));

    static assert(is(
      Unqual!(typeof(my_cx1pointer)) == const(int)*));

    const(int)* my_cx1pointerPrime = my_cx1pointer;

    version(noneint* my_cx1pointerPrime = &cx;

D enforces a few more things than C++ with const. In D const is transitive, which means that all elements under const are also const. By declaring my pointer to cx as const the type is const(int*) and not const(const(int)*) as that is already the definition.

Utilizing Unqual will only strip off the top level const, since this preserves the constness it is possible to assign to this new type when declaring a new variable. The compiler will stop you if the type does not preserve the constness as demonstrated by my_cx1pointerPrime.

C++
    decltype(cx) my_cx2 = cx;

D does not provide a decltype as typeof already does the same thing; this is the appropriate way in C++ to declare a variable with the same type as cx.

C++
    template<typename T>
    void f1(T param);
    f1(cx);

dlang
    void f1(T)(T param) {
        static assert(is(typeof(param) == const int));
    }

    f1(cx);

C++ selects int as the type for T, while D utilizes the type of the passed variable the same auto.

C++
    template<typename T>
    void f2(T& param);
    f2(cx);

dlang
    void f2(T)(ref T param) {
        static assert(is(typeof(param) == const int));
    }

    f2(cx);

With the use of a reference C++ changes sides and will now keep T as const int as the location of memory is supposed to be unchanged by the const variable we are now referring.

On the other hand D stays consistent and keeps the type of the original variable.

C++
    template<typename T>
    void f3(T&& param);
    f3(cx);

The final template takes a universal reference which is typed as const int ref. The ref was added to allow for perfect forwarding. D does not have universal reference but perfect forwarding is simple; with one exception.

dlang
    import std.traits : ParameterTypeTuple;
    T f3(T)(ParameterTypeTuple!T args) {
        return T(args);
    }

By using ParameterTypeTuple we are able to obtain the types utilized within the opCall, it also works with functions. I did not locate a way to get the types utilized by the constructor.

dlang
    struct A {
        static A opCall(int a, int b) {
            return A.init;
        }
    }
    f3!A(2,3);

    class B {
        static B opCall(int a, ref int b) {
           b = 42;
           return B.init;
        }
    }
    auto b = 3;
    f3!B(2,b);
    assert(b == 42);
    version(none) f3!B(2,3); // Error
}

Building a couple types with an opCall allows our function to be called to construct a type. Obviously this isn't ideal and it is recommend to utilize a factory function instead of static opCall too.

Conclusion

There is some room for improvement with the way D handles perfect forwarding. It has started with a very consistent foundation and is positioned to very easily support perfect forwarding without additional complexity with grammar rules or semantics.

  1. Part 1: Default Initialization
  2. Part 2: Const Inference
  3. Part 3: Lambdas in C++
  4. Part 4: Lambdas in D
  5. Part 5: Type Inference
  6. Part 6: Inheritance
  7. Part 7: Algorithm Complexity
  8. Part 8: Essential Complexity

?

Log in