Using user-defined attributes

D supports user-defined attributes (sometimes called annotations), which are a way to add custom compile-time information to declarations that can be retrieved later by reflection. Here, we'll look at their capabilities and their limitations.

How to do it…

Let's execute the following steps to use user-defined attributes:

  1. Create a struct or enum to use as the attribute. A struct attribute should have data members, as shown in the following code. An enum attribute is best used for a simple flag:
    struct MyNote { string note; }
  2. Attach the attribute to a declaration with the @ sigil, as shown in the following code:
    @MyNote("this is my note on foo") void foo() {}
  3. Retrieve attributes by using the __traits(getAttributes, symbol) function. To pass the symbol to a function or template, use a compile-time parameter with the alias keyword.
  4. Loop over the attributes, retrieving the one you want by identifying the type with the basic form of the is expression. For flags, check for the validity of the type and return true if it is present. For data annotations, such as MyNote, check the typeof parameter and then return the actual annotation so the data can be checked.

The code is as follows:

MyNote getMyNoteAnnotation(alias F)() {
    foreach(attr; __traits(getAttributes, F)) {
         static if(is(typeof(attr) == MyNote))
             return attr;
    }
    assert(0); // the annotation wasn't there
}
// get the note and print out the value
// (the member note is of the MyNote struct)
pragma(msg, getMyNoteAnnotation!(foo).note);

How it works…

D's user-defined attributes never modify a type or generate new code, but do allow you to attach data to declaration that you can use later through the compile-time reflection for code generation if you wish.

The attributes are regular D types, holding regular D data. Once you retrieve them, you use the value like any other variable. To attach them, you can write @some_expression, where some_expression can be almost anything, as long as it gives a result at compile time. Typically, the expression is a struct constructor @MyNote("string"), which attaches a value to the declaration similar to the auto a = MyNote("string") statement that assigns a value to a variable.

Tip

It is also legal to attach plain data to a declaration, such as @1 int foo; will have an int annotation with the value of 1. Since int is not a unique type and attributes are retrieved by type comparison, using naked attributes like this will lead to conflicts where one module interprets int in one way and another sees it as something entirely different. To avoid this situation, always declare a new type for your attributes.

The __traits(getAttributes) function returns a list of all the attributes attached to a symbol. Since the attributes may include data from other libraries about which you know nothing, it is best to loop through them and find only the ones that match your type. Once you have your data, you may do anything with it; but remember, it is just data. We'll look at a concrete example of a user-defined attribute flag in the next recipe.

Note

A common question is how to modify a type with an attribute. This is not possible in D. You may loop over a module, looking for types with an annotation, and then declare a new one with the transformation. However, there's already a tool in the language to modify types: templates and compile-time parameters! So, instead of trying to write @NotNull Object, use NotNull!Object and you'll find easier success.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.136.18.218