17

恕我直言,哎呀,设计模式很有意义,我已经能够实际应用它们。

但是当谈到现代 C++ 类型的“通用编程/元编程” 时,我感到很困惑。

——它是一种新的编程/设计范式吗?

——是否仅限于“图书馆开发”?如果不是,什么设计/编码情况需要使用元编程/通用编程。

-- 使用模板是否意味着我在做泛型编程?

我在这个话题上搜索了很多,但没有完全掌握大局。另见这篇文章


在阅读了下面的讨论之后,到目前为止,我确信(可能仍然不正确):

a) 通用编程和元编程是两个不同的概念。

4

9 回答 9

30

元编程是一个非常奇特的话题。了解它很有趣,它是一个强大的工具,有时您可能会发现它很有用。但它永远不会是您工具箱中最常用的工具。有时,您可能希望代码作用于具有不同属性的一系列不相关类型,这就是元编程的用武之地。通过一些技巧,您可以编写仅在参数类型为整数时才可用的函数的重载,或者如果它是一个指针,或者它是 X、Y 或 Z 类型(可能忽略 Z 上的常量)。

它本质上是使用类型进行编程。通常,您的程序可以执行诸如取两个数字并产生第三个数字之类的操作,或者告诉您一个数字是否满足某些要求。元编程可以采用两种类型并产生第三种类型,或者告诉您一种类型是否满足某些要求。是的,它可能在图书馆开发中最有用。但是话又说回来,大多数代码都可以被认为是库代码。您可以说 main() 函数之外的所有内容都是库代码。

通常,如果您想通过元编程解决问题,您可能需要使用相关的 boost 库来完成繁重的工作。Boost.TypeTraits 当然还有 Boost.Mpl 可以真正为你简化事情。但这不是你需要知道的东西,也不是你可能经常需要的东西。

泛型编程是相关的(并且可能在某些情况下使用底层元编程变得真正通用,例如标准库使用元编程将原始指针转换为有效的迭代器,这是“迭代器”概念泛型所必需的) ,但不完全一样。而且它的使用范围更广。

每次实例化 astd::vector时,都会使用泛型编程。每次您使用一对迭代器来处理一系列值时,您就使用了泛型编程。泛型编程就是你的代码应该尽可能通用,并且无论放入什么类型都应该工作。std::vector 不需要包含的类型来实现“ICanBeContained”接口(请记住 Java 是如何要求所有内容都从 Object 派生以便将其存储在容器类中?这意味着原始类型被装箱,并且我们失去了类型安全。这不是通用的,这是一个毫无意义的限制。)

使用迭代器对序列进行迭代的代码是通用的,并且可以与任何类型的迭代器一起使用,甚至可以与普通指针一起使用。

泛型编程非常有用,通常可以在很大程度上取代 OOP。(见上面的例子。如果我能避免这个限制,我为什么要编写一个需要包含的类型来实现接口的容器?)

通常,当您在 OOP 中使用接口时,不允许类型在运行时更改(当然这种情况有时也会发生),而是允许您在编译时交换另一种类型(可能注入一个在测试期间模拟对象,而不是使用成熟的实现),或者只是为了解耦两个类。通用编程可以做到这一点,而无需您进行定义和维护接口的繁琐工作。在这些情况下,泛型编程意味着您必须编写和维护更少的代码,并且您可以获得更好的性能和更好的类型安全性。所以,是的,你绝对应该对泛型编程感到宾至如归。C++ 不是一个很好的 OOP 语言。如果您想严格遵守 OOP,则应该切换到 Java 或其他更偏向于 OOP 的语言。C++允许您编写 OO 代码,但这通常不是最佳解决方案。几乎整个标准库都依赖于泛型编程,而不是 OOP,这是有原因的。标准库中几乎没有继承或多态性。他们不需要它,没有它,代码变得更简单易用、更强大。

为了回答您的其他问题,是的,通用编程几乎是一个独立的范例。模板元编程不是。它是一种用于操作类型系统的相当具体的技术,并且非常擅长解决少数问题。要被认为是一种范式,我认为它必须更普遍有用,并且您可以将其用于基本上所有事物,例如函数式、OO 或泛型编程。

我认为 xtofl 确实做到了:通用编程就是让你的代码不知道类型。(一个 std::vector 不关心,或者需要知道其中存储了什么类型。它只是工作。)

另一方面,元编程是关于类型计算的。给定类型 T0 和 T1,我们可以定义一个类型 T2,就像我们可以定义的那样,给定整数 N0 和 N1,我们可以定义一个 N2,它是 N0 和 N1 的和。

Boost.Mpl 库有一个明显的例子。在您的正常代码中,如果您有整数 N0、N1 和 N2,您可以创建一个包含这三个值的 std::vector。然后我可以使用其他算法来计算索引,然后提取存储在向量中该位置的值。

给定类型 T0、T1 和 T2,我们可以创建一个包含这三种类型的 mpl::vector 。我现在可以使用其他算法在编译时计算索引,并提取存储在向量中该位置的类型。

于 2009-06-11T10:49:32.927 回答
12

