C++模板只是伪装成宏的东西吗?

67

我已经用C++编程几年了,使用STL相当多,并且创建过自己的模板类来查看其工作方式。

现在我正在尝试将模板更深入地集成到我的OO设计中,并且总有一个令人讨厌的想法萦绕在我脑海里:它们只是宏,真的...如果您真的想这样做,可以使用#define实现(相当丑陋的)auto_ptrs。

这种关于模板的思考方式帮助我理解我的代码实际上将如何工作,但我感觉我一定某些方面上错了。宏被认为是邪恶的化身,然而“模板元编程”却非常流行。

那么,真正的区别是什么?模板如何避免#定义引导您进入的危险,比如:

  • 出现在不应出现错误的地方不可读的编译器错误?
  • 代码膨胀?
  • 追踪代码的难度?
  • 设置调试器断点?

1
你为什么相信“模板元编程非常流行”? - Partial
不,它们并不是。 :-) 而且你的问题非常有偏见,因为模板对你提到的所有问题都没有帮助。相反,它们有助于解决一整类你方便忽略的问题。这个问题预设了答案,如果我有权力的话,我会认为它是主观和争论性的,并将其取消资格。 - Omnifarious
@Omnifarious - 这是对这个问题的评论,还是与我的合并的那个问题?这场对一年前的问题的争论让我感到头疼... - Roddy
4
模板元编程是非常棒的(非常流行)——至少我认为它在可移植低级编程领域有用武之地。自从 ANSI-C 中出现了标记串联以来,人们一直在使用预处理器(第一级)元编程,而利用 C++ 的模板机制,其潜力是无限的——这正是大势所趋;然而,我们缺乏工具,这对事情产生了阻碍。 - Hassan Syed
25个回答

53

宏是一种文本替换机制。

模板是一种在编译时执行并集成到C++类型系统中的功能图灵完备语言。您可以将其视为语言的插件机制。


10
这并没有真正解释相关的差异。“苹果不就是橙子吗?”——不好的回答:“不,橙子是柑橘类水果而苹果是由* Malus domestica *生产。”——好的回答:“不,橙子是橙色的,含有酸汁和一些果肉,而苹果是红色或绿色的,含有更密实的果肉和较少量更甜的汁液。” - user253751
@user253751 说实话,这是一个非常模糊的问题,很难回答得好。 - quant

40

这些代码被编译器解析,而不是在编译器之前运行的预处理器解析。

MSDN对此的解释如下: http://msdn.microsoft.com/en-us/library/aa903548(VS.71).aspx

以下是宏的一些问题:

  • 编译器无法验证宏参数是否具有兼容类型。
  • 该宏会在没有任何特殊类型检查的情况下展开。
  • i和j参数将被评估两次。例如,如果任何一个参数具有后增变量,则增量将执行两次。
  • 由于宏是由预处理器展开的,因此编译器错误消息将引用展开的宏,而不是宏定义本身。此外,在调试期间,该宏也将以展开形式显示。

如果这还不足够让你明白,那我也不知道什么能让你明白了。


19
MSDN链接使用了一个Min的模板,这几乎是一个“糟糕的示例”。请参考Scott Meyer关于Min/Max模板的论文。http://www.aristeia.com/Papers/C++ReportColumns/jan95.pdf - Roddy
6
显然,从技术上讲你是正确的,但仅仅说一个被预处理器处理而另一个被编译器处理并不能说明其中一个比另一个更好的原因。 - Roel
3
@Roddy,你有些不公平。即使Min模板在其不完美的状态下也相当简单易懂,并比宏提供更好的保护。Alexandrescu已经为min/max问题提供了解决方案,但它非常复杂,对我来说过于复杂了。 - rlerallut
1
@Roel 嗯...这就是为什么我引用MSDN的原因。它们非常明确:类型检查、双重增量保护、错误消息。它们来自于它在编译器内部处理的事实,你不能在预处理器中完成它。谁关心模板是图灵完备语言呢? - rlerallut
1
@rlerallut - 是的,Min函数很容易理解,但是它也没有什么用处,正如SM所提到的那样。正如他所说“真是够了,我们在这里讨论最大值函数!一个概念上如此简单的函数怎么会引起这么多麻烦呢?” - Roddy
显示剩余3条评论

34

这里有很多评论试图区分宏和模板。

是的,它们都是同样的东西:代码生成工具。

宏是一种原始形式,没有太多的编译器强制执行(就像在 C 中使用对象一样 - 可以做到,但不好看)。 模板更先进,并且具有更好的编译器类型检查、错误消息等功能。

然而,每种方法都有其优点。

模板只能生成动态类类型 - 宏可以生成几乎任何你想要的代码(除了另一个宏定义)。 宏非常有用,可以将结构化数据的静态表嵌入到您的代码中。

另一方面,模板可以实现一些真正奇特的事情,这是宏所不能实现的。例如:

template<int d,int t> class Unit
{
    double value;
public:
    Unit(double n)
    {
        value = n;
    }
    Unit<d,t> operator+(Unit<d,t> n)
    {
        return Unit<d,t>(value + n.value);
    }
    Unit<d,t> operator-(Unit<d,t> n)
    {
        return Unit<d,t>(value - n.value);
    }
    Unit<d,t> operator*(double n)
    {
        return Unit<d,t>(value * n);
    }
    Unit<d,t> operator/(double n)
    {
        return Unit<d,t>(value / n);
    }
    Unit<d+d2,t+t2> operator*(Unit<d2,t2> n)
    {
        return Unit<d+d2,t+t2>(value * n.value);
    }
    Unit<d-d2,t-t2> operator/(Unit<d2,t2> n)
    {
        return Unit<d-d2,t-t2>(value / n.value);
    }
    etc....
};

#define Distance Unit<1,0>
#define Time     Unit<0,1>
#define Second   Time(1.0)
#define Meter    Distance(1.0)

void foo()
{
   Distance moved1 = 5 * Meter;
   Distance moved2 = 10 * Meter;
   Time time1 = 10 * Second;
   Time time2 = 20 * Second;
   if ((moved1 / time1) == (moved2 / time2))
       printf("Same speed!");
}

模板允许编译器在运行时动态地创建和使用类型安全的模板实例。编译器实际上在编译时进行模板参数计算,为每个唯一结果需要创建单独的类。在条件语句中有一个隐含的Unit<1,-1>(距离/时间=速度)类型,在代码中从未明确声明。

显然,某个大学的某人已经定义了这种带有40多个参数的模板(需要参考),每个参数代表不同的物理单位类型。考虑一下这种类的类型安全性,仅就你的数字而言。


2
我对你的意图有一些了解,直到看到“Unit<d+d2,t+t2>”时,我就有点懵了。你能解释一下它的作用以及相比于“typedef double Distance”/“typedef double Time”所带来的优势吗?因为它们似乎会得到相同的结果。 - Roddy
4
声明两个变量: 距离 d; 时间 t;如果距离和时间都是双精度浮点数,语句 (d = t) 和表达式 (d == t) 都是有效的。模板可以防止这种情况-为数字值提供类型安全性。 - Jeff B
1
啊!谢谢。我自己肯定想不到这个! - Roddy
1
@@Roddy:+1 给“推断出” - Chubsdad
1
一个很好的模板函数无法实现的例子是包含行信息的日志记录函数,其中在代码中被调用:#define log(...) someLoggingFunction(__LINE__, __VA_ARGS__) - Aconcagua
显示剩余4条评论

24
答案太长了,我无法总结所有内容,但是:
  • 例如宏不保证类型安全,而函数模板可以:
    编译器无法验证宏参数是否具有兼容的类型--在函数模板实例化时,编译器知道intfloat是否定义了operator+
  • 模板为元编程(简单来说,在编译时评估事物并做出决策)打开了大门:
    在编译时,可以知道一个类型是整数还是浮点数;它是否是指针或是否具有const限定符等... 请参见C++0x中即将推出的"type traits".
  • 类模板具有部分特化
  • 函数模板具有显式完全特化,在你的示例中,add<float>(5, 3);add<int>(5, 3);可以有不同的实现方式,这在宏中是不可能的
  • 宏没有任何作用域
  • #define min(i, j) (((i) < (j)) ? (i) : (j)) - 参数ij会被评估两次。例如,如果任何一个参数具有后增变量,则增量会执行两次
  • 由于宏是由预处理器展开的,因此编译器错误消息将引用展开的宏,而不是宏定义本身。此外,在调试期间,宏将以展开形式显示
  • 等等...

