programing tip

C ++ 템플릿은 단지 매크로 일 뿐입니 까?

itbloger 2021. 1. 9. 09:35
반응형

C ++ 템플릿은 단지 매크로 일 뿐입니 까?


저는 몇 년 동안 C ++로 프로그래밍을 해왔고, STL을 꽤 많이 사용해 왔으며 어떻게 수행되는지보기 위해 자체 템플릿 클래스를 몇 번 만들었습니다.

이제 템플릿을 내 OO 디자인에 더 깊이 통합하려고 노력하고 있는데 잔소리가 계속 나에게 돌아옵니다. 그들은 단지 매크로 일뿐입니다. 정말 ... #defines를 사용하여 auto_ptrs를 구현할 수 있습니다. 에 원.

템플릿에 대한 이러한 생각은 내 코드가 실제로 어떻게 작동하는지 이해하는 데 도움이되지만, 어떻게 든 요점을 놓치고 있어야한다고 생각합니다. 매크로는 사악한 화신을 의미하지만 "템플릿 메타 프로그래밍"은 모든 분노입니다.

그렇다면 실제 차이점은 무엇입니까? 템플릿은 어떻게 #define으로 이어지는 위험을 피할 수 있습니까?

  • 예상하지 못한 곳에서 이해할 수없는 컴파일러 오류?
  • 코드 팽창?
  • 코드 추적이 어렵습니까?
  • 디버거 중단 점을 설정 하시겠습니까?

매크로는 텍스트 대체 메커니즘입니다.

템플릿은 컴파일 타임에 실행되고 C ++ 유형 시스템에 통합되는 기능적 튜링 완료 언어입니다. 그것들을 언어에 대한 플러그인 메커니즘으로 생각할 수 있습니다.


매크로와 템플릿을 구별하기 위해 여기에 많은 주석이 있습니다.

예-둘 다 같은 것입니다 : 코드 생성 도구.