您确实必须区分泛型编程(有点不知道类型)和元编程,这是在类型系统中进行计算的完全合法的方式。

当您在大量代码中找到可概括的模式时,通用编程非常有用。如果模式的差异不仅在于变量值,还在于不同的类型,泛型编程对于重构代码很有用。

元编程适用于完全不同的领域。确实,该领域非常新,需要您自己进行一些探索。也有趣!

一种非常有用且常见的元编程模式是 Traits/Policy 概念。

于 2009-06-11T10:52:33.293 回答
10

C++ 模板元编程是一种强大的代码混淆技术,适用于一系列应用程序:

  • 当您想编写团队中其他人无法理解的代码时
  • 当您想要编写后 7 天无法理解的代码时
  • 当代码性能对您来说比可维护性更重要时
  • 当您希望能够将“模板元编程”列为您的简历中的一项技能时。
  • 当您需要编写不太可能在许多实际编译器上运行的代码时
  • 如果您宁愿吃剃须刀片也不愿使用预处理器宏

另一种情况:

  • 如果你想知道 Boost 库是如何工作的,或者你想为它们做出贡献。

“通用编程”(想想 STL 容器、auto_ptrs 等)之间的区别是 C++ 模板旨在完成的功能和“模板元编程”(使用模板系统让编译器为您有效地“运行算法”)是重要的。

我都赞成第一个,但很难从后者中看到现实世界的好处。

于 2009-06-11T09:58:13.710 回答
5

对于高级模板和技术,我推荐:Modern C++ Design,作者 Andrei Alexandrescu

C++ 是一种严格的语言,几乎没有运行时自省功能。很多你会遇到的麻烦都可以通过模板来解决。此外,模板语言是图灵完备的,因此可以生成复杂的数据结构并在编译时预先计算值等。在许多情况下,它可能比它值得的麻烦更多。

于 2009-06-11T09:46:16.527 回答
4

一个尚未给出的答案:虽然通用编程和元编程确实是彼此完全不同的技术,但它们都旨在解决(本质上)相同的问题:如何摆脱样板。这些也体现在软件工程原则的两个首字母缩写词中:DRY(不要重复自己)和 DIE(重复是邪恶的)这是对Occam's Razor的现代改写,即“实体不得在必要时相乘”(entia non sunt multiplicanda praeter necessitatem)。[谁知道 14 世纪的逻辑学家可以想出对 21 世纪软件设计有用的东西?]。

无论如何,这两种技术的重点是找到将“几乎相同”的代码统一为单个参数化代码的方法。在元编程案例中,您正在使用代码生成,因此该技术涉及将一段通用代码(也称为模板,或更一般地说是代码生成函数)专门用于特定案例在通用编程案例中,您尝试使用类型系统来识别不同的代码段是“相同的”,然后使用基于类型的分派进行专门化。

It turns out that these two techniques can even be used together to do rather fancy things. See Multi-stage programming with functors and monads: eliminating abstraction overhead from generic code for an example of this. But if you think that the STL is scary, better not look at it...

于 2010-06-17T01:07:01.510 回答
3

什么时候:

模板元编程是一种编写由编译器解释的程序的方式。它是更高的抽象级别。你可以用它做各种奇怪和时髦的事情。boost 库包含许多示例:

  • 不喜欢 c++ 是静态类型的吗?使用 boost::any。
  • 喜欢 lambda 演算,但你必须 C++ 吗?使用 boost:lambda。
  • 有 EBNF 并需要解析器?使用 boost::spirit。

为什么:

这个很酷。你可以打动你的约会对象(也许)。

于 2009-06-11T11:04:18.697 回答
1

当你还有很长的时间想玩的时候。

当您需要提升算法并且不希望虚拟函数调用产生开销时,您可以将运行时绑定替换为编译时绑定。

于 2009-06-11T09:46:19.133 回答
1

简单的例子:

template<typename T>
void
swap(T& var1, T& var2)
{
  T var3 = var1;
  var1 = var2;
  var2 = var3;
}

int a = 2;
int b = 3;
swap(a, b);

float c = 400.0f;
float d = 500.0f;
swap(c, d);

交换具有相同类型的 2 个变量的值。

使用模板,我们避免显式地为各种类型组合创建函数,如下所示:

void
swap(int& var1, int& var2)
{
  int var3 = var1;
  var1 = var2;
  var2 = var3;
}

void
swap(float& var1, float& var2)
{
  float var3 = var1;
  var1 = var2;
  var2 = var3;
}

上面的函数将由编译器自动创建,在我的示例中,如果在代码中的某处使用intfloat变量调用 swap()。

于 2009-06-11T09:51:13.083 回答
0

我认为,如果您不是库开发人员,那么最好忘记模板元编程。我相信,它在生产代码中毫无用处,带来更多问题,然后是好处:“一旦你开始使用它,你就会失去与这个解决方案相关的灵活性。在你使用模板编译代码之后很难摆脱它开始使用它。这是一个问题,因为模板不够灵活“。

PS当然我不是指模板容器,交换,...​​...而是类似Alexandrescu的代码

于 2009-06-11T09:53:53.640 回答