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.
3.147.80.100