繁体   English   中英

C++ 和 D 中的元编程

[英]Metaprogramming in C++ and in D

C++ 中的模板机制只是偶然地对模板元编程有用。 另一方面,D's 是专门为促进这一点而设计的。 显然它更容易理解(或者我听说过)。

我没有使用 D 的经验,但我很好奇,在模板元编程方面,您可以在 D 中做什么而在 C++ 中不能做什么?

在 D 中帮助模板元编程的两个最重要的事情是模板约束和static if - C++ 理论上可以添加这两者,并且会从中受益匪浅。

模板约束允许您在模板上放置一个条件,该条件必须为真,模板才能被实例化。 例如,这是std.algorithm.find重载之一的签名:

R find(alias pred = "a == b", R, E)(R haystack, E needle)
    if (isInputRange!R &&
        is(typeof(binaryFun!pred(haystack.front, needle)) : bool))

为了能够实例化这个模板化函数,类型R必须是由std.range.isInputRange定义的输入范围(所以isInputRange!R必须是true ),并且给定的谓词需要是一个二元函数使用给定的参数进行编译并返回一个可隐式转换为bool的类型。 如果模板约束条件的结果为false ,则模板将不会编译。 当模板无法使用给定的参数编译时,这不仅可以保护您免受 C++ 中令人讨厌的模板错误的影响,而且还可以根据模板约束重载模板。 例如, find的另一个重载是

R1 find(alias pred = "a == b", R1, R2)(R1 haystack, R2 needle)
if (isForwardRange!R1 && isForwardRange!R2
        && is(typeof(binaryFun!pred(haystack.front, needle.front)) : bool)
        && !isRandomAccessRange!R1)

它采用完全相同的参数,但其约束不同。 因此,不同类型使用相同模板化函数的不同重载,并且可以对每种类型使用find的最佳实现。 没有办法在 C++ 中干净地做那种事情。 稍微熟悉典型模板约束中使用的函数和模板后,D 中的模板约束相当容易阅读,而您需要在 C++ 中进行一些非常复杂的模板元编程才能尝试这样的事情,而您的普通程序员不会反正能看懂,更不用说自己动手了。 Boost 就是一个典型的例子。 它做了一些了不起的事情,但它非常复杂。

static if进一步改善情况。 就像模板约束一样,可以在编译时评估的任何条件都可以与它一起使用。 例如

static if(isIntegral!T)
{
    //...
}
else static if(isFloatingPoint!T)
{
    //...
}
else static if(isSomeString!T)
{
    //...
}
else static if(isDynamicArray!T)
{
    //...
}
else
{
    //...
}

编译哪个分支取决于哪个条件首先评估为true 因此,在模板中,您可以根据模板实例化的类型来专门化其实现的部分 - 或者基于可以在编译时评估的任何其他内容。 例如, core.time使用

static if(is(typeof(clock_gettime)))

根据系统是否提供clock_gettime以不同方式编译代码(如果存在clock_gettime则使用它,否则使用gettimeofday )。

可能我见过的最明显的例子是 D 改进模板的一个问题是我的团队在 C++ 中遇到的问题。 我们需要根据给定的类型是否从特定基类派生来以不同的方式实例化模板。 我们最终使用了基于此堆栈溢出问题的解决方案。 它有效,但仅测试一种类型是否源自另一种类型就相当复杂。

但是,在 D 中,您所要做的就是使用:运算符。 例如

auto func(T : U)(T val) {...}

如果T可隐式转换为U (就像T派生自U ),则func将编译,而如果T不可隐式转换为U ,则不会。 这种简单的改进甚至使基本模板特化变得更加强大(即使没有模板约束或static if )。

就我个人而言,除了容器和<algorithm>的偶尔函数之外,我很少在 C++ 中使用模板,因为它们使用起来非常痛苦。 它们会导致丑陋的错误,并且很难做任何花哨的事情。 要做任何稍微复杂的事情,您都需要非常熟练地使用模板和模板元编程。 但是使用 D 中的模板,它非常容易,我一直使用它们。 这些错误更容易理解和处理(尽管它们仍然比非模板化函数的错误更糟糕),而且我不必弄清楚如何通过花哨的元编程来强制语言做我想做的事.

C++ 没有理由不能获得 D 拥有的这些能力中的很多(C++ 概念如果能够解决这些问题会有所帮助),但是直到他们添加具有类似于模板约束和static if构造的基本条件编译到 C++、C++模板只是在易用性和功能方面无法与 D 模板进行比较。

我相信没有什么比我多年前发现的这个渲染器更能展示 D 模板系统令人难以置信的力量 (TM) 了:

编译器输出

是的! 这实际上是由编译器生成的……它是“程序”,确实是一个非常丰富多彩的程序。

编辑

来源似乎重新上线。