매크로는 컴파일러 시행이 많지 않은 원시 형태입니다 (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 개 이상의 매개 변수 (참조 필요)를 사용하여 이러한 종류의 템플릿을 정의했으며, 각각은 서로 다른 물리 단위 유형을 나타냅니다. 당신의 숫자에 대해 그런 종류의 클래스의 유형 안전성에 대해 생각해보십시오.


이들은 컴파일러 이전실행되는 전처리 기가 아닌 컴파일러에 의해 구문 분석됩니다 .

MSDN이 이에 대해 말하는 내용은 다음과 같습니다. http://msdn.microsoft.com/en-us/library/aa903548(VS.71).aspx

다음은 매크로와 관련된 몇 가지 문제입니다.

  • 컴파일러에서 매크로 매개 변수가 호환 가능한 유형인지 확인할 수있는 방법은 없습니다.
  • 매크로는 특별한 유형 검사없이 확장됩니다.
  • i 및 j 매개 변수는 두 번 평가됩니다. 예를 들어, 매개 변수 중 하나에 사후 증가 변수가있는 경우 증가가 두 번 수행됩니다.
  • 매크로는 전처리기에 의해 확장되기 때문에 컴파일러 오류 메시지는 매크로 정의 자체가 아닌 확장 된 매크로를 참조합니다. 또한 매크로는 디버깅 중에 확장 된 형태로 표시됩니다.

그것이 당신에게 충분하지 않다면, 나는 그것이 무엇인지 모릅니다.


대답은 너무 길어서 모든 것을 요약 할 수는 없지만 다음과 같습니다.

  • 예를 매크로 유형의 안전을 보장하지 않습니다에 대한 기능 템플릿 할 때 : 컴파일러가 매크로 변수가 호환되는 유형인지 확인하는 방법이 없습니다 - 또한 시간에 함수 템플릿이 컴파일러가 알고 인스턴스화 여부 int또는 float정의가operator +
  • 템플릿은 메타 프로그래밍을위한 문을 엽니 다 (간단히 말해서 컴파일 타임에 사물을 평가하고 결정) : 컴파일 타임에 타입이 정수인지 부동 소수점인지 알 수 있습니다. 포인터인지 아니면 const 한정인지 등 ... 곧 출시 될 C ++ 0x의 "유형 특성"을 참조하십시오.
  • 클래스 템플릿에는 부분적인 전문화가 있습니다.
  • 함수 템플릿에는 명시 적 전체 전문화가 있습니다. 예제 에서는 매크로로 불가능한 add<float>(5, 3);것과 다르게 구현할 수 있습니다.add<int>(5, 3);
  • 매크로에는 범위가 없습니다.
  • #define min(i, j) (((i) < (j)) ? (i) : (j))- ij매개 변수는 두 번 평가됩니다. 예를 들어, 매개 변수 중 하나에 사후 증가 변수가있는 경우 증가는 두 번 수행됩니다.
  • 매크로는 전처리기에 의해 확장되기 때문에 컴파일러 오류 메시지는 매크로 정의 자체가 아닌 확장 된 매크로를 참조합니다. 또한 매크로는 디버깅 중에 확장 된 형태로 표시됩니다.
  • 기타...

참고 : 드문 경우지만 c ++ 0x가 주류가 될 때까지 가변 템플릿과 같은 것이 없기 때문에 가변 매크로에 의존하는 것을 선호했습니다. C ++ 11 은 라이브입니다.

참조 :


매우 기본적인 수준에서 예, 템플릿은 매크로 대체 일뿐입니다. 하지만 당신은 그렇게 생각함으로써 많은 것을 생략하고 있습니다.

내가 아는 한 매크로로 시뮬레이션 할 수없는 템플릿 전문화를 고려하십시오. 특정 유형에 대한 특수 구현을 허용 할뿐만 아니라 템플릿 메타 프로그래밍의 핵심 부분 중 하나입니다.

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

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

그 자체로 당신이 할 수 있는 많은 일들 의 한 예일뿐입니다 . 템플릿 자체는 Turing-complete입니다.

이것은 범위, 유형 안전성과 같은 매우 기본적인 것을 무시하고 매크로는 더 지저분합니다.


아니 . 하나의 간단한 카운터 예 : 템플릿은 네임 스페이스를 준수하고 매크로는 네임 스페이스를 무시합니다 (전 처리기 문이므로).

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

C ++ 템플릿은 이미 구문 분석 된 코드 버전에서 작동하고 컴파일 타임에 임의의 코드를 생성 할 수 있다는 점에서 Lisp 매크로 (C 매크로가 아님)와 비슷합니다. 불행히도 원시 Lambda 미적분과 유사한 것으로 프로그래밍하고 있으므로 루핑과 같은 고급 기술은 다소 번거 롭습니다. 자세한 내용 은 Krysztof Czarnecki 및 Ulrich Eisenecker의 Generative Programming참조하십시오 .


이 주제에 대해 좀 더 심층적 인 처리를 원하신다면, 모두가 좋아하는 C ++ 싫어하는 사람을 소개해 드릴 수 있습니다 . 이 사람은 내가 꿈꾸는 것보다 더 많은 C ++를 알고 싫어합니다. 이것은 동시에 FQA를 엄청나게 자극적이고 훌륭한 자원으로 만듭니다.


  • 템플릿은 형식이 안전합니다.
  • 템플릿 객체 / 유형은 네임 스페이스가 될 수 있고, 클래스의 개인 멤버가 될 수 있습니다.
  • 템플릿 함수에 대한 매개 변수는 함수 본문 전체에 복제되지 않습니다.

이것은 정말 큰 문제이며 수많은 버그를 방지합니다.


언급되지 않은 것은 템플릿 함수가 매개 변수 유형을 추론 할 수 있다는 것입니다.

템플릿 <typename T>
무효 func (T t)
{
  T make_another = t;

다가오는 "typeof"연산자가이를 고칠 수 있지만 다른 템플릿을 분리 할 수는 없다고 주장 할 수 있습니다.

템플릿 <typename T>
void func (container <T> c)

또는:

템플릿 <tempate <typename> 클래스 컨테이너, typename T>
void func (Container <T> ct)

나는 또한 전문화의 주제가 충분히 다루어지지 않았다고 느낍니다. 다음은 매크로로 할 수없는 간단한 예입니다.

템플릿 <typename T>
T 분 (T a, TB)
{
  반환 a <b? a : b;
}

템플릿 <>
char * min (char * a, char * b)
{
  if (strcmp (a, b) <0)
    반환 a;
  그밖에
    반환 b;
}

공간이 너무 작아서 유형 전문화에 들어갈 수 없지만 제가 아는 한 당신이 할 수있는 일은 정말 놀랍습니다.


아니요, 불가능합니다. 전처리 기는 T의 컨테이너와 같은 몇 가지 작업에 (거의) 충분하지만 템플릿이 할 수있는 다른 작업에는 충분하지 않습니다.

실제 예제를 보려면 Modern C ++ Programming , Andre Alexandrescu 또는 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 컴파일러에게 특정 이름이 변수 / 값이 아닌 유형을 참조하도록 의도되었음을 알려줍니다. 따라서 (예를 들어) 해당 유형의 다른 변수를 정의 할 수 있습니다.


이 답변은 C 전처리기에 대한 설명과 일반 프로그래밍에 사용되는 방법을 알려줍니다.


그들은 유사한 의미를 가능하게하므로 어떤면에서 그렇습니다. C 전처리 기는 일반 데이터 구조 및 알고리즘을 활성화하는 데 사용되었습니다 ( 토큰 연결 참조 ). 그러나 C ++ 템플릿의 다른 기능을 고려하지 않으면 전체 일반 프로그래밍 게임 을 읽고 구현하기가 훨씬 더 명확 해집니다.

누군가 하드 코어 C 전용 일반 프로그래밍이 작동하는 것을보고 싶다면 libevent 소스 코드를 읽으 십시오 . 여기 에서도 언급 됩니다 . 방대한 컨테이너 / 알고리즘 모음이 구현되고 SINGLE 헤더 파일 (매우 읽기 쉬운) 에서 수행됩니다 . 나는 이것을 정말로 존경한다. C ++ 템플릿 코드 (다른 속성에 대해 선호한다)는 매우 장황하다.


템플릿은 형식이 안전합니다. 정의를 사용하면 컴파일되지만 여전히 제대로 작동하지 않는 코드를 가질 수 있습니다.

컴파일러가 코드에 도달하기 전에 매크로가 확장됩니다. 즉, 확장 된 코드에 대한 오류 메시지가 표시되고 디버거는 확장 된 버전 만 볼 수 있습니다.

매크로를 사용하면 항상 일부 표현식이 두 번 평가 될 가능성이 있습니다. ++ x와 같은 것을 매개 변수로 전달한다고 상상해보십시오.


템플릿은 네임 스페이스에 넣거나 클래스의 멤버가 될 수 있습니다. 매크로는 전처리 단계 일뿐입니다. 기본적으로 템플릿은 다른 모든 것들과 잘 어울리는 언어의 일급 멤버입니다.


템플릿은 매크로 전처리 기가 할 수있는 것보다 훨씬 더 많은 일을 할 수 있습니다.

예를 들어 템플릿 전문화가 있습니다. 이 템플릿이이 유형 또는 상수로 인스턴스화 된 경우 기본 구현을 사용하지 않는 것보다 여기에 있습니다.

... 템플릿은 일부 매개 변수의 유형이 동일하도록 강제 할 수 있습니다.


다음은 몇 가지 소스입니다.

  • Vandervoorde 및 Jossutis의 C ++ 템플릿 . 이 책은 내가 아는 템플릿에 대한 가장 훌륭하고 완전한 책입니다.
  • 부스트 라이브러리 는 거의 전적으로 템플릿 정의로 구성됩니다.

제 생각에 매크로는 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);


    }
};

