Compile-time function execution

Phobos ships with one of the fastest regular expression engines available. This is possible in part because of its ability to make use of CTFE and other compile-time features to compile regular expressions and generate native machine code for matching (Dmitry Olshansky's DConf 2014 talk gives insight into the regular expression engine; refer to http://dconf.org/2014/talks/olshansky.html). Keep in mind that the performance benefit doesn't come for free; the cost is paid for as an increase in compile time and the potential for code bloat. Still, CTFE can often prove to be a big enough win in terms of performance and/or maintenance costs to outweigh the drawbacks.

Any D function can be executed at compile time as long as it doesn't depend on runtime data. As an example, let's revisit the packRGBA function from earlier in the book.

uint packRGBA(ubyte r, ubyte g, ubyte b, ubyte a = 255) {
  return (r << 24) + (g << 16) + (b << 8) + a;
}

This function is a candidate for compile-time execution because all of the data can be known at compile time. The default value of a is a compile-time value, as are the literals used in the function body. This leaves the other parameters, r, g, and b. Whether or not they are compile-time values depends on the context. Consider the following invocations:

int red = 255, blue, green;
auto col = packRGBA(red, blue, green);
col = packRGBA(255, 0, 0);

There is no possibility whatsoever for the first call to occur at compile time; the arguments are all runtime values. The second invocation uses integer literals, so it meets the requirement that the function use only compile-time values. However, the return value is assigned to a runtime variable. In this case, the compiler doesn't need to execute the function at compile time, so it doesn't. More generally, if a function must be run at compile time, it will be; if it doesn't have to be executed at compile time, it won't be. The following examples all force the function to be called at compile time:

enum red = packRGBA(255, 0, 0);        // manifest constant
immutable green = packRGBA(0, 255, 0); // module-scope immutable
const blue = packRGBA(0, 0, 255);      // module-scope constant
int white = packRGBA(255, 255, 255);   // module-scope mutable
enum Color : uint {                    // enum members
  red = packRGBA(255, 0, 0),
  green = packRGBA(0, 255, 0),
  blue = packRGBA(0, 0, 255),        
}
struct FooColor {
  // Set default init value for user-defined type fields
  uint r = packRGBA(255, 0, 0);     
  // Initialize static user-defined type members
  static uint green = packRGBA(0, 255, 0);
}
void someFunc() {
  // Initialize local static variables
  static auto red = packRGBA(255, 0, 0);
}

In each case, integer literals are used as parameters and the result is assigned in a variable or constant declaration. The compiler will pick up on all of that and execute the function at compile time without any further coercion. If CTFE is not possible in any given context, the compiler will emit an error. When the compiler does execute a function, it is acting as a D interpreter. Consider:

string makeID(string s, string suffix = null) {
  auto ret = "ID_" ~ s;
  ret ~= suffix;
  return ret;
}
enum ID : string {
  One = makeID("One"),
  OneEx = makeID("One", "Ex"),
}
pragma(msg, ID.One);
pragma(msg, ID.OneEx);

When the compiler encounters the calls to makeID in the declaration of the ID members, it determines that the function can be executed at compile time and goes into interpreter mode to do so. From inside the function, this essentially looks like any other runtime execution, and it basically is. The difference is only in the context in which it is executed. Let's modify makeID a little.

string prefix = "ID_";
string makeID(string s, string suffix = null) {
    auto ret = prefix ~ s;
    ret ~= suffix;
    return ret;
}

Now the function makes use of a mutable, module-scope variable. Although the variable is initialized with a compile time value, prefix itself is a runtime variable; it cannot be known in a compile-time context. Execute makeID at runtime and all is well, but execute it at compile-time and an error is produced saying that the static variable prefix cannot be read at compile time. Change makeID one more time.

enum usePlatformPrefix = true;
string makeID(string s, string suffix = null) {
    static if(usePlatformPrefix) {
        version(Windows) enum prefix = "WIN_ID_";
        else enum prefix = "NIX_ID_";
    }
    else enum prefix = "ID_";
    auto ret = prefix ~ s;
    ret ~= suffix;
    return ret;
}

This version still uses an external variable, but this time it's a manifest constant that can be known at compile time. It's also got some new compile-time constructs inside. Here's where some people get confused. The static if and version blocks are evaluated before the function is executed by the interpreter, not during CTFE. Again, inside makeID there is no difference whether the function is executed at compile time or at runtime; the same code is run either way. Another way to look at it is that a function body is not a compile-time construct such as a static if block or a manifest constant. In a compile-time context, the function is run and its result is used in a compile-time construct; in a runtime context, the function is run and its result is used in runtime construct. To the function itself, there is absolutely no difference.

Sometimes, we really do want the implementation of a function to behave somewhat differently in compile-time and runtime contexts. To facilitate this, the language provides a special variable, __ctfe, which is true when the function is being executed by the built-in interpreter at compile time and false during normal runtime execution. A common mistake new D users make is to try and use __ctfe with static if, but it's a runtime variable. Here's an example of __ctfe being used to produce context-dependent output.

string genDebugMsg(string msg) {
  if(__ctfe)
    return "CTFE_" ~ msg;
  else
    return "DBG_" ~ msg;
}
pragma(msg, genDebugMsg("Running at compile-time."));
void main() {
  writeln(genDebugMsg("Running at runtime."));
}

Some may cringe at the idea of introducing a runtime branch just to distinguish between the two contexts, but there's no need to worry. Because __ctfe is always false at runtime, the branch will never make it into the binary even when optimizations are not enabled.

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

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