注意:在一些罕见情况下,我更喜欢依赖可变参数宏,因为直到C++0x成为主流,才会有可变模板。 C++11已经发布。

参考资料:


我们这里有一个生气的踩负者;-) 我也不知道我被踩了什么;-) - Michael Krelin - hacker
你可能会受到其他回答者的负面评价,因为他们想让自己的答案先出现在列表顶部,这样更有可能被标记为被采纳的答案。 - Shaggy Frog
现在问题已经合并,我感觉自己很蠢——我的评论似乎在指责那个发布者从未做过的事情;-) - Michael Krelin - hacker
哦,Gregory,你的回答很有帮助,提出了一些好观点。+1 - Roddy
@Gregory,谢谢 - 那解释了很多问题。[不,我不是幽灵投票者。我的时间有更好的利用!] - Roddy
显示剩余7条评论

12

从基础层面上来说,模板确实只是宏替换。但是如果你这样想就会忽略掉很多东西。

考虑模板特化,据我所知,你无法使用宏模拟它。这不仅允许对某些类型进行特殊实现,而且是模板元编程的关键部分之一:

template <typename T>
struct is_void
{
    static const bool value = false;
}

template <>
struct is_void<void>
{
    static const bool value = true;
}

这只是许多你可以做的事情之一的例子。模板本身就是图灵完备的

这忽略了非常基础的东西,比如作用域、类型安全和宏更加混乱。


10

不行。一个简单的反例:模板遵循命名空间,宏会忽略命名空间(因为它们是预处理器语句)。

namespace foo {
    template <class NumberType>
    NumberType add(NumberType a, NumberType b)
    {
        return a+b;
    }

    #define ADD(x, y) ((x)+(y))
} // namespace foo

namespace logspace 
{
    // no problemo
    template <class NumberType>
    NumberType add(NumberType a, NumberType b)
    {
        return log(a)+log(b);
    }

    // redefintion: warning/error/bugs!
    #define ADD(x, y) (log(x)+log(y))

} // namespace logspace

10

C++模板类似于Lisp宏(不是C宏),因为它们操作已解析的代码版本,并让您在编译时生成任意代码。不幸的是,您正在编写类似于原始Lambda演算的东西,因此像循环这样的高级技术有点麻烦。有关所有令人毛骨悚然的细节,请参见Krysztof Czarnecki和Ulrich Eisenecker的生成式编程


6

如果你想更深入地了解这个主题,我可以向大家介绍一个众所周知的C++反对者。这个人比我更了解和憎恨C++,同时也使得FQA变得极具争议性并且是一种极佳的资源。


10
除了每次看 FQA 时,我意识到他真的不知道在说什么。他的许多抱怨都是由于误用 C++ 导致的。 - David Thornley

5
不,这是不可能的。预处理器对于像T容器之类的一些东西(勉强)足够了,但对于模板可以做的很多其他事情来说,它根本不足够。
举个真实的例子,阅读Andre Alexandrescu的《现代C++编程》或Dave Abrahams和Aleksey Gurtovoy的《C++元编程》,几乎没有任何一本书中所做的事情可以被模拟到比预处理器更少的程度。
编辑:就typename而言,要求非常简单。编译器不能总是确定依赖名称是引用类型还是其他类型。显式使用typename告诉编译器它是引用类型。
struct X { 
    int x;
};

struct Y {
    typedef long x;
};

template <class T>
class Z { 
    T::x;
};

Z<X>; // T::x == the int variable named x
Z<Y>; // T::x == a typedef for the type 'long'

typename关键字告诉编译器特定的名称意图是引用类型,而不是变量/值,所以您可以定义其他该类型的变量。


5
  • 模板是类型安全的。
  • 模板对象/类型可以进行命名空间、成为类的私有成员等操作。
  • 模板函数的参数不会在整个函数体中重复出现。

这些都非常重要,能够预防很多错误。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接