Have a look at the information from cplusplus.com on templates! You can use templates to do what is called traits which is used has a sort of documentation for types and such. You can do so much more with templates then what is possible with macros!


The typename keyword is presented to enable context-free nested typdef's. These were needed for the trait technique which allow meta-data to be added to types (especially built-in types such as a pointer), this was required to write the STL. The typename keyword is otherwise the same as the class keyword.


Let's try primitive example. Consider

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

invoked as

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

Of course, the real difference is deeper, but that should be enough to discard similarities to macros.

Edit: And no, you can't ensure type safety with macros. How would you implement typesafe min() for every type defining less than comparison (i.e. operrator<)?


Templates understand data types. Macros do not.

This means that you can do stuff like the following...

  • Define an operation (e.g., one for wrapping numbers) that can take any data type, then provide specializations that pick the appropriate algorithm based on whether the data type is integral or floating point
  • Determine aspects of your data types at compile time, permitting tricks like template deduction of array size, which Microsoft uses for its C++ overloads of strcpy_s and its ilk

Additionally, because templates are type safe, there are a number of template coding techniques that could conceivably be performed with some hypothetical advanced preprocessor but would be kludgy and error-prone at best (e.g., template template parameters, default template arguments, policy templates as discussed in Modern C++ Design).


Templates are only similar to macros in their most basic functionality. After all, templates were introduced into language as "civilized" alternative to macros. But even when it comes to that most basic functionality, the similarity is only skin-deep.

