본문 바로가기
프로그래밍/C & C++

[C++] 템플릿 기본 세부사항

by 별준 2023. 1. 8.

References

  • Ch12, C++ Templates The Complete 2nd

Contents

  • Parameterized Declarations
  • Template Parameters
  • Template Arguments
  • Variadic Templates
  • Friends

[C++] Function Templates

[C++] Class Templates

[C++] Nontype Template Parameters

[C++] Variadic Templates (가변 템플릿)

[C++] 템플릿에서 이동의미론과 enable_if<>

[C++] Templates in Practice

앞선 포스팅들에서 템플릿에 대한 기본적인 내용, 즉, 템플릿의 선언, 템플릿 파라미터의 제약, 템플릿 인자의 제약 등에 대해 살펴봤습니다. 이번에는 이들에 대해서 조금 더 세부적인 내용들을 살펴보도록 하겠습니다.

 


Parameterized Declarations

현재 C++에서는 클래스 템플릿, 함수 템플릿, 변수 템플릿, 별칭(alias) 템플릿을 지원합니다 (C++17 기준, C++20부터 추가된 것이 있는지는 확인하지 못했습니다). 각 템플릿은 네임스페이스 영역(namespace scope)에 나타날 수도 있고, 클래스 영역(class scope)에서 나타날 수도 있습니다. 클래스 영역에서는 중첩 클래스 템플릿, 멤버 함수 템플릿, 정적 데이터 멤버 템플릿과 멤버 별칭 템플릿이 있습니다.

template<parameters here>

위와 같은 parameterization clause로 시작한다는 점을 제외하면 이러한 템플릿은 일반적인 클래스, 함수, 변수 등과 거의 같은 방식으로 선언됩니다.

 

우선 아래의 예제 코드를 살펴보겠습니다. 아래 코드에서는 4가지 종류의 템플릿이 있는데, 모두 다 네임스페이스 영역(global 또는 namespace)에 나타납니다.

template<typename T> // a namespace scope class template
class Data {
public:
    static constexpr bool copyable = true;
};

template<typename T> // a namespace scope function template
void log(T x) {
    
}

template<typename T> // a namespace scope variable template (since C++14)
T zero = 0;

template<typename T> // a namespace scope variable template (since C++14)
bool dataCopyable = Data<T>::copyable;

template<typename T> // a namespace scope alias template
using DataList = Data<T*>;

위 예제 코드에서 Data<T>::copyable은 변수 템플릿은 아니며, 클래스 템플릿 Data가 파라미터화되면서 간접적으로 파라미터화되었을 뿐입니다. 그러나, 변수 템플릿은 클래스 영역(class scope)에 나타날 수 있고, 이 경우에서는 정적 데이터 멤버 템플릿이 됩니다. 바로 아래의 예제 코드에서 살펴보겠습니다.

 

아래 예제 코드는 부모 클래스 내부에 정의한 클래스 멤버인 4가지 템플릿을 보여줍니다.

class Collection {
public:
    template<typename T> // an in-class member class template definition
    class Node {
    };

    template<typename T> // an in-class (and therefore implicitly inline)
    T* alloc() {         // member function template definition
    }

    template<typename T> // a member variable template (since C++14)
    static T zero = 0;
    
    template<typename T> // a member alias template
    using NodePtr = Node<T>*;
};

C++17에서 변수와 변수 템플릿은 inline 키워드를 명시하여 인라인될 수 있습니다. 

 

다음으로 멤버 템플릿이 클래스 외부에서 정의되는 경우의 코드를 살펴보겠습니다.

template<typename T> // a namespace scope class template
class List {
public:
    List() = default; // because a template constructor is defined

    template<typename U> // another member class template,
    class Handle;        // without its definition

    template<typename U>  // a member function template
    List(List<U> const&); // (constructor)

    template<typename U> // a member variable template (since C++14)
    static U zero;
};

template<typename T> // out-of-class member class template definition
template<typename U>
class List<T>::Handle {
};

template<typename T> // out-of-class member function template definition
template<typename T2>
List<T>::List(List<T2> const& b) {
};

template<typename T> // out-of-class static data member template definition
template<typename U>
U List<T>::zero = 0;

여기서 멤버 템플릿이 자신의 클래스 외부에서 선언될 때 어떤 방식으로 여러 개의 template<...>를 갖는지 잘 살펴봅시다. 이때는 가장 바깥을 둘러싼 클래스 템플릿에서부터 하나씩 나열됩니다.

 

생성자 템플릿이 있으면 암묵적으로 선언되는 기본 생성자들이 만들어지지 않습니다. 따라서 위의 코드에서는

List() = default;

로 기본 생성자를 명시적으로 생성하고 있습니다.

 

Union Templates

공용체 템플릿(union templates)도 가능하며, 클래스 템플릿의 한 종류로 간주됩니다.

template<typename T>
union AllocChunk {
    T object;
    unsigned char bytes[sizeof(T)];
};

 

Default Call Arguments

일반 함수 선언과 같이 함수 템플릿도 기본 호출 인자(default call arguments)를 가질 수 있습니다.

