C++模板是伪装的宏吗?

Rod*_*ddy 54 c++ macros templates

我已经用C++编程了几年,我已经使用了很多STL并且已经创建了我自己的模板类几次以了解它是如何完成的.

现在我正在尝试将模板更深入地集成到我的OO设计中,一个唠叨的想法不断回到我身边:它们只是一个宏,真的......你可以使用#defines实现(而不是UGLY)auto_ptrs,如果你真的想要.

这种思考模板的方式有助于我理解我的代码将如何实际工作,但我觉得我必须以某种方式忽略这一点.宏是邪恶的化身,但"模板元编程"风靡一时.

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

  • 在您不期望它们的地方难以理解的编译器错误?
  • 代码臃肿?
  • 跟踪代码有困难吗?
  • 设置调试器断点?

Fer*_*cio 50

宏是一种文本替换机制.

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

  • 这并不能真正解释相关差异。“苹果不就是橙子吗?” -- 错误的答案:“不,橙子是柑橘类水果,但苹果是由 *Malus Domestica* 生产的。” -- 好的答案:“不,橙子是橙色的,充满酸汁和一些果肉,而苹果是红色或绿色的,含有更致密的果肉和少量的甜汁。” (4认同)

Jef*_*f B 31

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

是的 - 它们都是一样的:代码生成工具.

宏是一种原始形式,没有太多的编译器实施(比如在C中做对象 - 它可以完成,但它并不漂亮).模板更高级,并且有更好的编译器类型检查,错误消息等.

但是,每个人都有其他人没有的优势.

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

另一方面,模板可以完成一些真正的FUNKY事情,这是宏不可能实现的.例如:

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!");
}
Run Code Online (Sandbox Code Playgroud)

该模板允许编译器动态地动态创建和使用模板的类型安全实例.编译器实际上在编译时执行模板参数数学运算,在每个唯一结果所需的位置创建单独的类.隐含的单位<1,-1>(距离/时间=速度)类型是在条件中创建和比较的,但从未在代码中明确声明.

显然,大学里的某个人已经定义了40多个参数的模板(需要参考),每个参数代表不同的物理单位类型.考虑一下这类课程的类型安全性,仅针对您的数字.

  • 声明两个变量:距离d; 时间t; 如果距离和时间都是双精度数,则语句(d = t)和表达式(d == t)都是有效的.模板可以防止这种情况 - 为数值提供类型安全性. (3认同)
  • 我知道你想做什么,直到我到达“Unit&lt;d+d2,t+t2&gt;”,当时我失去了情节。你能解释一下它想要做什么以及它比“typedef double Distance”/“typedef double Time”有什么优势,这似乎会给出相同的结果? (2认同)

rle*_*lut 30

它们由编译器解析,而不是由编译器之前运行的预处理器解析.

这是MSDN所说的:http: //msdn.microsoft.com/en-us/library/aa903548(VS.71).aspx

以下是宏的一些问题:

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

如果这对你来说还不够,我不知道是什么.

  • MSDN链接使用Min的模板,这几乎是最坏的"坏例子".请参阅Scott Meyer关于Min/Max模板的论文.http://www.aristeia.com/Papers/C++ReportColumns/jan95.pdf (17认同)
  • 显然你在技术上是正确的,但是说一个是由预处理器处理的,而另一个是由编译器处理的,并没有说明为什么一个比另一个好. (3认同)
  • @Roddy你不公平.Min作为模板在其不完美状态下相当容易理解,并提供比宏更好的保护.Alexandrescu有一个解决最小/最大问题的解决方案,但它非常复杂,对我来说太复杂了. (3认同)

Gre*_*osz 22

答案是这么久,我不能总结一切,但:

  • 例如,宏在功能模板没有确保类型安全时:编译器无法验证宏参数是否兼容类型 - 在实例化函数模板时编译器知道是否定义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现场直播.

参考文献:


GMa*_*ckG 12

在最基本的层面上,是的,模板只是宏替换.但是,通过这种方式思考,你就会跳过很多东西.

考虑模板专业化,据我所知,你不能用宏来模拟.这不仅允许某些类型的特殊实现,它还是模板元编程中的关键部分之一:

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

template <>
struct is_void<void>
{
    static const bool value = true;
}
Run Code Online (Sandbox Code Playgroud)

这本身就是你能做很多事情的一个例子.模板本身是图灵完备的.

这忽略了非常基本的东西,例如范围,类型安全性,以及宏的问题.


cat*_*try 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
Run Code Online (Sandbox Code Playgroud)


And*_*isi 9

C++模板有点像Lisp宏(不是C宏),因为它们在已经解析的代码版本上运行,它们允许您在编译时生成任意代码.不幸的是,你正在编写类似于原始Lambda演算的东西,所以像循环这样的高级技术有点麻烦.有关所有血腥细节,请参阅Krysztof Czarnecki和Ulrich Eisenecker的Generative Programming.


Rya*_*yan 6

如果您正在寻找对该主题的更深入的处理,我可以将您转向每个人最喜欢的C++仇恨.这个人知道并且讨厌比我梦想的更多的C++.这同时使FQA令人难以置信的炎症和优秀的资源.

  • 除了每当我看到FQA时,我都意识到他真的不知道他在说什么.他的许多抱怨是由于滥用C++. (10认同)

moo*_*dow 5

  • 模板是类型安全的.
  • 模板化对象/类型可以是命名空间,是类的私有成员等.
  • 模板化函数的参数不会在整个函数体中复制.

这些确实是一个大问题,可以防止大量的错误.


Jer*_*fin 5

不,这不可能。预处理器对于像 T 的容器这样的一些事情来说(勉强)足够了,但是对于模板可以做的很多其他事情来说,它根本不够。

有关一些真实示例,请阅读Andre Alexandrescu 的Modern C++ Programming或Dave Abrahams 和 Aleksey Gurtovoy 的C++ Metaprogramming。这两本书中几乎没有任何东西可以用预处理器模拟到极小的程度。

编辑:就目前typename而言,要求非常简单。编译器无法始终确定依赖名称是否引用类型。Usingtypename显式地告诉编译器它引用了一个类型。

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'
Run Code Online (Sandbox Code Playgroud)

typename 告诉编译器特定名称旨在引用类型,而不是变量/值,因此(例如)您可以定义该类型的其他变量。