특정 문자에 대한 의존도를 최소화하면서 '문자열'의 개념을 표현하고자 한다. 예를 들어 부호 있는 문자, 부호 없는 문자, 중국어, 그리스어 등등 다양한 context에 유용하게 사용한다. 문자열의 정의는 문자를 복사할 수 있다는 사실에 의존하고 다른 것은 거의 없다.
template<typename C>
class String
{
public:
String();
explicit String(const C∗);
String(const String&);
String operator=(const String&);
// ...
C& operator[](int n) { return ptr[n]; } // unchecked element access
String& operator+=(C c); // add c at end
// ...
private:
static const int short_max = 15; // for the short str ing optimization
int sz;
C∗ ptr; // ptr points to sz Cs
};
template<typename C> 접두사는 템플릿이 선언되고 형식 인수 C가 선언에 사용됨을 지정한다. C가 도입된 후 C는 다른 유형 이름과 정확히 동일하게 사용된다. C의 범위는 template<typename C> 접두사가 붙은 선언의 끝까지 확장된다. 더 짧고 동등한 형식의 template<class C>를 선호할 수 있다. 두 경우 모두 C는 유형 이름이다. 클래스 이름일 필요는 없다.
String<char> cs;
String<unsigned char> us;
String<wchar_t> ws;
struct Jchar { /* ... */ }; // Japanese character
String<Jchar> js;
이름에 특수 구문을 제외하고 String<char>은 클래스 String의 정의를 사용하여 정의된 것처럼 정확히 작동한다. String을 템플릿으로 만들면 모든 종류의 문자에 대해 char의 String에 대한 기능을 제공할 수 있다.
예를 들어 표준 라이브러리 맵과 문자열 템플릿을 사용하는 경우 다음과 같다.
int main() // count the occurrences of each word on input
{
map<String<char>,int> m;
for (String<char> buf; cin>>buf;)
++m[buf];
// ... write out result ...
}
표준 라이브러리는 템플릿화된 String과 유사한 basic_string 템플릿 클래스를 제공한다. 표준 라이브러리에서 string은 basic_string<char>의 동의어다.
using string = std::basic_string<char>
이를 통해 다음과 같은 단어 계산 프로그램을 작성할 수 있다.
int main() // count the occurrences of each word on input
{
map<string,int> m;
for (string buf; cin>>buf;)
++m[buf];
// ... write out result ...
}
일반적으로 유형 별칭은 템플릿에서 생성된 클래스의 긴 이름을 줄이는 데 유용하다. 종종 유형이 어떻게 정의되는지에 대한 세부 사항을 모르는 것을 선호하며 별칭을 사용하면 유형이 템플릿에서 생성된다는 사실을 숨길 수 있다.
템플릿 정의
클래스 템플릿에서 생성된 클래스는 완전히 평범한 클래스다. 따라서 템플릿의 사용은 클래스를 사용하는 것 이상의 런타임 메커니즘을 의미하지는 않는다. 실제로 템플릿을 사용하면 멤버 함수에 대한 코드가 생성되기 때문에 생성되는 코드가 감소할 수 있다. 클래스 템플릿은 해당 멤버가 사용되는 경우에만 생성된다.
C++는 클래스 템플릿 외에도 함수 템플릿을 제공한다. 템플릿은 적절한 템플릿 인수에서도 무언가를 생성하는 방법에 대한 사양이다. 해당 생성을 수행하기 위한 언어 메커니즘(인스턴스화 및 특수화)은 클래스 또는 함수가 생성되는지 여부를 크게 신경쓰지 않는다. 따라서 달리 명시되지 않는 한 템플릿 규칙은 클래스 템플릿과 함수 템플릿에 동일하게 적용된다. 템플릿은 별칭으로 정의할 수도 있지만, 네임스페이스 템플릿과 같은 다른 그럴듯한 구성은 제공하지 않는다.
클래스 템플릿을 디자인할 때 일반적으로 String<C>와 같은 템플릿으로 변환하기 전에 String과 같은 특정 클래스를 디버그하는 것이 좋다. 그렇게 함으로써 구체적인 예의 맥락에서 많은 설계 문제와 대부분의 코드 오류를 처리한다. 이런 종류의 디버깅은 모든 프로그래머에게 친숙하며 대부분의 사람들은 추상적인 개념보다 구체적인 예에 더 잘 대처한다.
클래스 템플릿의 멤버는 템플릿이 아닌 클래스의 경우와 똑같이 선언되고 정의된다. 템플릿 멤버는 템플릿 클래스 자체 내에서 정의할 필요가 없다. 이 경우 템플릿이 아닌 클래스 멤버와 마찬가지로 정의가 다른 위치에 제공되어야 한다. 템플릿 클래스의 멤버는 템플릿 클래스의 매개변수에 의해 매개변수화된 템플릿 자체다. 이러한 멤버가 해당 클래스 외부에 정의되면 멩시적으로 템플릿으로 선언해야 한다.
template<typename C>
String<C>::String() // String<C>’s constr uctor
:sz{0}, ptr{ch}
{
ch[0] = {}; // terminating 0 of the appropriate character type
}
template<typename C>
String& String<C>::operator+=(C c)
{
// ... add c to the end of this string ...
return ∗this;
}
C와 같은 템플릿 매개변수는 특정 유형의 이름이 아닌 매개변수다. 그러나 이름을 사용하여 템플릿 코드를 작성하는 방식에는 영향을 미치지 않는다. String<C>의 범위 내에서 <C>를 사용한 한정은 템플릿 자체의 이름에 대해 중복되므로 String<C>::String은 생성자 이름이다.
프로그램에서 클래스 멤버 함수를 정의하는 함수가 하나만 있을 수 있는 것처럼 프로그램에서도 클래스 템플릿 멤버 함수를 정의하는 함수 템플릿은 하나만 있을 수 있다. 그러나 전문화를 통해 특정 템플릿 인수가 제공된 템플릿에 대한 대체 구현을 제공할 수 있다. 함수의 경우 오버로딩을 사용하여 다른 인수 유형에 대해 다른 정의를 제공할 수도 있다.
클래스 템플릿 이름을 오버로드하는 것은 불가능하므로 클래스 템플릿이 범위에서 선언되면 동일한 이름으로 다른 엔터티를 선언할 수 없다. 예를 들면,
template<typename T>
class String { /* ... */ };
class String { /* ... */ }; // error : double definition
템플릿 인수로 사용되는 유형은 템플릿에서 예상하는 인터페이스를 제공해야 한다. 예를 들어 String에 대한 인수로 사용되는 형식은 일반적인 복사 작업을 제공해야 한다. 동일한 템플릿 매개변수에 대해 서로 다른 인수가 상속에 의해 관련되어야 한다는 요구 사항은 없다.
템플릿 인스턴스화
템플릿과 템플릿 인수 목록에서 클래스 또는 함수를 생성하는 프로세스를 종종 템플릿 인스턴스화라고 한다. 특정 템플릿 인수 목록에 대한 템플릿 버전을 specialization이라고 한다.
일반적으로 사용된 각 템플릿 인수 목록에 대해 템플릿의 특수화가 생성되도록 하는 것은 프로그래머가 아니라 구현의 작업이다.
String<char> cs;
void f()
{
String<Jchar> js;
cs = "It's the implementation's job to figure out what code needs to be generated";
}
이를 위해 구현은 String<char> 및 String<JChar> 클래스, 해당 소멸자 및 기본 생성자 및 String<char>::operator=(char*)에 대한 선언을 생성한다. 다른 멤버 함수는 사용되지 않으며 생성되지 않는다. 생성된 클래스는 클래스에 대한 모든 일반적ㅇ니 규칙을 따르는 완전히 평범한 클래스다. 유사하게, 생성된 함수는 함수에 대한 모든 일반적인 규칙을 따르는 일반 함수다.
분명히 템플릿은 비교적 짧은 정의에서 많은 코드를 생성하는 강력한 방법을 제공한다. 따라서 거의 동일한 함수 정의로 메모리가 초과되는 것을 방지하기 위해 어느 정도 주의가 필요하다. 반면에 생성된 코드의 품질을 달성할 수 없도록 템플릿을 작성할 수도 있다. 특히, 단순 인라인과 결합된 템플릿을 사용한 합성은 많은 직접 및 간접 함수 호출을 제거하는데 사용할 수도 있다. 따라서 매우 유사한 큰 함수의 생성으로 이어지는 템플릿의 부주의한 사용은 코드 팽창을 유발 할 수 있는 반면, 작은 함수의 인라인을 가능하게 하는 템플릿의 사용은 대안에 비해 상당한 코드 축소(및 속도 향상)로 이어질 수 있다. 특히, 간단한 < 또는 []에 대해 생성된 코드는 종종 단일 기계 명령어로, 어떤 함수 호출보다 훨씬 빠르고 함수를 호출하고 결과를 수신하는 데 필요한 코드보다 작다.
유형 검사
템플릿 인스턴스화는 템플릿과 템플릿 인수 집합을 가져와서 코드를 생성한다. 인스턴스화 시 사용할 수 있는 정보가 너무 많기 때문에 템플릿 정의와 템플릿 인수 유형의 정보를 결합하면 뛰어난 유연성을 제공하고 비교할 수 없는 런타임 성능을 얻을 수 있다. 불행히도, 이러한 유연성은 또한 유형 검사의 복잡성과 유형 오류의 정확한 보고를 위한 어려움을 의미한다.
유형 검사는 템플릿 인스턴스화에 의해 생성된 코드에서 수행된다. (프로그래머가 손으로 템플릿을 확장한 것처럼). 이 생성된 코드에는 템플릿 사용자가 들어본 적이 없는 많은 내용(예: 템플릿 구현 세부 정보 이름)이 포함될 수 있으며 종종 빌드 프로세스에서 불편할 정도로 늦게 발생한다. 프로그래머가 보고 쓰는 것과 컴파일러 유형이 검사하는 것 사이의 이러한 불일치는 주요 문제가 될 수 있으며 그 결과를 최소화하도록 프로그램을 설계해야 한다.
템플릿 메커니즘의 근본적인 약점은 템플릿 인수에 대한 요구 사항을 직접 표현할 수 없다는 것이다. 예를 들어 다음과 같다.
template<Container Cont, typename Elem>
requires Equal_comparable<Cont::value_type ,Elem>() // requirements for types Cont and Elem
int find_index(Cont& c, Elem e); // find the index of e in c
즉, C++자체에서 Cont는 컨테이너 역할을 할 수 있는 유형이어야 하고 Elem 유형은 값을 Cont의 요소와 비교할 수 있는 유형이어야 한다고 직접 말할 수 있는 방법이 없다.
유형 동등성
템플릿이 주어지면 템플릿 인수를 제공하여 유형을 생성할 수 있다.
String<char> s1;
String<unsigned char> s2;
String<int> s3;
using Uchar = unsigned char;
using uchar = unsigned char;
String<Uchar> s4;
String<uchar> s5;
String<char> s6;
template<typename T, int N> // §25.2.2
class Buffer;
Buffer<String<char>,10> b1;
Buffer<char,10> b2;
Buffer<char,20−10> b3;
템플릿에 대해 동일한 템플릿 인수 집합을 사용할 때 항상 동일한 생성 유형을 참조한다.
다른 템플릿 인수에 의해 단일 템플릿에서 생성된 유형은 다른 유형이다. 특히 관련 인수에서 생성된 유형은 자동으로 관련되지 않는다. 예를 들어 Circle이 Shape의 일정이라고 가정하자.
Shape∗ p {new Circle(p,100)}; // Circle* converts to Shape*
vector<Shape>∗ q {new vector<Circle>{}}; // error : no vector<Circle>* to vector<Shape>* conversion
vector<Shape> vs {vector<Circle>{}}; // error : no vector<Circle> to vector<Shape> conversion
vector<Shape∗> vs {vector<Circle∗>{}}; // error : no vector<Circle*> to vector<Shape*> conversion
그러한 변환이 허용되었다면 유형 오류가 발생 했을 것이다. 생성된 클래스 간의 변환이 필요한 경우 프로그래머가 정의할 수 있다.
에러 검출
템플릿이 정의된 다음 나중에 템플릿 인수 집합과 함께 사용된다. 템플릿이 정의되면 정의에서 구문 오류가 있는지 확인하고 특정 템플릿 인수 집합에서 분리하여 감지할 수 있는 다른 오류도 확인한다.
template<typename T>
struct Link {
Link∗ pre;
Link∗ suc // syntax error: missing semicolon
T val;
};
template<typename T>
class List {
Link<T>∗ head;
public:
List() :head{7} { } // error : pointer initialized with int
List(const T& t) : head{new Link<T>{0,o,t}} { } // error : undefined identifier o
// ...
void print_all() const;
};
컴파일러는 정의 지점에서 또는 나중에 사용 지점에서 간단한 의미 오류를 잡을 수 있다. 사용자는 일반적으로 조기 감지를 선호하지만 모든 '단순' 오류를 감지하기 쉬운 것은 아니다. 여기에서는 세 가지 <실수>를 했다.
- 단순 구문 오류 : 선언 끝에 세미콜론을 생략한다.
- 단순 유형 오류 : 템플릿 매개변수와 상관없이 포인터는 정수 7로 초기화할 수 없다.
- 이름 조회 오류 : 식별자 o (물론 잘못 입력된 0)는 범위에 해당 이름이 없기 때문에 Link<T>의 생성자에 대한 인수가 될 수 없다.
템플릿 정의에 사용되는 이름은 범위 내에 있거나 템플릿 매개변수에 따라 합리적으로 분명한 방식이어야 한다. 템플릿 매개변수 T에 의존하는 가장 일반적이고 분명한 방법은 T라는 이름을 명시적으로 사용하고, T의 멤버를 사용하고, T 유형의 인수를 취하는 것이다.
template<typename T>
void List<T>::print_all() const
{
for (Link<T>∗ p = head; p; p=p−>suc) // p depends on T
cout << ∗p; // << depends on T
}
템플릿 매개변수 사용과 관련된 오류는 템플릿을 사용할 때까지 감지할 수 없다.
class Rec {
string name;
string address;
};
void f(const List<int>& li, const List<Rec>& lr)
{
li.print_all();
lr.print_all();
}
li.print_all()은 잘 체크하지만 lr.print_all()은 Rec에 대해 출력이 없기 때문에 유형 오류를 제공한다. 템플릿 매개변수와 관련된 오류가 가장 빨리 감지될 수 있는 것은 특정 템플릿 인수에 대한 템플릿 사용의 첫 번째 지점이다. 이 지점을 첫 번째 인스턴스화 지점이라고 한다. 구현은 프로그램이 링크될 때까지 본질적으로 모든 검사를 연기할 수 있으며 일부 오류의 경우 링크 시간이 완전한 검사가 가능한 가장 빠른 시점이기도 하다. 검사가 완료될 때와 관계없이 동일한 규칙 집합이 검사된다. 당연히 사용자는 조기 확인을 선호한다.
'Program Language > C & C++' 카테고리의 다른 글
[C++] Console 한 줄 Refresh (0) | 2022.02.16 |
---|---|
[C++] Class Template (0) | 2022.02.10 |
[C++] std::chrono로 시간 측정하기 (0) | 2022.01.21 |
[C++] std::list 정렬하기 (0) | 2022.01.21 |
[C++] unique_lock, lock_guard (0) | 2022.01.19 |