D 元编程的最佳示例是大量使用它的 D 标准库模块,而不是 C++ Boost 和 STL 模块。 查看 D 的std.rangestd.algorithmstd.functionalstd.parallelism 这些都不容易在 C++ 中实现,至少使用 D 模块具有的那种干净、富有表现力的 API。

学习 D 元编程的最好方法,恕我直言,就是通过这些例子。 我主要是通过阅读 Std.algorithm 和 std.range 的代码来学习的,这些代码由 Andrei Alexandrescu(一位 C++ 模板元编程大师,与 D 密切相关)编写。 然后我使用了我学到的知识并贡献了 std.parallelism 模块。

另请注意,D 具有编译时函数求值 (CTFE),它类似于 C++1x 的constexpr但更通用,因为可以在运行时求值的大量且不断增长的函数子集可以在编译时不加修改地求值。 这对于编译时代码生成很有用,生成的代码可以使用字符串 mixins进行编译。

好吧,在 D 中,您可以轻松地对模板参数施加静态约束,并根据使用static if的实际模板参数编写代码。
可以通过使用模板特化和其他技巧(参见 boost)模拟 C++ 的简单情况,但它是一个 PITA 并且非常有限,因为编译器不会公开有关类型的许多细节。

C++ 真正不能做的一件事是复杂的编译时代码生成。

这是一段 D 代码,它执行定制的map()它通过引用返回其结果

它创建两个长度为 4 的数组,每对对应的元素映射到具有最小值的元素,并将其乘以 50,并将结果存储回原始数组

需要注意的一些重要功能如下:

  • 模板是可变参数的: map()可以接受任意数量的参数。

  • 代码(相对)短 作为核心逻辑的Mapper结构只有 15 行——但它可以用这么少的东西做这么多。 我的观点并不是说这在 C++ 中是不可能的,而是肯定没有那么紧凑和干净。


import std.metastrings, std.typetuple, std.range, std.stdio;

void main() {
    auto arr1 = [1, 10, 5, 6], arr2 = [3, 9, 80, 4];

    foreach (ref m; map!min(arr1, arr2)[1 .. 3])
        m *= 50;

    writeln(arr1, arr2); // Voila! You get:  [1, 10, 250, 6][3, 450, 80, 4]
}

auto ref min(T...)(ref T values) {
    auto p = &values[0];
    foreach (i, v; values)
        if (v < *p)
            p = &values[i];
    return *p;
}

Mapper!(F, T) map(alias F, T...)(T args) { return Mapper!(F, T)(args); }

struct Mapper(alias F, T...) {
    T src;  // It's a tuple!

    @property bool empty() { return src[0].empty; }

    @property auto ref front() {
        immutable sources = FormatIota!(q{src[%s].front}, T.length);
        return mixin(Format!(q{F(%s)}, sources));
    }

    void popFront() { foreach (i, x; src) { src[i].popFront(); } }

    auto opSlice(size_t a, size_t b) {
        immutable sliced = FormatIota!(q{src[%s][a .. b]}, T.length);
        return mixin(Format!(q{map!F(%s)}, sliced));
    }
}


// All this does is go through the numbers [0, len),
// and return string 'f' formatted with each integer, all joined with commas
template FormatIota(string f, int len, int i = 0) {
    static if (i + 1 < len)
        enum FormatIota = Format!(f, i) ~ ", " ~ FormatIota!(f, len, i + 1);
    else
        enum FormatIota = Format!(f, i);
}

我写了我在 D 的模板、字符串混合和模板混合方面的经验: http : //david.rothlis.net/d/templates/

它应该让您了解 D 中的可能性——我不认为在 C++ 中您可以将标识符作为字符串访问,在编译时转换该字符串,并从操作的字符串生成代码。

我的结论:非常灵活,非常强大,普通人也可以使用,但是当涉及到更高级的编译时元编程内容时,参考编译器仍然有些错误。

字符串操作,甚至字符串解析。

这是一个 MP 库,它基于使用(或多或少)BNF 的字符串中定义的语法生成递归体面的解析器。 我已经好几年没碰它了,但它曾经奏效。

在 D 中,您可以检查类型的大小及其上可用的方法,并决定要使用哪种实现

这用于例如core.atomic模块

bool cas(T,V1,V2)( shared(T)* here, const V1 ifThis, const V2 writeThis ){
    static if(T.sizeof == byte.sizeof){
       //do 1 byte CaS
    }else static if(T.sizeof == short.sizeof){
       //do 2 byte CaS
    }else static if( T.sizeof == int.sizeof ){
       //do 4 byte CaS
    }else static if( T.sizeof == long.sizeof ){
       //do 8 byte CaS
    }else static assert(false);
}

只是为了对抗 D 射线追踪帖子,这里是一个 C++ 编译时射线追踪器( metatrace ):

在此处输入图片说明

(顺便说一下,它主要使用 C++2003 元编程;使用新的constexpr会更易读)

在 D 中的模板元编程中,您可以做一些在 C++ 中无法做的事情。 最重要的是,您可以轻松地进行模板元编程!

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM