64

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

现在我正在尝试将模板更深入地集成到我的 OO 设计中,一个烦人的想法不断地回到我的脑海:它们只是一个宏,真的……你可以使用 #defines 实现(相当丑陋)auto_ptrs,如果你真的想要。

这种对模板的思考方式有助于我理解我的代码实际上是如何工作的,但我觉得我一定是以某种方式错过了这一点。宏意味着邪恶的化身,但“模板元编程”风靡一时。

那么,真正的区别是什么?以及模板如何避免#define 导致您陷入的危险,例如

  • 在您不希望出现的地方出现难以理解的编译器错误?
  • 代码膨胀?
  • 追溯代码困难?
  • 设置调试器断点?
4

25 回答 25

51

宏是一种文本替换机制。

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

于 2008-10-07T20:46:51.070 回答
38

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

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

以下是宏的一些问题:

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

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

于 2008-10-07T20:44:34.787 回答
34

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

是的 - 它们都是同一件事:代码生成工具。

宏是一种原始形式,没有太多的编译器强制执行(就像在 C 中做 Objects - 它可以完成,但它并不漂亮)。模板更高级,并且有更好的编译器类型检查、错误消息等。

然而,每个人都有对方没有的优势。

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

另一方面,模板可以完成一些用宏无法实现的真正 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!");
}

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

显然,大学里有人用 40 多个参数(需要参考)定义了这种模板,每个参数代表不同的物理单元类型。考虑一下那种类的类型安全,只是为了你的数字。

于 2008-10-07T21:34:37.150 回答
22

答案太长了,我无法总结所有内容,但是:

  • 例如,宏不能确保类型安全,而函数模板可以:
    编译器无法验证宏参数的类型是否兼容——在函数模板被实例化时,编译器也知道是否intfloat定义operator +
  • 模板为元编程打开了大门(简而言之,在编译时评估事物并做出决定):
    在编译时可以知道类型是整数还是浮点数;无论是指针还是 const 限定等...请参阅即将发​​布的 c++0x 中的“类型特征”
  • 类模板具有部分特化
  • 函数模板具有显式的完全专业化,在您的示例中,其实现方式可能与宏无法add<float>(5, 3);实现的方式不同add<int>(5, 3);
  • 宏没有任何作用域
  • #define min(i, j) (((i) < (j)) ? (i) : (j))-ij参数被评估两次。例如,如果任一参数具有后增量变量,则增量执行两次
  • 因为宏由预处理器扩展,编译器错误消息将引用扩展的宏,而不是宏定义本身。此外,宏将在调试期间以扩展形式显示
  • ETC...

注意:在极少数情况下,我更喜欢依赖可变参数宏,因为在 c++0x 成为主流之前,没有可变参数模板之类的东西。 C++11已上线。

参考:

于 2009-12-14T18:20:02.883 回答
12

在非常基本的层面上,是的,模板只是宏替换。但是你这样想就跳过了很多事情。

考虑模板专业化,据我所知,您无法使用宏进行模拟。这不仅允许特定类型的特殊实现,它还是模板元编程中的关键部分之一:

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

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

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

这忽略了非常基本的东西,例如范围,类型安全,并且宏更混乱。

于 2009-12-14T18:21:20.567 回答
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
于 2009-12-14T18:20:11.177 回答
9

C++ 模板有点像 Lisp 宏(不是 C 宏),因为它们对已经解析的代码版本进行操作,并且它们允许您在编译时生成任意代码。不幸的是,您正在使用类似于原始 Lambda 演算的东西进行编程,因此循环等高级技术有点麻烦。有关所有血腥细节,请参阅Krysztof Czarnecki 和 Ulrich Eisenecker 的Generative Programming

于 2008-10-07T20:50:05.290 回答
6

如果您正在寻找对该主题的更深入处理,我可以将您变成每个人最喜欢的 C++ 仇恨者。这个人知道和讨厌的 C++ 比我想象的要多。这同时使 FQA 非常具有煽动性和极好的资源。

于 2008-10-07T20:53:47.577 回答
5
  • 模板是类型安全的。
  • 模板化的对象/类型可以命名空间,成为类的私有成员等。
  • 模板化函数的参数不会在整个函数体中复制。

这些确实很重要,可以防止大量错误。

