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

时间:2008-10-07 20:43:07

标签: c++ templates macros

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

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

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

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

  • 不可思议的编译器错误 你不指望它们的地方?
  • 代码臃肿?
  • 难以追踪代码?
  • 设置调试器断点?

25 个答案:

答案 0 :(得分:51)

宏是一种文本替换机制。

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

答案 1 :(得分:30)

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

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

宏是一种原始形式,没有太多的编译器实施(比如在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!");
}

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

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

答案 2 :(得分:29)

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

以下是MSDN对此的评价: http://msdn.microsoft.com/en-us/library/aa903548(VS.71).aspx

  

以下是宏的一些问题:

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

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

答案 3 :(得分:22)

答案很长,我不能总结一切,但是:

  • 例如宏不能确保类型安全,而功能模板会这样做:编译器无法验证宏参数是否兼容类型 - 在实例化函数模板时编译器知道是否{ {1}}或int定义float
  • 模板为元编程打开了大门(简而言之,评估事物并在编译时做出决定):在编译时,可以知道类型是整数还是浮点;它是指针还是const限定等等...... see "type traits" in upcoming c++0x
  • 类模板具有部分特化
  • 函数模板具有明确的完全特化,在您的示例中,operator +的实现方式可能与add<float>(5, 3);不同,这是宏无法实现的
  • 宏没有任何范围
  • add<int>(5, 3); - 对#define min(i, j) (((i) < (j)) ? (i) : (j))i参数进行两次评估。例如,如果任一参数具有后增量变量,则增量执行两次
  • 因为宏由预处理器扩展,编译器错误消息将引用扩展宏,而不是宏定义本身。此外,宏将在调试期间以扩展形式显示
  • 等...

注意:在极少数情况下,我更倾向于使用可变参数宏,因为在c ++ 0x成为主流之前不存在可变参数模板。 C++11是实时的。

参考文献:

答案 4 :(得分:12)

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

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

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

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

这本身只是many things you can do的一个例子。模板本身就是Turing-complete。

这忽略了一些非常基本的东西,比如范围,类型安全,以及那些宏的问题。

答案 5 :(得分: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

答案 6 :(得分:9)

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

答案 7 :(得分:6)

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

答案 8 :(得分:5)

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

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

答案 9 :(得分:4)

未提及的是模板函数可以推导出参数类型。

template <typename T>
void func(T t)
{
  T make_another = t;

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

template <typename T>
void func(container<T> c)

甚至:

template <tempate <typename> class Container, typename T>
void func(Container<T> ct)

我也觉得专业化的主题不够充分。这是一个宏不能做的简单示例:

template <typename T>
T min(T a, T B)
{
  return a < b ? a : b;
}

template <>
char* min(char* a, char* b)
{
  if (strcmp(a, b) < 0)
    return a;
  else
    return b;
}

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

答案 10 :(得分:4)

不,这是不可能的。预处理器(几乎)不足以容纳像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告诉编译器一个特定的名称是指一个类型,而不是一个变量/值,所以(例如)你可以定义该类型的其他变量。

答案 11 :(得分:4)

这个答案旨在揭示C预处理器及其如何用于泛型编程


他们在某些方面,因为他们启用了一些类似的语义。 C预处理器已用于启用通用数据结构和算法(参见token Concatination)。但是,如果不考虑C ++模板的任何其他功能,它将使整个通用编程游戏成为 LOT CLEARER 来阅读和实现。

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

答案 12 :(得分:2)

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

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

答案 13 :(得分:2)

模板了解数据类型。宏不会。

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

  • 定义可以采用任何数据类型的操作(例如,wrapping numbers的操作),然后根据数据类型是整数还是浮点提供选择适当算法的特殊化
  • 在编译时确定数据类型的各个方面,允许使用template deduction of array size等技巧,Microsoft将其用于strcpy_s的C ++重载及其同类

此外,因为模板是类型安全的,所以有许多模板编码技术可以用一些假设的高级预处理器来执行,但最好是笨拙且容易出错(例如,template template parameters,默认模板参数, Modern C ++ Design 中讨论的策略模板。)

答案 14 :(得分:2)

让我们试试原始的例子。考虑

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

作为

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

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

编辑:不,您无法确保宏的类型安全。如何为每个定义少于比较的类型(即min())实现类型安全operrator<

答案 15 :(得分: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);


    }
};

看看information from cplusplus.com on templates!您可以使用模板来执行所谓的特征,所使用的特征具有类型等的文档。你可以用模板做更多的事情,然后用宏来做什么!

答案 16 :(得分:2)

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

答案 17 :(得分:2)

虽然模板参数是经过类型检查的,但模板相对于宏有许多优点,但模板非常像宏,因为它们仍然基于文本替换。在您为其替换类型参数之前,编译器不会验证您的模板代码是否有意义。例如,只要你没有实际调用它,Visual C ++就不会抱怨这个函数:

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

因此,对于模板旨在接受的类型参数的给定类别,通常无法知道模板代码是否能够正常工作或成功编译。

答案 18 :(得分:2)

模板可以比宏预处理器能够做的更多。

E.g。有模板特化:如果这个模板是用这种类型或常量实现的,那么就不要使用默认的实现,但这里有一个......

...模板可以强制某些参数属于同一类型等......


以下是您可能想要查看的一些来源:

    Vandervoorde和Jossutis的
  • C++ templates。这是关于我所知道的模板的最好,最完整的书。
  • The boost library几乎完全由模板定义组成。

答案 19 :(得分:2)

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

答案 20 :(得分:2)

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

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

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

答案 21 :(得分:1)

这不是答案,而是已经说明的答案的结果。

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

普通的天文学家或电子工程师可能会很好地理解宏,甚至可能理解为什么应该避免使用宏,但是不能很好地模拟日常使用的模板。在这种情况下,宏实际上更好。当然,存在许多例外情况;一些物理学家在专业软件工程师周围跑圈,但这不典型。

答案 22 :(得分:1)

宏存在一些基本问题。

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

其次,宏不能改变。模板std::swap通常只会声明一个临时变量并执行明显的赋值,因为这是通常可行的明显方法。这就是宏将被限制的范围。对于大型向量而言,这将是非常低效的,因此向量具有特殊的swap,其交换引用而不是整个内容。 (这对普通C ++程序员不应该编写但确实使用的东西非常重要。)

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

模板功能的一个很好的例子是最初称为标准模板库的标准模板库,它在标准中作为容器,算法和迭代器。看看它们是如何工作的,并试着想一想如何用宏替换它。 Alexander Stepanov查看了各种语言以实现他的STL思想,并得出结论,带有模板的C ++是唯一可以使用的语言。

答案 23 :(得分:0)

模板集成在该语言中,并且是类型安全的。

告诉我你将如何用宏来做这件事。这是沉重的模板元编程。

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

我认为宏AFAIK不能像模板部分专业化那样计算类型。

答案 24 :(得分:0)

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