template<typename T>
void report_top(Stack<T> const&, int number = 10;

template<typename T>
void fill(Array<T>&, T const& = T{}); // T{} is zero for built-in types

위 예제 코드에서 두 번째 선언을 보면 기본 호출 인자가 템플릿 파라미터에 종속될 수 있음을 알 수 있습니다. 두 번째 선언은 다음과 같이 정의할 수도 있습니다 (C++11 이전에서는 아래의 방법만 가능).

template<typename T>
void fill(Array<T>&, T const& = T()); // T() is zero for built-in types

 

fill() 함수를 호출할 때, 두 번째 함수 호출 인자가 주어진다면 기본 인자는 인스턴스화되지 않습니다. 따라서, 어떤 타입 T에 대해서는 인스턴스화될 수 없는 것을 기본 호출 인자로 사용해도 에러가 발생하지 않습니다. 아래 코드를 참조바랍니다.

class Value {
public:
    explicit Value(int); // no default constructor
};

void init(Array<Value>& array)
{
    Value zero(0);
    
    fill(array, zero); // OK: default constructor not used
    fill(array);       // ERROR: undefined default constructor for Value is used
}

 

Nontemplate Members of Class Templates

클래스 내에서 선언될 수 있는 4가지 기본 템플릿 외에도 클래스 템플릿의 일부로 파라미터화된 일반 클래스 멤버가 있을 수 있습니다. 이런 멤버들을 멤버 템플릿이라고 종종 잘못 부르기도 하는데, 이런 멤버들은 파라미터화되긴 했지만 해당 멤버의 정의는 first-class 템플릿은 아닙니다. 해당 멤버의 파라미터는 전적으로 자신이 멤버로 속해 있는 템플릿에 따라 결정됩니다. 아래 예제 코드를 살펴봅시다.

template<int I>
class CupBoard
{
    class Shelf;                // ordinary class in class template
    void open();                // ordinary function in class template
    enum Wood : unsigned char;  // ordinary enumeration type in class template
    static double totalWeight;  // ordinary static data member in class template
};

위에서 각각 대응되는 정의는 멤버 자체가 아닌 부모 클래스 템플릿에 대한 파라미터화 절(parameterization clause)을 명시할 뿐인데, 이들은 템플릿이 아니기 때문입니다. 따라서, 아래 정의을 살펴보면 '::' 이후에 나타나는 이름에 대해서는 관련된 파라미터화 절이 없습니다.

template<int I> // definition of ordinary class in class template
class CupBoard<I>::Shelf {
    ...
};

template<int I> // definition of ordinary function in class template
void CupBoard<I>::open()
{
    ...
}

template<int I> // definition of ordinary enumeration type in class template
enum CupBoard<I>::Wood {
    Maple, Cherry, Oak
};

template<int I> // definition of ordinary static member in class template
double CupBoard<I>::totalWeight = 0.0;

참고로 C++17에서부터는 정적 데이터 멤버를 inline을 사용하여 클래스 템플릿 내부에서 초기화할 수 있습니다.

 

비록 이러한 파라미터화 정의를 일반적으로 템플릿이라고 부르긴 하지만, 이 용어가 꼭 들어맞는 것은 아닙니다. 이러한 엔티티들을 가리키기 위해 temploid라는 용어가 제안되기도 했습니다. C++17에서부터는 표준에서 temploided entity라는 표현을 정의했습니다.

 

Virtual Member Functions

멤버 함수 템플릿은 virtual로 선언될 수 없습니다. 가상 함수 호출 메커니즘은 보통 가상 함수당 하나의 엔트리를 갖는 고정된 크기의 테이블을 사용하는 방식으로 구현되는데, 멤버 함수 템플릿은 전체 프로그램이 번역되는 동안 무한정 인스턴스화될 수 있기 때문입니다. 따라서 가상 멤버 함수 템플릿을 지원하려면 C++ 컴파일러와 링커를 위한 새로운 메커니즘이 필요합니다.

 

대조적으로 클래스 템플릿의 일반 멤버는 virtual로 선언할 수 있는데, 클래스가 인스턴스화될 때 이들 멤버의 수가 고정되기 때문입니다.

template<typename T>
class Dynamic {
public:
    virtual ~Dynamic(); // OK: one destructor per instance of Dynamic<T>

    template<typename T2>
    virtual void copy(T2 const&); // ERROR: unknown number of instance of copy()
                                  //        given an instance of Dynamic<T>
};

 

Linkage of Templates

모든 템플릿은 이름(name)을 가지며, 그 이름은 해당 영역 내에서 고유해야 합니다. 단, 함수 템플릿은 오버로딩될 수 있습니다. 특별히 주의해야 할 점은 클래스 타입과 달리, 클래스 템플릿은 다른 종류의 엔티티와 이름을 공유할 수 없습니다.

int C;
...
class C; // OK: class names and non-class names are in a different "space"

int X;
...
template<typename T>
class X; // ERROR: conflict with variable X

struct S;
...
template<typename T>
class S; // ERROR: conflict with struct S

 

템플릿의 이름인 링크(linkage)를 갖는데, C 링크는 가질 수 없습니다. 비표준 링크(non-standard linkage)는 구현 방식에 따라 다를 수 있습니다.

extern "C++" template<typename T>
void normal();  // this is the default: the linkage specification could be left out

extern "C" template<typename T>
void invalid(); // ERROR: templates cannot have C linkage

extern "Java" template<typename T>
void javaLink(); // nonstandard, but maybe some compiler will someday
                 // support linkage compatible with Java generics

 

템플릿은 일반적으로 external linkage를 가집니다. 단, static 지정자를 갖는 namespace scope 함수 템플릿, unnamed namespace(internal linkage를 가짐)의 direct/indirect 멤버인 템플릿과 unnamed class(no linkage)의 멤버 템플릿은 예외입니다.

template<typename T> // refers to the same entity as a declaration of the
void external();     // same name (and scope) in another file

template<typename T>    // unrelated to a template with the same name in
static void internal(); // another file

template<typename T>    // redeclaration of the previous declaration
static void internal();

namespace {
    template<typename>    // also unrelated to a template with the same name
    void otherInternal(); // in another file, even one that similarly appears
                          // in an unnamed namespace
}

namespace {
    template<typename>    // redeclaration of the previous template declaration
    void otherInternal();
}

struct {
    template<typename T> void f(T){} // no linkage: cannot be redeclared
} x;

마지막 구조체에서 멤버 템플릿은 링크가 없기 때문에 이름이 없는 클래스 내부에서 정의되어야만 하며, 클래스 외부에서 정의를 제공할 수 없습니다.

 

현재 함수 영역이나 로컬 클래스 영역에서는 템플릿을 선언할 수 없지만, 제너릭 람다(generic lambda)는 로컬 영역에 나타날 수 있습니다. 제너릭 람다는 멤버 함수 템플릿을 포함하는 연관 클로저 타입(associated closuer type)을 말합니다. 따라서, 일종의 지역 멤버 함수 템플릿입니다.

 

템플릿 인스턴스의 링크는 템플릿의 링크입니다. 예를 들어, 바로 위 예제 코드에서 선언한 템플릿 internal()에서 인스턴스화된 함수 internal<void>()는 internal linkage를 갖습니다. 이러한 이유 때문에 변수 템플릿에서는 조금 재밋는 일이 발생합니다.

template<typename T> T zero = T{};

위 코드에서 zero의 모든 인스턴스화는 external linkage를 갖습니다.

하지만 const가 붙은 변수는 internal linkage를 가지는데, 위 템플릿과 유사하게 아래와 같은 템플릿에서 발생하는 인스턴스화는 int const 타입이지만 external linkage를 갖게 됩니다.

template<typename T> int const max_volumn = 11;

 

Primary Templates

일반적인 템플릿 선언은 primary templates이라고 합니다. 이러한 템플릿 선언에서는 템플릿 이름 뒤에 꺽쇠로 둘러싸인 템플릿 인자가 붙지 않습니다.

template<typename T> class Box;    // OK: primary template
template<typename T> class Box<T>; // ERROR: does not specialize

template<typename T> void translate(T);    // OK: primary template
template<typename T> void translate<T>(T); // ERROR: not allowed for functions

template<typename T> constexpr T zero = T{};    // OK: primary template
template<typename T> constexpr T zero<T> = T{}; // ERROR: does not specialize

non-primary template은 클래스나 변수 템플릿의 부분 특수화를 선언할 때 발생하며, 함수 템플릿은 항상 primary template이어야 합니다.

 


Template Parameters

템플릿 파라미터에는 세 가지 종류가 있습니다.

  • Type parameters (가장 흔히 사용됨)
  • Nontype parameters
  • Template template parameters

위와 같은 템플릿 파라미터들은 어떤 것이든지 템플릿 파라미터 팩(template parameter pack)으로 사용될 수 있습니다.

 

템플릿 파라미터는 템플릿 선언 도입부에 있는 파라미터화 절(parameterization clause)에서 선언됩니다. 선언할 때는 이 파라미터의 이름을 쓰지 않아도 됩니다.

template<typename, int>
class X; // X<> is parameterized by a type and an integer

물론 파라미터를 참조하려면 파라미터에 이름이 있어야 하며, 템플리 파라미터 이름은 뒤에 따라오는 함수의 파라미터 선언에서도 사용될 수 있습니다.

template<typename T,            // the first parameter is used
         T Root,                // in the declaration of the second one and
         template<T> class Buf> // in the declaration of the third one
class Structure;

 

Type Parameters

타입 파라미터는 typename 키워드나 class 키워드로 도입하는데, 어느 것을 사용해도 동일합니다 (class를 사용하더라도 인자가 반드시 클래스 타입일 필요는 없습니다). 템플릿 선언 내에서 타입 파라미터는 타입 별칭(type alias)과 유사하게 동작하는데, 예를 들어, T가 템플릿 파라미터라면, T를 클래스 타입으로 치환하더라도 class T라는 형태의 이름은 사용할 수 없습니다.

template<typename Allocator>
class List {
    class Allocator* allocptr; // ERROR: use "Allocator* allocptr"
    friend class Allocator;    // ERROR: use "friend Allocator"
    ...
};

 

Nontype Parameters

타입이 아닌 템플릿 파라미터는 컴파일이나 링크 타입에 결정되는 상수 값을 나타냅니다. 이러한 파라미터의 타입은 다음 중 하나이어야만 합니다.

  • an integer type or an enumeration type
  • a pointer type
  • a pointer-to-member type
  • an lvalue reference type (references to both objects and functions)
  • std::nullptr_t
  • auto나 decltype(auto)를 포함하는 타입 (since C++17)

C++17 기준으로 이외의 타입은 사용할 수 없습니다.

참고로 타입이 아닌 템플릿 파라미터의 선언도 typename 키워드로 시작할 수 있습니다.

 

함수와 배열 타입 역시 가능하지만, 이들은 형 소실이 되어 암묵적으로 포인터 타입으로 변환됩니다.

template<int buf[5]> class Lexer; // buf is really an int*
template<int* buf> class Lexer;   // OK: this is a redeclaration

template<int fun()> struct FuncWrap; // fun really has pointer to function type
template<int(*)()> struct FuncWrap;  // OK: this is a redeclaration

타입이 아닌 파라미터는 변수와 비슷한 방식으로 선언되지만, 이들은 static, mutual과 같은 지시자를 가질 수 없습니다. 이들도 const와 volatile과 같은 한정자를 가질 수 있지만, 한정자가 파라미터 타입의 맨 뒤에 있다면 그냥 무시합니다.

template<int const length> class Buffer; // const is useless here
template<int length> class Buffer;       // same as previous declaration

마지막으로, 참조자가 아니면서 타입이 아닌 파라미터가 표현식에 사용되면 언제나 prvalue입니다. 따라서 주소도 얻을 수 없고, 값을 할당할 수도 없습니다. 이와 반대로 lvalue 참조자 타입에 대한 타입이 아닌 파라미터는 lvalue를 표시하기 위해 사용될 수 있습니다.

template<int& Counter>
struct LocalIncrement {
    LocalIncrement() { Counter = Counter + 1; } // OK: reference to an integer
    ~LocalIncrement() { Counter = Counter - 1; }
};

 

Template Template Parameters

템플릿 템플릿 파라미터는 클래스 템플릿이나 별칭 템플릿을 위한 일종의 플레이스홀더(placeholder) 입니다. 클래스 템플릿과 유사하게 선언하지만, struct 키워드나 union 키워드는 사용할 수 없습니다.

template<template<typename X> class C> // OK
void f(C<int>* p);

template<template<typename X> struct C> // ERROR: struct not valid here
void f(C<int>* p);

template<template<typename X> union C> // ERROR: union not valid here
void f(C<int>* p);

C++17에서부터는 class 대신 typename을 사용할 수 있는데, 템플릿 템플릿 파라미터가 클래스 템플릿뿐만 아니라 별칭 템플릿으로 치환될 수도 있기 때문입니다. 따라서, C++17에서는 다음과 같이 선언할 수도 있습니다.

template<template<typename X> typename C> // OK since C++17
void f(C<int>* p);

 

템플릿 템플릿 파라미터는 기본 템플릿 인자를 가질 수 있는데, 파라미터가 명시되지 않는다면 기본 인자를 사용합니다.

template<template<typename T,
    typename A = MyAllocator> class Container>
class Adaptation {
    Container<int> storage; // implicitly equivalent to Container<int,MyAllocator>
    ...
};

여기서 T와 A는 템플릿 템플릿 파라미터 Container의 템플릿 파라미터 이름입니다. 따라서, 템플릿 템플릿 파라미터의 파라미터로만 사용할 수 있습니다.

아래 예제 코드에서는 템플릿 템플릿 파라미터의 파라미터는 템플릿에서 사용할 수 없다는 것을 보여줍니다.

template<template<typename T, T*> class Buf> // OK
class Lexer {
    static T* storage; // ERROR: a template template parameter cannot be used here
    ...
};

 

Template Parameter Packs

C++11부터 어떤 종류의 템플릿 파라미터든 간에 템플릿 파라미터의 이름 앞, 혹은 템플릿 파라미터의 이름이 없으면 템플릿 파라미터의 이름이 와야하는 곳 앞에 '...'을 두면 템플릿 파라미터 팩(template parameter pack)으로 바뀝니다.

template<typename... Types> // declares a template parameter pack named Types
class Tuple;

 

템플릿 파라미터 팩의 동작은 일반적인 템플릿 파라미터와 유사하지만 중요한 차이점이 있습니다. 바로 일반 템플릿 파라미터는 단 하나의 템플릿 인자에만 일치하지만, 템플릿 파라미터 팩은 템플릿 인자가 몇 개이든 간에 일치할 수 있습니다. 즉, 위에서 선언한 Tuple 클래스는 템플릿 인자로 타입을 몇 개든지 받아들일 수 있습니다.

using IntTuple = Tuple<int>;            // OK: one template argument
using IntCharTuple = Tuple<int, char>;  // OK: two template arguments
using IntTriple = Tuple<int, int, int>; // OK: three template arguments
using EmptyTuple = Tuple<>;             // OK: zero template arguments

 

타입이 아닌 템플릿 파라미터와 템플릿 템플릿 파라미터에 대한 템플릿 파라미터 팩도 인자를 몇 개든지 받을 수 있습니다.

template<typename T, unsigned... Dimensions>
class MultiArray; // OK: declares a non-type template parameter pack

using TransformMatrix = MultiArray<double, 3, 3> // OK: 3x3 matrix

template<typename T, template<typename, typename>... Containers>
void testContainers(); // OK: declares a template template parameter pack

MutliArray 클래스에서는 모든 nontype 템플릿 인자가 unsigned 타입이어야만 하는데, C++17에서는 nontype 템플릿 인자도 추론할 수 있게 되어서 이 제약을 어느 정도 극복할 수는 있습니다.

 

Primary 클래스 템플릿, 변수 템플릿, 별칭 템플릿은 최대 하나의 템플릿 파라미터 팩을 가질 수 있으며, 항상 가장 마지막 템플릿 파라미터이어야 합니다. 함수 템플릿에서는 이 제약이 조금 완화되어 있는데, 템플릿 파라미터 팩 다음에 나오는 각 템플릿 파라미터가 기본값을 가지거나 추론될 수 있으면 여러 템플릿 파라미터 팩을 사용할 수 있습니다.

template<typename... Types, typename Last>
class LastType; // ERROR: template parameter pack is not the last template parameter

template<typename... TestTypes, typename T>
void runTests(T value); // OK: template parameter pack is followed
                        //     by a deducible template parameter
template<unsigned...> struct Tensor;
template<unsigned... Dims1, unsigned... Dims2>
auto compose(Tensor<Dims1...>, Tensor<Dims2...>); // OK: the tensor dimensions can be deduced

 

타입 파라미터 팩은 자신의 파라미터 절(parameterization clause) 내에서 확장될 수 없습니다.

template<typename... Ts, Ts... vals> struct StaticValues{};
    // ERROR: Ts cannot be expanded in its own parameter list

그러나, 중첩 템플릿을 사용하면 위의 목적대로 사용할 수 있습니다.

template<typename... Ts> struct ArgList {
    template<Ts... vals> struct Vals{};
};
ArgList<int, char, char>::Vals<3, 'x', 'y'> tada;

템플릿 파라미터 팩을 가진 템플릿을 가변 템플릿(variadic template)이라고 부르는데, 아래쪽에서 가변 템플릿에 대해 더 살펴보도록 하겠습니다.

 

Default Template Arguments

템플릿 파라미터 팩이 아닌 모든 템플릿 파라미터는 기본 템플릿 인자(default template arguments)를 가질 수 있습니다. 단, 해당 파라미터에 정확히 일치해야 하는데, 예를 들어, 타입 파라미터는 타입이 아닌 기본 인자를 가질 수 없습니다. 기본 인자 선언이 끝나기 전까지는 파라미터의 이름이 아직 영역 내에 있는게 아니므로 기본 인자는 자신의 파라미터에 종속될 수 없습니다. 하지만 이전 파라미터에는 종속될 수 있습니다.

template<typename T, typename Allocator = allocator<T>>
class List;

 

(클래스 템플릿, 변수 템플릿, 별칭 템플릿에서는) 함수 호출 파라미터의 기본 인자와 동일하게 템플릿도 자신보다 뒤에 나오는 모든 파라미터가 기본 인자를 가질 때에만 기본 템플릿 인자를 가질 수 있습니다. 같은 템플릿 선언에서 연속해서 기본값을 제공하기도 하지만, 해당 템플릿의 이전 선언에서 선언될 수도 있는데, 예제를 통해 살펴보겠습니다.

template<typename T1, typename T2, typename T3,
    typename T4 = char, typename T5 = char>
class Quintuple; // OK

template<typename T1, typename T2, typename T3 = char,
    typename T4, typename T5>
class Quintuple; // OK: T4 nad T5 already have defaults

template<typename T1 = char, typename T2, typename T3,
    typename T4, typename T5>
class Quintuple; // ERROR: T1 cannot have a default argument
                 // because T2 doesn't have a default

 

함수 템플릿의 템플릿 파라미터는 뒤따라오는 템플릿 파라미터가 기본 템플릿 인자를 갖지 않아도 기본 인자를 가질 수 있습니다 (템플릿 인자 추론을 통해 뒤따라오는 템플릿 파라미터의 템플릿 인자를 결정할 수 있음).

template<typename R = void, typename T>
R* addressof(T& value); // OK: if not explicitly specified, R will be void

 

기본 템플릿 인자는 반복될 수 없습니다.

template<typename T = void>
class Value;

template<typename T = void>
class Value; // ERROR: repeated default argument

 

기본 템플릿 인자를 허용하지 않는 상황도 많습니다.

// Partial specializations
template<typename T>
class C;
...
template<typename T = int>
class C<T*>; // ERROR

// Parameter packs
template<typename... Ts = int> struct X; // ERROR

// The out-of-class definition of a member of a class template
template<typename T> struct X
{
    T f();
};
template<typename T = int> T X<T>::f() { // ERROR
    ...
}

// A friend class template declarations
struct S {
    template<typename = void> friend struct F;
};

// A friend function template declaration unless it is a definition
// and no declaration of it appears anywhere else in the translation unit
struct S {
    template<typename = void> friend void f();  // ERROR: not a definition
    template<typename = void> friend void g() { // OK so far
    }
};
template<typename> void g(); // ERROR: g() was given a default template argument
                             // when defined; no other declaration may exist here

 


Template Arguments

템플릿을 인스턴스화할 때 템플릿 파라미터는 템플릿의 인자로 치환됩니다. 이 값들은 아래의 다양한 메커니즘을 통해 결정됩니다.

  • explicit template arguments(명시적 템플릿 인자) : 꺽쇠 사이에 템플릿 인자 값을 명시적으로 템플릿 이름에 대입합니다. 그 결과 만들어진 이름은 템플릿 식별자(template-id)라고 합니다.
  • injected class name(주입된 클래스 이름) : 템플릿 파라미터 P1, P2, ...을 갖는 클래스 템플릿 X의 영역 내라면 템플릿 X의 이름은 템플릿 식별자 X<P1, P2, ...>와 같습니다.
  • default template arguments(기본 템플릿 인자) : 기본 템플릿 인자가 있다면 클래스 템플릿 인스턴스에서 템플릿 인자를 생략할 수 있습니다. 단, 모든 템플릿 파라미터가 기본값을 가지고 있더라도 꺽쇠(안이 비어있더라도)는 반드시 있어야 합니다.
  • argument deduction(인자 추론) : 명시적으로 제시되지 않은 함수 템플릿 인자는 호출 시 함수 호출 인자의 타입을 통해 추론할 수 있습니다. 이와 관련된 내용은 이것만 다루는 포스팅에서 따로 자세히 살펴볼 예정입니다. 그 이외의 일부 상황에서도 추론할 수 있는데, 모든 템플릿 인자를 추론할 수 있다면 함수 템플릿 이름 뒤에 꺽쇠를 붙이지 않아도 됩니다. C++17에서부터는 변수 선언의 초기화자(intializer) 함수식 표기를 사용한 타입 변환을 통해서도 클래스 템플릿 인자를 추론할 수 있습니다. 이 또한 별도의 포스팅을 통해 알아보겠습니다.

 

Function Template Arguments

함수 템플릿의 템플릿 인자는 명시적으로 나열되거나, 템플릿이 사용된 방식을 통해 추론되거나, 기본 템플릿 인자로 제공됩니다. 아래의 예제 코드를 살펴보겠습니다.

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

int main()
{
    ::max<double>(1.0, -3.0); // explicitly specify template argument
    ::max(1.0, -3.0);         // template argument is implicitly deduced to be double
    ::max<int>(1.0, 3.0);     // the explicitly <int> inhibits the deduction;
                              // hence the result has type int
}

 

어떤 템플릿 인자는 템플릿 파라미터가 함수 파라미터 타입에 나오지 않는다는 등의 이유로 절대 추론될 수 없습니다. 이러한 파라미터는 템플릿 파라미터 리스트의 첫 부분에 둔다면 이런 인자만을 명시적으로 알려주고 다른 인자는 추론하도록 할 수 있습니다.

template<typename DstT, typename SrcT>
DstT implicit_cast(SrcT const& x) // SrcT can be deduced, but DstT cannot
{
    return x;
}

int main()
{
    double value = implicit_cast<double>(-1);
}

만약 위 코드에서 템플릿 파라미터의 순서를 반대로 한다면, 함수 템플릿을 호출할 때 두 템플릿 인자를 명시적으로 지정해야만 합니다.

게다가, 이런 파라미터는 템플릿 파라미터 팩 다음에 놓일 수 없으며, 부분 특수화에도 나올 수 없습니다. 이는 명시적으로 표기할 방법도 없으며, 추론할 방법도 없기 때문입니다.

template<typename... Ts, int N>
void f(double (&)[N+1], Ts... ps); // useless declaration because N
                                   // cannot be specified or deduced

 

함수 템플릿은 오버로딩될 수 있으므로, 함수 템플릿의 모든 인자를 명시적으로 지정하는 것만으로 하나의 함수를 식별하지 못할 수 있습니다. 이러한 경우 전달한 인자에 일치하는 함수 집합을 식별하게 됩니다. 아래 예제 코드를 살펴봅시다.

template<typename Func, typename T>
void apply(Func funcPtr, T x)
{
    funcPtr(x);
}

template<typename T> void single(T);
template<typename T> void multi(T);
template<typename T> void multi(T*);

int main()
{
    apply(&single<int>, 3); // OK
    apply(&multi<int>, 7);  // ERROR: no single multi<int>
}

위 예제 코드에서 첫 번째 apply 호출은 &single<int>가 명확하기 때문에 잘 동작합니다. 하지만 두 번째 호출은 &multi<int>가 한 개 이상의 타입이 될 수 있으므로 Func가 추론되지 않습니다.

 

뿐만 아니라 함수 템플릿의 템플릿 인자를 명시할 때 옳지 않은 C++ 타입이나 표현식을 생성하려고 할 수 있습니다. 아래 코드를 고려해봅시다.

// RT1 and RT2 are unspecified types
template<typename T> RT1 test(typename T::X const*);
template<typename T> RT2 test(..);

test<int>라는 표현식에서 int에는 X라는 타입의 멤버가 없으므로 두 함수 템플릿 중 첫 번째와는 맞지 않습니다. 하지만 두 번째 템플릿에서는 아무런 문제가 없습니다. 따라서, &test<int>는 단 하나의 함수 주소만을 갖게 됩니다. int를 첫 번째 템플릿으로 치환하려는 시도가 실패했지만, 표현식이 유효하지 않은 것은 아니라는 것에 주의해야 합니다.

이와 같은 SFINAE(subsitution failure is not an error; 치환 실패는 에러가 아님) 원칙은 함수 템플릿 오버로딩이 활용되는 데 중요한 역할을 합니다.

 

Type Arguments

템플릿 타입 인자는 템플릿 타입 파라미터를 위해 명시된 '값' 입니다. 템플릿 인자로 어떠한 타입(void, function types, reference types 등)이든지 사용할 수 있지만, 템플릿 파라미터를 치환하여 생성된 코드가 유효해야 합니다.

template<typename T>
void clear(T p)
{
    *p = 0; // requires that the unary * be applicable to T
}

int main()
{
    int a;
    clear(a); // ERROR: int doesn't support the unary *
}

 

Nontype Arguments

타입이 아닌 템플릿 인자(nontype template arguments)는 타입이 아닌 파라미터에 대해 치환되는 값 입니다. 이러한 값은 다음 중 하나이어야 합니다.

  • 올바른 타입을 가지는 nontype template parameter
  • 컴파일 시간에 결정되는 정수형(integer type) 또는 열거형의 상수값. 대응되는 파라미터의 타입이 그 값의 타입이나 narrowing(축소 변환)없이 암묵적으로 일치하는 경우에만 가능합니다. 예를 들어, int 파라미터에 char 타입이 제공되는 경우는 일치하다고 판단하지만, 반대의 경우(500을 char 파라미터에 대입)는 일치하지 않습니다.
  • 외부 변수나 함수(external variable or function) 이름 앞에 내장 & 연산자가 붙은 경우. 함수나 배열인 경우에는 &를 생략할 수 있습니다. 이와 같은 템플릿 인자는 포인터 타입의 nontype 템플릿 파라미터와 일치합니다. C++17부터는 함수나 변수의 포인터를 만드는 상수 표현식(constant-expression)이 허용됩니다.
  • 위의 종류의 인자이지만 & 연산자가 없는 경우, 참조자 타입의 nontype 파라미터에 일치합니다. 여기에서도 C++17부터는 함수나 변수의 glvalue를 만드는 어떠한 상수 표현식도 허용하도록 변경되었습니다.
  • 멤버 접근 포인터(pointer-to-member) 상수. 즉, &C::m과 같은 형태의 표현식. 여기서 C는 클래스 타입이고, m은 nonstatic member(data or function) 입니다. 이는 pointer-to-member 타입의 nontype 템플릿 파라미터에만 대응됩니다. 역시 C++17부터는 실제 문법 구문에 제한이 없습니다. 따라서, pointer-to-member 상수에 일치하는 어떠한 상수 표현식도 가능합니다.
  • null pointer 상수. 널 포인터 상수는 포인터나 pointer-to-member 타입의 nontype 파라미터로 사용할 수 있습니다.

 

정수형인 경우, 파라미터 타입으로의 암묵적 변환이 일어날 수 있습니다. C++11에서는 constexpr 변환 함수가 도입되어 변환 전의 인자가 클래스 타입일 수도 있습니다.

 

C++17 이전에는 포인터나 참조자인 파라미터에 인자를 일치시킬 때, 사용자 정의 변환(하나의 인자를 받는 생성자와 변환 연산자)과 파생 클래스에서 베이스 클래스로의 변환은 고려하지 않습니다 (다른 상황에서는 유효한 암묵적 변환일지라도). 인자에 const나 volatile을 붙이는 암묵적 변환은 괜찮습니다.

 

아래 코드는 nontype 템플릿 인자를 사용하는 방법을 보여줍니다.

template<typename T, T nontypeParam>
class C;

C<int, 33>* c1; // integer type
int a;
C<int*, &a>* c2; // address of an external variable

void f();
void f(int);
C<void(*)(int), f>* c3; // name of a function
                        // overload resolution selects f(int) in thie case
                        // the & is implied

template<typename T> void templ_func();
C<void(), &templ_func<double>>* c4; // function template instantiations are functions

struct X {
    static bool b;
    int n;
    constexpr operator int() const { return 42; }
};
C<bool&, X::b>* c5;     // static class members are accpetable variable/function names
C<int X::*, &X::n>* c6; // an example of a pointer-to-member constant
C<long, X{}>* c7;       // OK: X is first converted to int via a constexpr conversion
                        // function and then to long via a standard integer conversion

템플릿 인자는 컴파일러나 링커가 프로그램을 빌드할 때 그 값을 표현할 수 있어야 한다는 일반적인 제약을 만족해야 합니다. 프로그램이 실행될 때까지 알려지지 않은 값(예를 들어, 지역 변수의 주소)은 프로그램을 빌드하는 중 인스턴스화되는 템플릿의 개념과 상충됩니다.

 

아래 나열된 값들은 유효한 템플릿 인자로 사용할 수 없습니다.

  • 부동 소수점 수
  • 문자열 리터럴

문자열 리터럴의 문제는 완전히 같은 두 리터럴이더라도 다른 주소에 저장되면 다른 값으로 인식된다는 것입니다. 상수 문자열로 인스턴스화되는 템플릿을 표현하고 싶다면, 문자열을 저장하는 변수를 추가하는 방법을 사용할 수 있습니다.

template<char const* str>
class Message {

};

extern char const hello[] = "Hello World!";
char const hello11[] = "Hello World!";

void foo()
{
    static char const hello17[] = "Hello World!";

    Message<hello> msg03;   // OK in all version
    Message<hello11> msg11; // OK since C++11
    Message<hello17> msg17; // OK since C++17
}

참조자나 포인터로 선언된 nontype 템플릿 파라미터를 C++의 모든 버전에서 사용하려면 external linkage를 가진 상수 표현식이어야 합니다. C++11부터는 internal linkage를 가져도 되며, C++7에서부터는 어떤 종류든 링크를 가지고 있기만 하면 됩니다.

 

아래의 경우는 유효하지 않는 예시 몇 가지를 더 보여줍니다.

template<typename T, T nontypeParam>
class C;

struct Base {
    int i;
} base;

struct Derived : public base {

} derived;

C<Base*, &derived>* err1; // ERROR: derived-to-base conversions are not considered
C<int&, base.i>* err2;    // ERROR: fields of variables aren't considered to be variable

int a[10];
C<int*, &a[0]>* err3;     // ERROR: addresses of array elements aren't acceptable either

 

Template Template Arguments

템플릿 템플릿 인자는 자신이 치환할 템플릿 템플릿 파라미터의 파라미터와 정확히 일치하는 파라미터를 갖는 클래스 템플릿이어야 합니다. C++17 이전에는 템플릿 템플릿 인자의 기본 템플릿 인자는 무시했습니다. C++17에서부터는 템플릿 템플릿 파라미터가 해당 템플릿 템플릿 인자만큼이라 특수화되기만 하면 된다고 일치 조건이 완화되었습니다.

 

C++17 이전 버전에서는 아래의 코드가 유효하지 않았습니다.

#include <list>
// template<typename T, typename Allocator = allocator<T>>
// class list;
template<typename T1, typename T2,
    template<typename> class Cont> // Cont expects one parameter
class Rel {

};

Rel<int, double, std::list> rel; // ERROR before C++17
                                 // std::list has more than one template parameter

위 코드에서 표준 라이브러리의 std::list 템플릿이 하나 이상의 파라미터를 갖는다는 것이 문제입니다. 두 번째 파라미터에 기본값이 있긴 하지만 C++17 이전에는 std::list를 Cont 파라미터와 비교할 때 기본값을 고려하지 않습니다.

 

가변 템플릿 템플릿 파라미터는 앞서 살펴본 C++17 이전의 '정확한 일치' 법칙의 예외이며, 위 문제를 해결할 수 있습니다. 가변 템플릿은 템플릿 템플릿 인자에 대해 조금 더 관대한 일치 규칙을 적용합니다. 템플릿 템플릿 파라미터 팩은 인자 내 0개나 그보다 많은 같은 종류의 템플릿 파라미터와 일치할 수 있습니다.

#include <list>
template<typename T1, typename T2,
    template<typename...> class Cont> // Cont expects any number of type parameters
class Rel {

};

Rel<int, double, std::list> rel; // OK. std::list has two template parameters
                                 // but can be used with one argument

 

템플릿 파라미터 팩은 같은 종류의 템플릿 인자에만 일치할 수 있습니다. 예를 들어, 아래 코드에서 클래스 템플릿은 오직 템플릿 타입 파라미터만을 갖는 클래스 템플릿이나 별칭 템플릿으로 인스턴스화될 수 있습니다.

#include <list>
#include <map>
// template<typename Key, typename T,
//    typename Compare = less<Key>,
//    typename Allocator = allocator<pair<Key const, T>>>
// class map;
#include <array>
// template<typename T, size_t N>
// class array;

template<template<typename...> class TT>
class AlmostAnyTmpl{
};

AlmostAnyTmpl<std::list> withVector;   // two type parameters
AlmostAnyTmpl<std::map> withMap;       // four type parameters
AlmostAnyTmpl<std::array> withArray;   // ERROR: a template type parameter pack
                                       // doesn't match a nontype template parameter

 

Equivalence

두 세트의 템플릿 인자는 인자의 값이 1:1로 같을 때 동등하다고 간주합니다. 이때 타입 인자의 경우, 타입 별칭은 중요하지 않습니다. 그보다는 타입 별칭이 가리키는 진짜 타입이 중요합니다. 타입이 아닌 정수형 인자의 경우 인자의 값을 비교합니다. 하지만 값을 어떻게 표현했는지는 중요하지 않습니다. 아래 예제 코드에서 이를 보여줍니다.

template<typename T, int I>
class Mix;

using Int = int;
Mix<int, 3*3>* p1;
Mix<Int, 4+5>* p2; // p2 has the same type as p1

 

 

하지만 템플릿 종속적인 문맥 내에서 템플릿 인자의 '값'이 항상 확정되는 것은 아니기 때문에 동등성에 대한 규칙은 좀 더 복잡합니다.

template<int N> struct I {};

template<int M, int N> void f(I<M+N>); // #1
template<int N, int M> void f(I<N+M>); // #2

template<int M, int N> void f(I<N+M>); // #3 ERROR

#1과 #2의 선언을 살펴보면, M과 N을 N과 M으로 이름만 변경해주면 동일하다는 것을 알 수 있습니다. 따라서 이 둘은 동등하며 같은 함수 템플릿 f를 선언합니다. 두 선언 내의 M+N과 N+M 표현식은 동등(equivalent)하다고 합니다.

 

하지만 #3의 선언은 미묘하게 다릅니다. 피연산자의 위치가 변경되었는데, 그렇기 때문에 #3의 표현식 N+M은 위의 두 표현식과 동등하지 않습니다. 하지만 템플릿 파라미터에 어떤 값을 주든 표현식의 값이 같기 때문에 이 표현식들은 '기능적으로 동등(functionally equivalent)' 합니다. 각 선언들의 표현식이 기능적으로 동등하지만 실제로 동등하지 않기 때문에 이런 방식으로 템플릿을 선언하면 에러가 발생하지만, 컴파일러에서 에러를 발생시키지 않을 수도 있습니다 (제 PC의 경우 에러가 발생하지 않았습니다). 예를 들어, 일부 컴파일러는 내부적으로 N+1+1을 표현하는 방식이 N+2와 같지 않을 수도 있는데, 다른 컴파일러에서는 동일할 수 있습니다. 표준에서는 특정 구현 방법을 강제하지 않기 때문에 프로그래머가 주의해야 합니다.

 

함수 템플릿에서 생성된 함수는 동등한 타입과 동등한 이름을 갖더라도 일반 함수와는 동등할 수 없습니다. 따라서, 클래스 멤버에 대한 중요한 두 가지 결론은 다음과 같습니다.

  • 멤버 함수 템플릿으로 생성된 함수는 절대 가상 함수를 오버라이딩하지 않습니다.
  • 생성자 템플릿에서 만들어진 생성자는 기본 복사 생성자나 이동 생성자가 될 수 없습니다 (생성자 템플릿은 기본 생성자가 될 수 있음). 이와 유사하게 할당 템플릿에서 생성된 할당은 복사 할당 연산자나 이동 할당 연산자가 될 수 없습니다. 그렇지만 복사 할당 연산자나 이동 할당 연산자를 암시적으로 호출하는 경우는 드물기 때문에 일반적으로 문제가 되지는 않습니다.

 


Variadic Templates

템플릿 파라미터 팩을 가진 템플릿을 가변 템플릿이라고 부릅니다. 가변 템플릿은 인자가 몇 개가 들어오든 템플릿의 동작을 일반화해야 할 때 유용합니다. tuple 클래스 템플릿이 한 가지 예 입니다. 튜플은 요소를 몇 개든지 가질 수 있고, 각 요소들을 동일한 방식으로 취급합니다.

 

가변 템플릿에서 템플릿 인자를 결정할 때 가변 템플릿의 각 템플릿 파라미터 팩은 0개 이상의 템플릿 인자 시퀀스와 일치됩니다. 이러한 템플릿 인자의 시퀀스를 인자 팩(argument pack)이라고 부릅니다. 아래 예제 코드에서 Tuple의 템플릿 파라미터 팩 Types가 템플릿 인자에 따라 어떻게 인자 팩에 일치되는지 살펴보겠습니다.

template<typename... Types>
class Tuple {
    // provides operations on the list of types in Types
};

int main()
{
    Tuple<> t0;             // Types contains an empty list
    Tuple<int> t1;          // Types contains int
    Tuple<int, float> t2;   // Types contains int and float
}

템플릿 파라미터 팩은 단 하나의 템플릿 인자가 아닌 템플릿 인자 리스트 전체를 나타내기 때문에 인자 팩의 모든 인자들에게 동일한 언어 구성 방법이 적용되는 문맥에서 사용되어야 합니다. 한 가지 예시로 'sizeof...' 연산은 인자 팩 내의 인자 갯수를 반환합니다.

template<typename... Types>
class Tuple {
public:
    static constexpr std::size_t length = sizeof...(Types);
};

int a1[Tuple<int>::length];                 // array of one integer
int a2[Tuple<short, int, long>::length];    // array of three integers

 

Pack Expansions

sizeof... 표현식은 pack expansion의 한 가지 예 입니다. 팩 확장은 인자 팩을 분리된 인자들로 확장합니다. sizeof...는 분리된 인자들의 수를 세기 위해서만 확장하지만, 다른 형태의 파라미터 팩은 리스트 내의 여러 요소들로 확장될 수 있습니다. 이러한 팩 확장은 리스트 내의 인자 오른쪽에 '...'을 붙여 표시합니다. 아래 코드는 Tuple에서 상속받은 새로운 클래스 템플릿 MyTuple을 보여줍니다.

template<typename... Types>
class MyTuple : public Tuple<Types...> {
    // extra oprations provided only for MyTuple
};

MyTuple<int, float> t2; // inherits from Tuple<int, float>

템플릿 인자 Types...는 템플릿 인자 시퀀스를 만드는 팩 확장인데, Types는 인자 팩 내의 각 인자들로 치환됩니다. 위 예제에서처럼 MyTuple<int, float>를 인스턴스화하면 템플릿 타입 파라미터 팩 Types는 인자 팩 int, float로 치환됩니다. 그러고 나면 각각 int를 위해 한 개, float를 위해 한 개의 템플릿 인자를 받습니다. 이에 따라 MyTuple<int, float>는 Tuple<int, float>를 상속받게 됩니다.

 

직관적으로 이해하려면 일종의 문법 확장이라고 생각하면 됩니다. 템플릿 파라미터 확장은 정확히 같은 수의 템플릿 파라미터로 치환되고, 팩 확장은 팩이 아닌 템플릿 파라미터마다 하나씩 개별적인 인자로 표시된다고 생각할 수 있습니다. 아래 코드는 MyTuple이 두 개의 파라미터로 확장되었을 때 어떻게 보여지는지 보여줍니다.

template<typename T1, typename T2>
class MyTuple : public Tuple<T1, T2> {
    // extra oprations provided only for MyTuple
};

아래 코드는 세 개의 파라미터로 확장되었을 때의 예입니다.

template<typename T1, typename T2, typename T3>
class MyTuple : public Tuple<T1, T2, T3> {
    // extra oprations provided only for MyTuple
};

 

단, 파라미터 팩의 각 요소를 이름으로 바로 접근할 수는 없습니다. 가변 템플릿에서는 T1, T2와 같은 이름이 정의되지 않으며, 타입이 필요하다면 다른 클래스나 함수로 전달하는 수 밖에 없습니다.

 

각 팩 확장에는 패턴이 있습니다. 인자 팩 내의 개별 인자마다 반복되는 타입이나 표현식을 패턴이라고 부르는데, 일반적으로 팩 확장을 나타내는 '...' 앞에 나옵니다. 앞서 살펴본 예제는 단순한 패턴만을 사용했지만, 얼마든지 복잡해질 수 있습니다. 예를 들어, Tuple을 상속하는데, 인자의 타입에 대한 포인터를 갖도록 새로운 타입을 정의할 수 있습니다.

template<typename... Types>
class PtrTuple : public Tuple<Types*...> {
     // extra operations provided only for PtrTuple
};

PtrTuple<int, float> t3; // Inherits from Tuple<int*, float*>

위 예제에서 팩 확장 Types*...의 패턴은 Types* 입니다. 이 패턴으로 계속 치환하면 템플릿 타입 인자 시퀀스가 생성되는데, 여기서는 인자 팩 내의 타입에 대한 포인터들이 인자 시퀀스가 됩니다.

 

Where Can Pack Expansions Occur?

지금까지 템플릿 인자 시퀀스를 만들기 위해 팩 확장을 사용했습니다. 사실 쉼표로 분리된 리스트를 제공하는 문법이 있다면 어디서든 팩 확장을 사용할 수 있는데, 사용할 수 있는 문법의 예는 다음과 같습니다.

  • 베이스 클래스 리스트 내
  • 생성자 내에서 베이스 클래스 초기화자의 리스트 내
  • 호출 인자 리스트 내
  • 초기화자 리스트 내 (중괄호로 둘러싸인 초기화자 리스트 내)
  • 클래스, 함수, 별칭 템플릿의 템플릿 파라미터 리스트 내
  • 함수가 던질 수 있는 예외 리스트 내 (C++11,C++14에서는 폐기 예정, C++17부터 사용 불가)
  • 속성(attribute)이 팩 확장을 지원하는 경우, 속성 내 (C++표준에서는 이러한 속성이 정의되지 않음)
  • 선언의 정렬(alignment)를 명시할 때
  • 람다의 캡처 리스트를 명시할 때
  • 함수 타입의 파라미터 리스트 내
  • using 선언 내 (since C++17)

앞서 sizeof... 연산이 실제로는 리스트를 만들지 않는 팩 확장 메커니즘이라는 것은 미리 알아봤습니다. C++17에는 쉼표로 분리된 리스트를 생성하지 않는 또 다른 메커니즘인 폴드 표현식(fold expression)이 추가되었습니다.

 

다음으로 실제로 유용한 팩 확장 문맥을 살펴보겠습니다. 어떠한 상황에서도 팩 확장은 동일한 원칙과 문법을 따르기 때문에 여기서 더 확장하면 조금 더 난해한 팩 확장 문맥도 이해할 수 있습니다.

 

베이스 클래스 리스트 내의 팩 확장은 여러 개의 direct base class를 확장시킵니다. 외부에서 제공된 데이터와 기능을 믹스인(mixin)으로 통합할 때 유용합니다. 예를 들어, 아래의 Point 클래스는 임의의 믹스인을 허용하기 위해 여러 상황에서 팩 확장을 사용합니다.

template<typename... Mixins>
class Point : public Mixins... { // base class pack expansion
    double x, y, z;

public:
    Point() : Mixins()... {} // base class initializer pack expansion

    template<typename Visitor>
    void visitMixins(Visitor visitor) {
        visitor(static_cast<Mixins&>(*this)...); // call argument pack expansion
    }
};

struct Color { char red, green, blue; };
struct Label { std::string name; };
Point<Color, Label> p; // inherits from both Color and Label


타입이 아닌 값이나 템플릿 파라미터 팩을 만들기 위해 템플릿 파라미터 리스트 내에서 팩 확장이 사용될 수 도 있습니다.

template<typename... Ts>
struct Values {
    template<Ts... Vs>
    struct Holder {
    };
};

int i;
Values<char, int, int*>::Holder<'a', 17, &i> valueHolder;

 

Function Parameter Packs

함수 파라미터 팩은 0개 이상의 함수 호출 인자와 일치하는 함수 파라미터입니다. 템플릿 파라미터 팩과 같이 함수 파라미터 팩도 '...'을 사용하며, 함수 파라미터 팩을 사용할 때마다 팩 확장을 통해 펼쳐주어야 합니다. 템플릿 파라미터 팩과 함수 파라미터 팩을 통틀어 파라미터 팩(parameter pack)이라고 합니다.

 

템플릿 파라미터 팩과 달리 함수 파라미터 팩은 항상 팩 확장이므로, 선언된 타입에 적어도 하나의 파라미터 팩을 포함해야 합니다. 아래 예제 코드는 제공된 생성자 인자를 통해 각 믹스인을 복사 초기화하는 새로운 Point 생성자를 정의합니다.

template<typename... Mixins>
class Point : public Mixins... { // base class pack expansion
    double x, y, z;

public:
    ...
    Point(Mixins... mixins)    // mixins is a function parameter pack
        : Mixins(mixins)... {} // initialize each base with the supplied mixin value
};

struct Color { char red, green, blue; };
struct Label { std::string name; };
Point<Color, Label> p({0x7F, 0, 0x7F}, {"center"});

 

 

파라미터 리스트의 끝에 있는 이름이 없는 함수 파라미터 팩과 C 스타일의 vararg 파라미터는 문법적으로 모호합니다.

template<typename T> void c_style(int, T...);
template<typename... T> void pack(int, T...);

첫 번째 코드에서 'T...'은 'T, ...'으로 취급되며, 타입이 T인 이름이 없는 파라미터 뒤에 C 스타일의 vararg 파라미터가 따라옵니다. 두 번째 코드에서 'T...'은 T가 확장 패턴이기 때문에 함수 파라미터 팩으로 취급됩니다.

 

Multiple and Nested Pack Expansions

팩 확장 패턴은 얼마든지 복잡해질 수 있으며, 구별되는 여러 개의 파라미터 팩이 있을 수도 있습니다. 여러 개의 파라미터 팩을 포함하는 팩 확장을 인스턴스화할 때 모든 파라미터 팩의 길이는 같아야 합니다. 그러면 각 파라미터 팩의 첫 번째 인자를 패턴으로 치환하고, 그 뒤에 파라미터 팩의 두 번째 인자가 뒤따르도록 하는 방식으로 요소 별로 치환하여 타입이나 값의 시퀀스를 만듭니다. 예를 들어, 아래 함수는 함수 객체 f로 자신의 인자를 전달하기 전에 모두 복사합니다.

template<typename F, typename... Types>
void forwardCopy(F f, Types const&... values) {
    f(Types(values)...);
}

함수 내부에서 values의 i번째 값 Types의 i번째 타입으로 형 변환한 후 복사하는 방식으로 객체가 생성됩니다. 팩 확장에 대한 문법적 해석 방식에 따라 인자가 3개인 forwardCopy는 다음과 같습니다.

template<typename F, typename T1, Typename T2, typename T3>
void forwardCopy(F f, T1 const& v1, T2 const& v2, T3 const& v3) {
    f(T1(v1), T2(v2), T3(v3));
}

 

팩 확장 자체도 중첩될 수 있습니다. 이런 경우, 파라미터 팩을 만날 때마다 가장 가까이 둘러싼 팩 확장에 따라 확장됩니다. 아래 예제를 통해 세 가지 파라미터 팩이 관여되어 있는 중첩된 팩 확장을 살펴보겠습니다.

template<typename... OuterTypes>
class Nested {
    template<typename... InnerTypes>
    void f(InnerTypes const&... innerValues) {
        g(OuterTypes(InnerTypes(innerValues)...)...);
    }
};

g()를 호출할 때 가장 내부에 있는 팩 확장은 패턴 InnerTypes(innerValues)에 대한 팩 확장이므로, 각각 InnerTypes와 innerValues로 확장되어 OuterTypes로 표기된 객체를 초기화하기 위한 함수 호출 인자 시퀀스를 생성합니다. 외부에 있는 팩 확장의 패턴은 내부 팩 확장을 포함하는데, 그때 함수 g()에 대한 호출 인자 집합이 생성됩니다. 이는 내부 확장을 통해 생성된 함수 호출 인자 시퀀스로 OuterTypes 내의 각 타입을 초기화해 생성한 것 입니다. 예를 들어, OuterTypes가 두 개의 인자를 갖고, InnerTypes와 innerValues는 각각 3개의 인자를 갖는 상황을 문법적으로 해석하면 다음과 같습니다.

template<typename O1, typename O2>
class Nested {
    template<typename I1, typename I2, typename I3>
    void f(I1 const& iv1, I2 const& iv2, I3 const& iv3) {
        g(O1(I1(iv1), I2(iv2), I3(iv3),
            O2(I1(iv1), I2(iv2), I3(iv3)));
    }
};

 

Zero-Length Pack Expansions

팩 확장을 문법적 해석 방법에 따라 생각하면 가변 템플릿이 다양한 수의 인자에 대해 어떻게 인스턴스화되는지 이해할 수 있습니다. 하지만 이러한 방식은 인자 팩의 길이가 0일 때는 제대로 처리하지 못합니다. 이에 대한 예시로 위에서 살펴본 Point 클래스 템플릿에서 0개의 인자를 받는 경우에 대해 치환해보겠습니다.

template<>
class Point : {
    Point() : {}
};

위 코드는 잘못된 코드입니다. 템플릿 파라미터 리스트도 비었고, 기본 클래스와 기본 클래스의 초기화자 리스트도 없는데 ':' 문자가 있습니다.

 

사실 팩 확장은 의미 체계(semantic)상의 생성이기 때문에 인자 팩을 어떤 길이로 치환하든 간에 팩 확장이 파싱되는 방식에 영향을 주지는 않습니다. 팩 확장이 비어있는 리스트로 확장되면 프로그램은 의미론적으로 해당 리스트가 존재하지 않았던 것처럼 동작합니다.

 

template<typename T, typename... Types>
void g(Types... values) {
    T v(values...);
}

위에서 가변 함수 템플릿 g()는 값 시퀀스를 받아 직접 초기화되는 v라는 값을 생성합니다. 값 시퀀스가 비어 있다면 v의 선언은 문법적으로 함수 선언인 T v()처럼 보입니다. 하지만 팩 확장으로의 치환은 의미 체계상의 작업이기 때문에 파싱으로 생성된 실체의 종류에는 영향을 미치지 않습니다. 따라서, v는 0개의 인자로 초기화되기 때문에 결국 value-initialization 됩니다.

 

Fold Expressions

프로그래밍에서 재귀 패턴은 값 시퀀스에 대한 연산의 fold라고 볼 수 있습니다. 예를 들어, x[1], x[2], ..., x[n-1], x[n]이라는 시퀀스가 있을 때 함수 fn을 폴드한다면 다음과 같습니다.

fn(x[1], fn(x[2], fn(..., fn(x[n-1], x[n])...)))

 

C++ 표준 위원회에서는 logical binary operator (&& or ||)를 팩 확장에 적용해야 하는 특별한 상황을 처리해야 했는데, 이러한 기능이 제공되지 않는다면 && 연산자만으로 원하는 동작을 수행하려면 다음과 같은 코드를 직접 작성해야 합니다.

bool and_all() { return true; }
template<typename T>
bool and_all(T cond) { return cond; }
template<typename T, typename... Ts>
bool and_all(T cond, Ts... conds) {
    return cond && and_all(conds...);
}

 

C++17에서는 폴드 표현식(fold expressions)이라는 새로운 기능이 추가되었습니다. 이는 '->'과 '[]'을 제외한 모든 이항 연산자에 적용됩니다.

 

확장되지 않은 표현식 패턴 pack과 패턴이 아닌 표현식 value가 주어졌을 때, C++17에서는 임의의 연산자 op에 대해 다음과 같이 연산자를 오른쪽으로 폴드(binary right fold) 하거나

(pack op ... op value)

아니면 아래와 같이 왼쪽으로 폴드(binary left fold) 할 수 있습니다.

(value op ... op pack)

괄호는 꼭 써주어야 하고, 그 결과는 다음과 같습니다.

 

폴드 연산은 팩을 확장한 뒤 나온 시퀀스에 적용되며, 시퀀스의 마지막 요소(right fold인 경우)나 첫 번째 요소(left fold인 경우)로 value를 추가합니다. 이 특성을 활용하면 아래의 코드는

template<typename... T> bool g() {
    return and_all(trait<T>()...);
}

아래와 같이 바꿀 수 있습니다.

template<typename... T> bool g() {
    return (trait<>() && ... && true);
}

예상할 수 있듯이 폴드 표현식은 팩 확장입니다. 팩이 비어있더라도 폴드 표현식의 타입은 팩에 포함되지 않은 피연산자(여기서는 value)에서 결정할 수 있습니다.

 

다만, 이 기능을 사용할 때 value 피연산자를 쓰지 않아도 되는데, C++17에서는 unary right fold, unary left fold 형태도 있습니다.

(pack op ...)
(... op pack)

여기서도 괄호는 꼭 필요하며, 여기서는 빈 시퀀스를 확장할 때 문제가 발생합니다. unary fold를 비어있는 팩에 대해 확장하려고 하면 일반적으로 에러가 발생하지만, 다음의 세 가지 예외가 있습니다.

  • &&의 unary fold의 빈 확장의 결과는 true
  • ||의 unary fold의 빈 확장의 결과는 false
  • 쉼표(,)의 unary fold의 빈 확장의 결과는 void 표현식

 


Friends

friend 선언의 기본 아이디어 자체는 매우 간단합니다. 한 클래스와 특별한 관계를 갖는 클래스나 함수를 friend 선언을 통해 알려주는 것 입니다. 하지만, 다음의 두 가지 사실로 인해 문제가 다소 복잡합니다.

  1. friend 선언은 entity에 대한 선언만을 의미할 수 있습니다
  2. friend 함수 선언은 정의가 될 수 있습니다

 

Friend Classes of Class Templates

friend 클래스 선언은 정의가 될 수 없기 때문에 거의 문제가 되지 않습니다. 템플릿에서의 friend 클래스 선언은 기존 문법과 달리 클래스 템플릿의 특정 인스턴스를 friend로 지칭할 수 있습니다.

template<typename T>
class Node;

template<typename T>
class Tree {
    friend class Node<T>;
    ...
};

클래스 템플릿의 인스턴스 중 하나가 다른 클래스나 클래스 템플릿의 friend가 되려면 해당 클래스 템플릿이 visible해야만 합니다. 일반 클래스의 경우 이와 같은 조건이 필요 없습니다.

template<typename T>
class Tree {
    friend class Factory; // OK even if first declaration of Factory
    friend class Node<T>; // error if Node isn't visible
};

 

C++11에서는 템플릿 파라미터를 friend로 만드는 문법도 추가되었습니다.

template<typename T>
class Wrap {
    friend T;
    ...
};

T는 어떤 타입이어도 상관없으며 클래스 타입이 아니라면 이 선언은 그냥 무시됩니다.

 

Friend Functions of Class Templates

friend 함수의 이름 뒤에 꺽쇠를 붙여주면 함수 템플릿의 인스턴스를 friend로 만들 수 있습니다. 꺽쇠 안에 템플릿 인자가 있어도 되지만, 인자가 추론될 수 있다면 빈채로 두어도 됩니다.

class Mixer {
    friend void combine<>(int&, int&); // OK: T1 = int&, T2 = int&
    friend void combine<int, int>(int, int); // OK: T1 = int, T2 = int
    friend void combine<char>(char, int); // OK: T1 = char, T2 = int
    friend void combine<char>(char&, int); // ERROR: doesn't match combine() template
    friend void combine<>(long, long) {...} // ERROR: definition not allowed!
};

단, 템플릿 인스턴스를 정의할 수 없으며(특수화를 정의할 수 있을 뿐), 이에 따라 인스턴스를 지칭하는 friend 선언은 정의가 될 수 없다는 점을 기억해야 합니다.

 

꺽쇠가 따라오지 않는다면 아래와 같은 두 가지 상황이 가능합니다.

  1. 이름을 한정하지 않는다면('::'이 없다면) 절대 템플릿 인스턴스를 참조하지 않습니다. friend 선언을 할 때 템플릿이 아닌 함수 중 일치하는 것이 없다면 friend 선언은 해당 함수에 대한 첫 번째 선언이 되며, 선언은 정의가 될 수도 있습니다.
  2. 이름이 한정되었다면('::'이 있다면) 그 이름은 이전에 선언된 함수나 함수 템플릿을 가리켜야만 합니다. 함수와 함수 템플릿에 모두 일치한다면 함수 쪽을 선호합니다. 하지만 이러한 friend 선언은 정의가 될 수 없습니다.

 

예제를 통해서 다양한 가능성에 대해서 살펴보겠습니다.

void multiply(void*);    // ordinary function

template<typename T>
void multiply(T);        // function template

class Comrades {
    friend void multiply(int) {}
                         // defines a new function ::multiply(int)
    friend void ::multiply(void*);
                         // refers to the ordinary function above,
                         // not to the multiple<void*> instance
    friend void ::multiply(int);
                         // refers to an instance of the template
    friend void ::multiply<double*>(double*);
                         // qualified names can also have angle brackets,
                         // but a template must be visible
    friend void ::error {}
                         // ERROR: a qualified friend cannot be a definition
};

위 예제에서 friend 함수는 일반 클래스에서 선언되었습니다.

 

클래스 템플릿에서 friend를 선언할 때도 동일한 법칙이 적용되며, friend로 삼은 함수를 식별할 때 템플릿 파라미터를 사용할 수 있습니다.

template<typename T>
class Node {
    Node<T>* allocate();
    ...
};

template<typename T>
class List {
    friend Node<T>* Node<T>::allocate();
    ...
};

 

템플릿이 실제로 사용될 때만 정의될 수 있으면, 클래스 템플릿 내에서 friend 함수를 정의할 수도 있습니다. 그러려면 friend 함수의 타입에서 클래스 템플릿을 사용해야 하며, 그러면 클래스 템플릿 내의 함수를 네임스페이스 영역에서 보이는 것처럼 쉽게 표현할 수 있습니다.

template<typename T>
class Creator {
    friend void feed(Creator<T>) { // every T instantiates a different function ::feed()
        ...
    }
};

int main()
{
    Creator<void> one;
    feed(one);              // instantiates ::feed(Creator<void>)
    Creator<double> two;
    feed(two);              // instantiates ::feed(Creator<double>)
}

위 코드에서 Creator의 모든 인스턴스는 다른 함수를 생성합니다. 템플릿의 인스턴스화 과정에서 이러한 함수가 생성되긴 했지만 함수 자체는 템플릿의 인스턴스가 아닌 일반 함수입니다. 하지만 이들은 템플릿화된 실체로 간주되며, 사용될 때에만 그 정의를 인스턴스화합니다. 또한 함수의 body가 클래스 정의 내에 정의되어 있기 때문에 암묵적으로 인라인화됩니다. 따라서 두 번역 단위 내에서 같은 함수가 또 다시 정의되어도 에러가 아닙니다.

 

Friend Templates

일반적으로 함수나 클래스 템플릿의 인스턴스를 friend로 선언할 때 어떤 인스턴스를 friend로 선언할지 정확히 표현할 수 있습니다. 그렇지만 때로는 템플릿의 모든 인스턴스를 클래스의 friend로 만드는 것이 유용하며, 이런 것을 프렌드 템플릿(friend template)이라고 합니다.

class Manager {
    template<typename T>
    friend class Task;

    template<typename T>
    friend void Schedule<T>::dispatch(Task<T>*);

    template<typename T>
    friend int ticket() {
        return ++Manger::counter;
    }

    static int counter;
};

일반 friend 함수처럼 프렌드 템플릿도 함수 이름에 꺽쇠가 따라오지 않으며, 한정되지도 않은 이름일 때에만 정의가 될 수 있습니다.

 

 

fiend 템플릿은 primary 템플릿과 primary 템플릿의 멤버에서만 선언할 수 있습니다. 그 외에 연관된 부분 특수화나 명시적 특수화는 자동적으로 friend가 됩니다.

댓글