于 2009-12-14T18:21:47.870 回答
5

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

对于一些真实的例子,请阅读Andre Alexandrescu 的Modern C++ Programming或 Dave Abrahams 和 Aleksey Gurtovoy 的C++ Metaprogramming。几乎任何一本书中所做的任何事情都不能用预处理器模拟到极小的程度。

编辑:就目前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告诉编译器特定名称旨在引用类型,而不是变量/值,因此(例如)您可以定义该类型的其他变量。

于 2009-12-14T18:24:03.040 回答
4

没有提到的是模板函数可以推断参数类型。

模板 <类型名 T>
无效函数(T t)
{
  T make_another = t;

有人可能会争辩说,即将推出的“typeof”运算符可以解决这个问题,但即使它也无法分解其他模板:

模板 <类型名 T>
无效函数(容器<T> c)

甚至:

模板 <tempate <typename> 类容器,typename T>
无效函数(容器<T> ct)

我也觉得专业化的主题没有得到足够的覆盖。这是宏不能做的一个简单示例:

模板 <类型名 T>
T min(T a, TB)
{
  返回 a < b ? 一:乙;
}

模板<>
char* min(char* a, char* b)
{
  如果 (strcmp(a, b) < 0)
    返回一个;
  别的
    返回 b;
}

空间太小,无法进入类型专业化,但就我而言,你可以用它做些什么,令人兴奋。

于 2008-10-07T23:09:58.973 回答
4

这个答案旨在阐明 C 预处理器以及它如何用于泛型编程


它们在某些方面是因为它们启用了一些相似的语义。C 预处理器已用于启用通用数据结构和算法(请参阅令牌连接)。然而,在不考虑 C++ 模板的任何其他特性的情况下,它使整个通用编程游戏更易于阅读和实现。

如果有人想查看核心 C 仅通用编程的实际操作,请阅读libevent源代码 -这里也提到了这一点。实现了大量容器/算法,并在单个头文件中完成(非常易读)。我真的很佩服这一点,C++ 模板代码(我更喜欢它的其他属性)非常冗长。

于 2009-12-14T18:23:34.713 回答
3

让我们尝试原始示例。考虑

#define min(a,b) ((a)<(b))?(a):(b)

调用为

c = min(a++,++b);

当然,真正的区别更深,但这应该足以放弃与宏的相似之处。

编辑:不,你不能确保宏的类型安全。您将如何min()为定义小于比较(即operrator<)的每种类型实现类型安全?

于 2009-12-14T18:26:47.497 回答
2

宏存在一些基本问题。

首先,他们不尊重范围或类型。如果我有#define max(a, b)...,那么每当我的max程序中有令牌时,无论出于何种原因,它都会被替换。如果它是变量名或嵌套范围内的深处,它将被替换。这可能会导致难以发现的编译错误。相反,模板在 C++ 类型系统中工作。模板函数可以在范围内重复使用其名称,并且不会尝试重写变量名称。

其次,宏不能变化。模板std::swap通常只会声明一个临时变量并进行明显的分配,因为这是通常工作的明显方式。这就是宏的限制。这对于大型向量来说效率极低,因此向量具有swap交换引用而不是整个内容的特殊功能。(事实证明,这在普通 C++ 程序员不应该编写但确实使用的东西中非常重要。)

第三,宏不能进行任何形式的类型推断。首先你不能写一个通用的交换宏,因为它必须声明一个类型的变量,而且它不知道类型可能是什么。模板是类型感知的。

模板强大的一个很好的例子是最初称为标准模板库,它在标准中作为容器、算法和迭代器。看看它们是如何工作的,并尝试考虑如何用宏替换它。Alexander Stepanov 研究了多种语言来实现他的 STL 想法,并得出结论认为带有模板的 C++ 是唯一可以使用的语言。

于 2009-12-14T21:47:56.300 回答
2

Templates can do a lot more than the macro preprocessor is able to do.

E.g. there are template specializations: If this template is instanciated with this type or constant, than do not use the default implementation, but this one here...

... templates can enforce that some parameters are of the same type, etc...


Here are some sources You might want to look at:

  • C++ templates by Vandervoorde and Jossutis. This is the best and most complete book about templates I know.
  • The boost library consists almost entirely of template definitions.
于 2008-10-07T21:11:05.997 回答
2

模板是类型安全的。使用定义,您可以拥有可以编译但仍无法正常工作的代码。

宏在编译器获取代码之前扩展。这意味着您将收到扩展代码的错误消息,并且调试器只能看到扩展版本。

使用宏,某些表达式总是有可能被计算两次。想象一下将 ++x 之类的东西作为参数传递。

于 2008-10-07T20:48:49.427 回答
2

模板可以放在命名空间中,也可以是类的成员。宏只是一个预处理步骤。基本上,模板是该语言的一流成员,可以与其他所有内容配合得很好(更好?)。

于 2008-10-07T20:48:49.537 回答
2

这不是一个答案,而是已经陈述的答案的结果。

与需要编程的科学家、外科医生、图形艺术家和其他人一起工作——但不是也不会成为专业的全职软件开发人员——我发现偶尔的程序员很容易理解宏,而模板似乎需要更高的只有通过更深入和持续的 C++ 编程经验才能实现抽象思维水平。它需要许多使用模板作为有用概念的代码的实例,才能使该概念足够有意义以供使用。虽然这可以说是任何语言功能,但模板的经验量与专业的临时程序员可能从他们的日常工作中获得的差距更大。

一般的天文学家或电子工程师可能很好地理解宏,甚至可能理解为什么应该避免使用宏,但对于日常使用的模板却不够好。在这种情况下,宏实际上更好。当然,也有很多例外。一些物理学家围绕专业软件工程师转圈,但这并不典型。

于 2008-10-08T00:57:04.433 回答
2

提供 typename 关键字以启用上下文无关的嵌套 typdef。这些是允许将元数据添加到类型(尤其是内置类型,如指针)的特征技术所需要的,这是编写 STL 所必需的。typename 关键字在其他方面与 class 关键字相同。

于 2009-12-14T18:25:14.103 回答
2

尽管模板参数经过类型检查并且模板比宏有许多优点,但模板与宏非常相似,因为它们仍然基于文本替换。编译器不会验证您的模板代码是否有意义,除非您给它类型参数以进行替换。只要您实际上没有调用它,Visual C++ 就不会抱怨这个函数:

template<class T>
void Garbage(int a, int b)
{
    fdsa uiofew & (a9 s) fdsahj += *! wtf;
}

编辑:此示例仅适用于 Visual C++。在标准C++ 中,您的模板代码实际上在使用模板之前就被解析为语法树,因此该示例被 VC++ 接受,但不被 GCC 或 Clang 接受。(当我尝试将 VC++ 代码移植到 GCC 并且不得不处理我的非专业模板中的数百个语法错误时,我了解到这一点。)但是,语法树在语义上仍然不一定有意义。无论编译器如何,在您通过提供<template arguments>.

因此,一般来说,对于模板设计接受的给定类型参数类别,不可能知道您的模板代码是否能正常工作或编译成功。

于 2009-03-06T17:50:34.313 回答
2

模板理解数据类型。 宏没有。

这意味着您可以执行以下操作...

  • 定义可以采用任何数据类型的操作(例如,用于包装数字的操作),然后提供根据数据类型是整数还是浮点数选择适当算法的特化
  • 在编译时确定数据类型的各个方面,允许使用诸如模板推导数组大小之类的技巧,Microsoft 将其用于strcpy_s及其同类的 C++ 重载

此外,由于模板是类型安全的,因此可以想象有许多模板编码技术可以使用一些假设的高级预处理器来执行,但充其量是笨拙和容易出错的(例如,模板模板参数、默认模板参数、策略模板作为现代 C++ 设计中讨论)。

于 2009-12-14T18:33:51.523 回答
2

模板仅在最基本的功能上类似于宏。毕竟,模板作为宏的“文明”替代品被引入语言。但即使涉及到最基本的功能,相似之处也只是肤浅的。

然而,一旦我们了解了模板的更高级特性,例如专业化(部分或显式),与宏的任何明显相似性都会完全消失。

于 2009-12-14T18:36:16.463 回答
2

在我看来,宏是 C 语言的一个坏习惯。虽然它们对某些人有用,但当有 typedef 和模板时,我认为它们并没有真正的需要。模板是面向对象编程的自然延续。你可以用模板做更多的事情......

考虑这个...

int main()
{
    SimpleList<short> lstA;
    //...
    SimpleList<int> lstB = lstA; //would normally give an error after trying to compile
}

为了进行转换,您可以在相当完整的列表示例中使用称为转换构造函数和序列构造函数(看最后)的东西:

#include <algorithm>

template<class T>
class SimpleList
{
public:
    typedef T value_type;
    typedef std::size_t size_type;

private:
    struct Knot
    {
        value_type val_;
        Knot * next_;
        Knot(const value_type &val)
        :val_(val), next_(0)
        {}
    };
    Knot * head_;
    size_type nelems_;

public:
    //Default constructor
    SimpleList() throw()
    :head_(0), nelems_(0)
    {}
    bool empty() const throw()
    { return size() == 0; }
    size_type size() const throw()
    { return nelems_; }

private:
    Knot * last() throw() //could be done better
    {
        if(empty()) return 0;
        Knot *p = head_;
        while (p->next_)
            p = p->next_;
        return p;
    }

public:
    void push_back(const value_type & val)
    {
        Knot *p = last();
        if(!p)
            head_ = new Knot(val);
        else
            p->next_ = new Knot(val);
        ++nelems_;
    }
    void clear() throw()
    {
        while(head_)
        {
            Knot *p = head_->next_;
            delete head_;
            head_ = p;
        }
        nelems_ = 0;
    }
    //Destructor:
    ~SimpleList() throw()
    { clear(); }
    //Iterators:
    class iterator
    {
        Knot * cur_;
    public:
        iterator(Knot *p) throw()
        :cur_(p)
        {}
        bool operator==(const iterator & iter)const throw()
        { return cur_ == iter.cur_; }
        bool operator!=(const iterator & iter)const throw()
        { return !(*this == iter); }
        iterator & operator++()
        {
            cur_ = cur_->next_;
            return *this;
        }
        iterator operator++(int)
        {
            iterator temp(*this);
            operator++();
            return temp;
        }
        value_type & operator*()throw()
        { return cur_->val_; }
        value_type operator*() const
        { return cur_->val_; }
        value_type operator->()
        { return cur_->val_; }
        const value_type operator->() const
        { return cur_->val_; }
    };
    iterator begin() throw()
    { return iterator(head_); }
    iterator begin() const throw()
    { return iterator(head_); }
    iterator end() throw()
    { return iterator(0); }
    iterator end() const throw()
    { return iterator(0); }
    //Copy constructor:
    SimpleList(const SimpleList & lst)
    :head_(0), nelems_(0)
    {
        for(iterator i = lst.begin(); i != lst.end(); ++i)
            push_back(*i);
    }
    void swap(SimpleList & lst) throw()
    {
        std::swap(head_, lst.head_);
        std::swap(nelems_, lst.nelems_);
    }
    SimpleList & operator=(const SimpleList & lst)
    {
        SimpleList(lst).swap(*this);
        return *this;
    }
    //Conversion constructor
    template<class U>
    SimpleList(const SimpleList<U> &lst)
    :head_(0), nelems_(0)
    {
        for(typename SimpleList<U>::iterator iter = lst.begin(); iter != lst.end(); ++iter)
            push_back(*iter);
    }
    template<class U>
    SimpleList & operator=(const SimpleList<U> &lst)
    {
        SimpleList(lst).swap(*this);
        return *this;
    }
    //Sequence constructor:
    template<class Iter>
    SimpleList(Iter first, Iter last)
    :head_(0), nelems_(0)
    {
        for(;first!=last; ++first)
            push_back(*first);


    }
};

看看来自 cplusplus.com 的模板信息!您可以使用模板来执行所谓的特征,该特征使用具有类型等的文档。您可以使用模板做更多的事情,然后使用宏!

于 2009-09-08T19:11:32.863 回答
0

模板提供了某种程度的类型安全。

于 2008-10-07T20:46:51.023 回答
0

Templates are integrated in the language and are type-safe.

Tell me how you would do this with macros. This is heavy template metaprogramming.

https://www.youtube.com/watch?v=0A9pYr8wevk

I think that macros, AFAIK, cannot compute types the way that template partial specializations can do it.

于 2014-10-08T10:06:36.660 回答