Creating an output range

As input ranges serve as generators or iterators over data, output ranges serve as sinks for data. Output ranges may print data, perform one-way algorithms (such as message digests or hashes), collect data, or whatever else you can achieve with its required put method.

Here, we'll write an output range that prints the arrays it receives out in hexadecimal format. We'll write sixteen bytes per line, always ending with a new line.

How to do it…

Let's create an output range by executing the following steps:

  1. Create a struct with a put method that takes the data we want to consume. As our hex dumper takes any kind of array and doesn't store it, we want in void[]. The put method will perform the printing we need.
  2. Add any data we need to preserve between calls to put. To format our lines, we'll need to keep a count of how many items we've outputted so far.
  3. Add other functions we need, such as destructors, constructors, or other methods. We want to ensure there's always a new line at the end, so we'll add a writeln function in the destructor.
  4. Test the function with a static assert for isOutputRange for the types it must accept.

The code is as follows:

import std.range;
import std.stdio;
struct HexDumper {
    int outputted;
    void put(in void[] data) {
        foreach(b; cast(const ubyte[]) data) {
             writef("%02x ", b);
             outputted++;
             if(outputted == 16) {
                 writeln();
              outputted = 0;
          }
      }
    }
    ~this() {
        if(outputted)
            writeln();
      }
}
static assert(isOutputRange!(HexDumper, ubyte));
static assert(isOutputRange!(HexDumper, char));
void main() {
    HexDumper output;
    output.put("Hello, world! In hex.");
}

Run it and you'll see the following output:

48 65 6c 6c 6f 2c 20 77 6f 72 6c 64 21 20 49 6e
20 68 65 78 2c

How it works…

An output range is much simpler than an input range. It only has one required method: put. An output range has no required invariant; you are not required to set anything up in the constructor.

The put method doesn't have to do anything in particular. Its only requirement is to accept a single argument of the type the range can handle. Typically, the put method does one of the following three things:

  • Consume the received data with an algorithm. The std.digest provides output ranges of this style.
  • Collect the data, for example, by building an array. The std.array.Appender is an output range of this style.
  • Display the received data. This is what our example does.

Using an output range at the core consists only of creating the object and calling the put method to feed the object with data. Some ranges, such as the std.digest ranges, also require calls to methods such as start and finish to inform the object that you have finished the process so that it can perform finalization. In our case, we used the destructor to finalize.

A destructor is declared in a struct with the syntax ~this() { /* code */ }. The destructor runs immediately when the struct object goes out of scope.

Note

Destructors are not run when a pointer to a struct goes out of scope, which means a dynamic array of structs will not necessarily do a clean up as soon as possible, since a dynamic array uses a pointer.

Destructors are used for clean up—freeing memory, releasing resources, or finalizing an operation, with the limitation that the object is no longer available.

The std.digest cannot use a destructor as its finish method changes the data and returns the final result. A destructor cannot return a value; by the time the destructor runs, the object is no longer available, so it would be impossible to get the results. Our HexDumper method can output a new line in the destructor, as that does not need to return data.

The implementation of our put method loops over the data and writes it in hexadecimal using std.stdio.writef. The loop includes a cast to const ubyte[] because the function accepts generic data in the form of in void[]. Any array will implicitly cast to void[], allowing us to pass anything to the put method, including strings and other arrays. However, a void[] array is only usable if it is explicitly casted to something else first. We want to print out the individual bytes, so we casted to const ubyte[]. Then, the foreach loop will go one byte at a time.

Finally, the writef function works like writefln, but does not print an automatic new line at the end. The writef function is D's version of the classic C printf function. The first argument is a format string. Our format string, "%02x", means pad with zeroes to at least two digits wide, and then put out the argument in hexadecimal, followed by a space.

There's more…

The std.range.put function is a generic function that extends the ability of your output range to accept data. You only need to implement the core data type your function needs. Let other transformations such as accepting generic input ranges be handled by the standard library. An example of this can be seen in the Creating a digest recipe in Chapter 2, Phobos – The Standard Library.

See also

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

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