However, once we get to the more advanced features of templates, like specialization (partial or explicit) any apparent similarity with macros disappears entirely.


Although template parameters are type-checked and there are many advantages of templates over macros, templates are very much like macros in that they are still based on text substitution. The compiler will not verify that your template code makes any sense until you give it type parameters to substitute. Visual C++ doesn't complain about this function as long as you don't actually call it:

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

Edit: This example only applies to Visual C++. In standard C++ your template code is actually parsed into a syntax tree before the template is ever used, so the example is accepted by VC++ but not GCC or Clang. (I learned this when I tried to port VC++ code to GCC and had to deal with hundreds of syntax errors in my unspecialized templates.) However, a syntax tree still does not necessarily make any sense semantically. Regardless of compiler, no type checking occurs in the body until you instantiate the template by providing <template arguments>.

Consequently, it is, in general, impossible to know whether your template code will work correctly, or compile successfully, for a given category of the type parameters that the template is designed to accept.


This isn't an answer so much as a consequence of the answers already stated.

Working with scientists, surgeons, graphic artists and others who need to program - but aren't and won't ever be professional full time software developers - i see that macros are easily understood by the occasional programmer, while templates appear to require a higher level of abstract thinking possible only with deeper and ongoing experience programming in C++. It takes many instances of working with code where templates are useful concept, for the concept to make sense sufficiently for use. While that could be said of any language feature, the amount of experience for templates presents a larger gap than the specialist casual programmer is likely to gain from their everyday work.

The average astronomer or electronics engineer probably groks macros just fine, may even understand why macros should be avoided, but won't grok templates well enough for everyday use. In that context, macros are actually better. Naturally, there exist many pockets of exceptions; some physicists run circles around the pro software engineers, but this is not typical.


There are some basic problems with macros.

First, they don't respect scope or type. If I have #define max(a, b)..., then whenever I have the token max in my program, for whatever reason, it will be replaced. It will be replaced if it's a variable name or deep inside nested scopes. This can cause hard-to-find compilation errors. In contrast, templates work inside the C++ type system. A template function can have its name reused inside a scope, and won't try to rewrite a variable name.

Second, macros can't be varied. The template std::swap will normally just declare a temporary variable and do the obvious assignments, because that's the obvious way that normally works. That's what a macro would be limited to. That would be extremely inefficient for large vectors, and so vectors have a special swap that swaps the references rather than the entire content. (This turns out to be very important in stuff the average C++ programmer shouldn't write but does use.)

Third, macros can't do any form of type inferencing. You can't write a generic swap macro in the first place, because it would have to declare a variable of a type, and it doesn't know what the type could be. Templates are type-aware.

One great example of the power of templates is what was originally called the Standard Template Library, which is in the standard as containers and algorithms and iterators. Take a look at how they work, and try to think how you'd replace it with macros. Alexander Stepanov looked over a large variety of languages to implement his STL ideas in, and concluded that C++ with templates was the only one it would work in.


Templates offer some degree of type safety.


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.

ReferenceURL : https://stackoverflow.com/questions/180320/are-c-templates-just-macros-in-disguise

반응형