C++ 和 D 中的元编程

Posted

技术标签:

【中文标题】C++ 和 D 中的元编程【英文标题】:Metaprogramming in C++ and in D 【发布时间】:2011-11-10 03:32:08 【问题描述】:

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

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

【问题讨论】:

如果他们俩都图灵完整,答案是什么:) @awoodland:这仅适用于“做”的非常有限的定义。按照任何正常的定义,有很多事情是你不能用 C++ 模板做的(例如写入文件——但我想你也不能用 D 中的模板元编程来做到这一点)。 @awoodland:图灵 tarpit,有人吗? ;) @Paul:你是指C++03及更早的版本,还是C++0x/C++11? @Merhdad C++11 肯定会在模板(例如可变参数模板)中添加一些有用的东西,这样它们就不会那么糟糕了,但没有像 D 这样的条件编译有,他们仍然没有接近 D 的模板。因此,无论您是在谈论 C++11 还是 C++11 之前的版本,肯定与问题相关,但最终无关紧要。 【参考方案1】:

在 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++ 中遇到的一个问题。我们需要根据给定的类型是否从特定的基类派生来以不同的方式实例化模板。我们最终使用了基于this stack overflow question 的解决方案。它可以工作,但仅测试一种类型是否源自另一种类型是相当复杂的。

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

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

如果T 可以隐式转换为U(就像T 派生自U 一样),那么func 将编译,而如果T 不能隐式转换为@ 987654350@,那就不行了。 这种简单的改进甚至使基本的模板专业化变得更加强大(即使没有模板约束或static if)。

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

没有理由 C++ 不能获得 D 所具有的大部分能力(如果他们能够整理出这些能力,C++ 概念会有所帮助),但直到他们添加基本的条件编译以及类似于模板约束和static if 的构造对 C++ 而言,C++ 模板在易用性和功能方面无法与 D 模板相比。

【讨论】:

你可以在static if中声明不同类型的变量。非常有用。 C++ 现在有模板约束和static if的变体 是:if constexpr 和概念【参考方案2】:

我相信没有什么比我多年前发现的this renderer 更能证明 D 模板系统的强大功能了:

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

编辑

来源似乎重新上线了。

【讨论】:

酷!知道从哪里获得源代码吗? 我似乎找不到它(我想我前段时间下载了它)。但即使我在我的一个驱动器上找到它,我也不确定分享它是否合法。也许有人可以要求作者修复链接(它很可能不是故意破坏的)。 附带说明,那里的源代码是多年前编写的(如随附页面中所述) - 那里的很多代码(特别是 meta/ 目录中的代码)可以是 大大由于对 D 的更改而简化和缩短了,即使没有接近编译时函数执行。 @Jasu_M:您链接到的光线追踪器必须在编译后调用。我认为这与 ctrace 有很大的不同。这是一个很大的区别,如果您可以让您的 c++-template 系统生成一个可执行文件,该可执行文件将在标准输出上打印出图像,或者您可以让您的 d-template 系统获取 compiler 直接生成图像。 @Justin:恭喜你完全没有抓住重点;)它很酷,所以它比下面不那么酷但更有用的答案更受欢迎。问题是“我可以在 d 中做什么我在 c++ 中不能做什么”。输出 rgb 而不是程序与你在 c++ 中可以做的事情相去甚远,所以这就是你的答案。【参考方案3】:

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

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

另请注意,D 具有编译时函数评估 (CTFE),它类似于 C++1x 的constexpr,但更通用的是,可以在运行时评估的大量且不断增长的函数子集可以在未修改的情况下评估编译时间。这对于编译时代码生成很有用,生成的代码可以使用string mixins进行编译。

【讨论】:

对于CFTE,您可以阅读我的博文以获得更完整的解释:giovanni.bajo.it/2010/05/compile-time-function-execution-in-d【参考方案4】:

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

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

【讨论】:

【参考方案5】:

这是一段 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!(qsrc[%s].front, T.length);
        return mixin(Format!(qF(%s), sources));
    

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

    auto opSlice(size_t a, size_t b) 
        immutable sliced = FormatIota!(qsrc[%s][a .. b], T.length);
        return mixin(Format!(qmap!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);

【讨论】:

【参考方案6】:

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

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

我的结论:极其灵活、极其强大,并且可供普通人使用,但在涉及更高级的编译时元编程的东西时,参考编译器仍然有些错误。

【讨论】:

3天前发布的最新版dmd(D编译器)修复了我发现的两个错误之一。我已经更新了这篇文章以适应。 当我到达“关系代数的类型化实现”的代码示例时,恐怕你的文章让我迷失了方向,因为我还不太精通 D 或它是什么你想完成普通功能无法完成的事情。 Qwertie:考虑第一个代码示例中“这会导致编译错误”的部分——如果不使用元编程,我不知道如何实现这一点。像“project”(π)这样的函数正在在编译时动态创建可以被编译器检查的新类型——所以如果你说“ages[0].name”你会得到一个编译错误,而不是运行时错误。 (P.S. 我对 D 也不是很精通,所以我可能把事情复杂化了。)【参考方案7】:

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

This is a MP library 基于使用(或多或少)BNF 在字符串中定义的语法生成递归体面的解析器。我已经很多年没有碰过它了,但它曾经工作过。

【讨论】:

【参考方案8】:

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

这用于例如core.atomic module

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);

【讨论】:

在 C++ 中,您也可以检查 sizeof,尽管这最好由专业化处理 这不会在运行时发生,会增加开销吗?在 D 版本中,这一切都发生在编译时。没有分支。 C++ 编译器可以像这样优化检查(尽管不能保证),所以这不是一个很好的例子。在 C++ 中你不能轻易做到的事情是 static if (is(T == string)) writeln(t ~ t); else writeln(t * 2);。你不能在 C++ 中这样做,首先因为你不能那么容易地测试类型,其次因为如果 x 是字符串,x * 2 不会编译,如果 x 是数字,x ~ x 不会编译。 【参考方案9】:

为了反驳 D 光线追踪帖子,这里有一个 C++ 编译时光线追踪器 (metatrace):

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

【讨论】:

使用 D 2.0,主要区别在于编译时光线追踪器看起来像普通的 D 代码,而 C++ 光线追踪器要长得多,大多数开发人员甚至不想尝试理解它,更不用说编写任何显着大小的元程序了。 @Qwertie:这也许是对的。使用 C++11,您也可以进行非常易读的编译时元编程。目前有一个警告:constexpr 函数只能使用三元运算符和递归来进行流控制(例如:constexpr int fac(int c) return c&lt;=1 ? 1 : c*fac(c-1); )。未来的 C++ 版本可能也会提供静态 if。 @Qwertie:当然,问题是“什么是可能的”,而不是“什么是理智的”:D 请修复链接。 @nbro:谢谢,这样做了 :)【参考方案10】:

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

【讨论】:

以上是关于C++ 和 D 中的元编程的主要内容,如果未能解决你的问题,请参考以下文章

为啥学习 Ruby 中的元编程和特征类很重要?

Python中的元编程

Python中的元编程

Python中的元编程

Python中的元编程

C++ 模板元编程的最佳介绍? [关闭]