D Programming: How to Define and Use Custom Attributes

H. S. Teoh hsteoh at qfbox.info
Wed Sep 6 20:04:11 UTC 2023


On Wed, Sep 06, 2023 at 06:54:26PM +0000, Soham Mukherjee via Digitalmars-d wrote:
> I'm exploring the D programming language and I'm curious about custom
> attributes. How can I define and use custom attributes in D to add
> metadata or behavior to my code?
> 
> For example, let's say I want to create a custom attribute
> @MyCustomAttribute that I can apply to functions or types. How do I
> define this attribute, and how can I use it to influence the behavior
> of my D code?
> 
> It would be helpful if you could provide code examples and explain how
> custom attributes can be leveraged in practical scenarios within D
> programming. Thank you for your insights!

User-defined attributes (UDAs) in D are primarily used for compile-time
introspection.  It lets you tag declarations with compile-time metadata
that you can then inspect from other code at compile-time.

One example is command-line option parsing code that prints out
automatically-generated help text when the program is passed a `--help`
option.  The idea is to encapsulate your command-line options in a
struct, for example:

	struct Options
	{
		 double xmin;
		 double xmax;
		 double ymin;
		 double ymax;

		 int xres;
		 int yres;
	}

You could use compile-time introspection to iterate over struct members
and use field names as option names (e.g., so you could run the program
with `--xmin=1.0 --ymax=10.0`, etc.). You can do this without any UDAs,
but suppose you want to go one step further, and have `--help`
automatically print out all available options. You could manually write
a showHelp() function that prints this information as a big string, of
course, but that has the disadvantage that the help text is detached
from the definition of the Options struct. If you subsequently modify
the Options struct, your help text would become out-of-date and contain
possiubly misleading or wrong information.  It would be better if the
help text could be embedded directly in the definition of Options, so
that `--help` will *always* be up-to-date. The way you do this is to add
UDAs to it:

	struct Desc { string text; }

	struct Options
	{
		 @Desc("Minimum X-coordinate") double xmin;
		 @Desc("Maximum X-coordinate") double xmax;
		 @Desc("Minimum Y-coordinate") double ymin;
		 @Desc("Maximum Y-coordinate") double ymax;

		 @Desc("Output horizontal resolution") int xres;
		 @Desc("Output vertical resolution") int yres;
	}

By themselves, these UDAs don't do anything. But now you can inspect
them in your showHelp() function to print out the UDA strings as part of
its output:

	void showHelp() {
		Options opts;
		writefln("Usage: myprogram [options]");
		writeln("Options:");
		foreach (field; __traits(allMembers(Options))) {
			alias desc = getUDAs!(mixin("opts."~field), Desc);
			writefln("--%s", field);
			if (desc.length > 0)
				writefln("\t%s", desc);
		}
	}

Now showHelp() will print out the description for each field in Options,
and it will always display the correct information, as long as you
update the UDA in Options whenever you change the definition of a field.

You don't have to stop here.  You can go even further, and have
automatic option range enforcement. For example, if Options.xres must be
between 10 and 5000, you could write code explicitly to check that, but
again, the check will be detached from the definition of Options so it's
liable to go out-of-sync.  But with UDAs, we can add this information to
the definition of Options, and have the option-parsing code
automatically enforce this:

	struct Desc { string text; }

	struct Range { int minVal, maxVal; }

	struct Options
	{
		 @Desc("Minimum X-coordinate") double xmin;
		 @Desc("Maximum X-coordinate") double xmax;
		 @Desc("Minimum Y-coordinate") double ymin;
		 @Desc("Maximum Y-coordinate") double ymax;

		 @Range(10, 5000) @Desc("Output horizontal resolution") int xres;
		 @Range(8, 4000) @Desc("Output vertical resolution") int yres;
	}

Then in your option-parsing code:

	void parseOption(ref Options opts, string name, string value) {
		foreach (field; __traits(allMembers(Options))) {
			if (name == field) {
				import std.conv : to;
				alias T = typeof(mixin("opts."~field));
				T val = value.to!T; // parse option value

				// Automatic enforcement of option value
				// range
				alias range = getUDAs!(mixin("opts."~field), Range);
				if (range.length > 0) {
					enforce(val >= range.minVal,
						"value of "~field~" is below minimum");
					enforce(val <= range.maxVal,
						"value of "~field~" is above maximum");
				}

				// Range check passed (or no Range UDA
				// was found), assign parsed value to
				// options struct.
				mixin("opts."~field) = val;
			}
		}
	}

Now all you have to do is to update the Range UDA in Options, and
parseOption will automatically enforce the new range. A code change in
one place, and all other code that inspects the UDA will update
themselves automatically.

You can also use UDAs to mark options that require special processing.
For example, say int fields in Options are normally just converted from
base 10, but one particular field, say `octalvalue`, expects user input
in octal instead. Instead of writing if-then-else spaghetti in
parseOption for each such exception, create a new UDA, say Octal, that
you attach to the definition of `octalvalue`. Then in parseOption(),
check the field for this UDA, and if it's present, parse the value using
base 8 instead of base 10.

This may seem like a lot of work for something you could do with just a
single if-statement, but what if in the future you need another field
that's in octal?  With the UDA, you can just tack it on in the
definition of Options, and everything will Just Work(tm).  Glancing at
the definition of Options will immediately tell you that this field is
parsed in Octal; no need to dig through the code of parseOption() to
find out about this. The UDA serves as self-documentation, which is a
good thing.


There are many other examples of UDA usage that I'm sure others will be
able to provide.


T

-- 
The irony is that Bill Gates claims to be making a stable operating system and Linus Torvalds claims to be trying to take over the world. -- Anonymous


More information about the Digitalmars-d mailing list