{{ label!='' ? 'Label : ' : (q!='' ? '검색 : ' : '전체 게시글') }} {{ label }} {{ q }} {{ ('('+(pubs|date:'yyyy-MM')+')') }}

[C++] The C++ Programming Language: 3. A Tour of C++: Abstraction Mechanism (4)

3.4 Templates

  벡터 자료형을 원하 누군가가 항상 double 값으로 된 벡터를 원하는 것은 아니죠. 벡터는 일반적인 개념이고, 부동소수점 수의 표기와는 독립적입니다. 결론적으로, 벡터 요소의 자료형은 이를 독립적으로 표현할 필요가 있죠. 템플릿(template)은 자료형과 값의 집합으로 파라미터화하는 클래스 혹은 함수입니다. 우리는 double과 같은 특정한 인자로 특정한 자료형과 함수를 생성하기 위한 일반적인 것으로 가장 이해되기 좋은 개념을 표현하기 위해 템플릿을 사용합니다.


3.4.1 Parameterized Types

  우리는 vector-of-double 자료형을 vector-of-anything 자료형으로 일반화할 수 있습니다. 템플릿과 특정한 자료형인 double을 인자로 재배치함으로써 말이죠. 예를 들면:

1
2
3
4
5
6
7
8
9
10
11
12
13
template<typename T>
class Vector {
private:
    T∗ elem; // elem points to an array of sz elements of type T
    int sz;
public:
    Vector(int s); // constructor: establish invariant, acquire resources
    ˜Vector() { delete[] elem; } // destructor: release resources
    // ... copy and move operations ...
    T& operator[](int i);
    const T& operator[](int i) const;
    int size() const { return sz; }
};
cs

  template<typename T> 접두어는 T를 뒤에 있는 선언의 파라미터로 만듭니다. 이는 수학에서 "모든 T에 대해" 혹은 "모든 T형에 대해"의 C++ 버전입니다.

  멤버 함수는 다음과 같이 표현될 것입니다:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template<typename T>
Vector<T>::Vector(int s)
{
    if (s<0) throw Negative_siz e{};
    elem = new T[s];
    sz = s;
}
 
template<typename T>
const T& Vector<T>::operator[](int i) const
{
    if (i<0 || size()<=i)
        throw out_of_rang e{"Vector::operator[]"};
    return elem[i];
}
cs

  이 주어진 정의들로, Vector를 다음과 같이 정의할 수 있습니다.

1
2
3
Vector<char> vc(200); // vector of 200 characters
Vector<string> vs(17); // vector of 17 strings
Vector<list<int>> vli(45); // vector of 45 lists of integers
cs

  Vector를 다음과 같이 사용해볼 수도 있겠죠:

1
2
3
4
5
void write(const Vector<string>& vs) // Vector of some strings
{
    for (int i = 0; i!=vs.size(); ++i)
        cout << vs[i] << '\n';
}
cs


  반복문을 지원하기 위해, 적절한 begin()과 end() 함수를 정의해야 합니다:

1
2
3
4
5
6
7
8
9
10
11
template<typename T>
T∗ begin(Vector<T>& x)
{
    return &x[0]; // pointer to first element
}
 
template<typename T>
T∗ end(Vector<T>& x)
{
    return x.begin()+x.size(); // pointer to one-past-last element
}
cs

  다음처럼 사용해볼 수 있겠죠: 

1
2
3
4
5
void f2(const Vector<string>& vs) // Vector of some strings
{
    for (auto& s : vs)
    cout << s << '\n';
}
cs

  비슷하게, 리스트, 벡터, 맵 등을 템플릿으로 정의할 수 있습니다.

  템플릿은 컴파일 시기의 메커니즘이며, 따라서 손으로 직접 쓴 코드와 비교해 전혀 런타임 오버헤드가 없습니다.


3.4.2 Function Templates

  템플릿은 단순히 컨테이너를 파라미터화하는 것보다 더 많은 쓰임새가 있습니다. 특히, 표준 라이브러리 안의 자료형과 알고리즘 양쪽의 파라미터화에 확장적으로 사용됩니다. 가령 우리는 어떤 컨테이너든 그 안의 요소 값의 합을 구하는 함수를 다음과 같이 작성할 수 있습니다:

1
2
3
4
5
6
7
template<typename Container, typename Value>
Value sum(const Container& c, Value v)
{
    for (auto x : c)
    v+=x;
    return v;
}
cs

  템플릿 인자인 Value와 함수 인자인 v는 호출자가 자료형과 계산자(sum으로부터 계산되는 변수)의 초기 값을 명시할 수 있도록 해줍니다.

1
2
3
4
5
6
7
8
void user(Vector<int>& vi, std::list<double>& ld, std::vector<complex<double>>& vc)
{
    int x = sum(vi,0); // the sum of a vector of ints (add ints)
    double d = sum(vi,0.0); // the sum of a vector of ints (add doubles)
    double dd = sum(ld,0.0); // the sum of a list of doubles
    auto z = sum(vc,complex<double>{}); // the sum of a vector of complex<double>
    // the initial value is {0.0,0.0}
}
cs

  double에 int를 더하는 요점은 가장 큰 int 숫자보다 더 큰 수를 정상적으로 처리하는 것입니다. sum<T,V>의 템플릿 인자의 자료형이 어떻게 함수 인자를 추론하는지 확인해보세요. 운이 좋게도, 우리는 이러한 자료형들을 명시적으로 명세할 필요가 없습니다. sum()은 스탠다드 라이브러리 accumulate()의 간단화된 버전입니다.


3.4.3 Function Objects

  템플릿의 특히 유용한 사례 중 하나는 함수 객체(function object, functor)입니다. 함수처럼 호출될 수 있는 객체를 정의하는 것이죠. 다음을 참고하세요:

1
2
3
4
5
6
7
template<typename T>
class Less_than {
    const T val; // value to compare against
public:
    Less_than(const T& v) :val(v) { }
    bool operator()(const T& x) const { return x<val; } // call operator
};
cs

  operator()라는 함수는 "function call", "call", 혹은 "application" 연산자 ()를 구현합니다. 우리는 일부 인수 타입에 대해 Less_than 타입의 명명된 변수를 정의할 수 있습니다:

1
2
Less_than<int> lti {42}; // lti(i) will compare i to 42 using < (i<42)
Less_than<string> lts {"Backus"}; // lts(s) will compare s to "Backus" using < (s<"Backus")
cs

  우리는 객체들을, 함수를 호출하듯이 호출할 수 있습니다.

1
2
3
4
5
6
void fct(int n, const string & s)
{
    bool b1 = lti(n); // true if n<42
    bool b2 = lts(s); // true if s<"Backus"
    // ...
}
cs

  이러한 함수 객체는 알고리즘의 인자로서 널리 사용됩니다. 예를 들어, 우리는 특정 조건문에서 참 값을 반환하는 값들의 개수를 셀 수 있습니다:

1
2
3
4
5
6
7
8
9
template<typename C, typename P>
int count(const C& c, P pred)
{
    int cnt = 0;
    for (const auto& x : c)
        if (pred(x))
            ++cnt;
    return cnt;
}
cs

  pred(predicate)는 참 혹은 거짓을 반환바딕 위해 호출 가능한 것입니다. 가령:

1
2
3
4
5
6
7
8
9
void f(const Vector<int>& vec, const list<string>& lst, int x, const string& s)
{
    cout << "number of values less than " << x
        << ": " << count(vec,Less_than<int>{x})
        << '\n';
    cout << "number of values less than " << s
        << ": " << count(lst,Less_than<string>{s})
        << '\n';
}
cs

  여기서 Less_than<int>{x}는 x라는 정수와 비교하는 호출 연산자를 지닌 객체를 생성합니다. Less_than<string>{s}는 s라는 문자열과 비교하는 호출 연산자를 지닌 객체를 생성합니다. 이러한 함수 객체들의 진가는 다른 것들과 비교하기 위해 값을 지니고 있다는 것입니다. 우리는 각 값(각 타입)에 대해 분리된 함수를 작성할 필요가 없습니다. 특정 값을 지니기 위해 전역 변수 따위를 사용할 필요도 없죠. 더군다나, Less_than과 같이 간단한 함수는 인라인 처리가 간단하므로, 우회적인 함수 호출보다 훨씬 더 효율적입니다. 데이터와 효율을 둘 다 잡을 수 있는 능력은 알고리즘의 인자로서 함수 객체가 손쉽게 사용될 수 있도록 합니다.

  일반적인 알고리즘의 핵심 연산의 의미를 명세하기 위해 사용되는 함수 객체를 종종 정책 객체(policy objects)라고 합니다.

  우리는 Less_than의 쓰임과 분리하여 정의해야 합니다. 이는 불편해 보일 수 있죠. 따라서, 암시적으로 함수 객체를 생성하는 표기법이 있습니다:

1
2
3
4
5
6
7
8
9
void f(const Vector<int>& vec, const list<string>& lst, int x, const string& s)
{
    cout << "number of values less than " << x
        << ": " << count(vec,[&](int a){ return a<x; })
        << '\n';
    cout << "number of values less than " << s
        << ": " << count(lst,[&](const string& a){ return a<s; })
        << '\n';
}
cs

  [&](int a){ return a<x; }라는 표기는 람다 표기법(lambda expression)이라고 불립니다. 이는 Less_than<int>{x}와 완전히 같은 함수 객체를 생성하죠. [&]는 지역 변수(x)가 참조로 전달될 것임을 명시하는 캡처 목록(capture list)입니다.(함수는 외부 변수를 참조할 수 없기 때문에, 파라미터로 전달하는 것과 같은 맥락입니다-옮긴 이) 우리가 오로지 x만을 "캡처"하고 싶었다면, [&x]와 같이 쓸 수 있습니다. x의 복사본으로 객체를 생성하고 싶었다면, [=x]와 같이 쓸 수 있습니다. 아무것도 캡처하지 않을 것이라면, []를 사용합니다. 모든 지역 변수들의 참조를 원한다면 [&]를, 모든 지역 변수들의 값을 원한다면 [=]를 사용합니다.

  람다를 사용하는 것은 편리하고 간결합니다. 하지만 모호하죠. 비자명한 행위들에 대해서는, 저는 이들의 이름을 명확히 하고 그들의 목적을 명확하게 진술하여 프로그램의 곳곳에서 적절히 사용할 수 있도록 만들기를 권합니다.

  우리는 포인터의 컨테이너의 요소들로부터 가리켜진(pointed) 각 객체에게 행위할 수 있도록 해주는 함수가 필요합니다:

1
2
3
4
5
6
template<class C, class Oper>
void for_all(C& c, Oper op) // assume that C is a container of pointers
{
    for (auto& x : c)
        op(∗x); //pass op() a reference to each element pointed to
}
cs

  이제 우리는 user() 형식의 버전을 작성할 수 있습니다. _all 함수 없이 말이죠:

1
2
3
4
5
6
7
8
void user()
{
    vector<unique_ptr<Shape>> v;
    while (cin)
        v.push_back(read_shape(cin));
    for_all(v,[](Shape& s){ s.draw(); }); // draw_all()
    for_all(v,[](Shape& s){ s.rotate(45); }); // rotate_all(45)
}
cs

  이는 Shape의 참조를 람다에게 전달하며, 람다는 각 객체가 컨테이너에 어떻게 저장되어있는지 상관하지 않습니다. 특히, 이 for_all() 함수는 v를 vector<Shape*>으로 변경하도 여전히 잘 동작할 것입니다.


3.4.4 Variadic Templates

  템플릿은 임의의 개수의, 임의의 자료형의 인자를 받아들이도록 정의될 수 있습니다. 그런 템플릿은 variadic template이라고 불립니다. 가령:

1
2
3
4
5
6
7
8
template<typename T, typename ... Tail>
void f(T head, Tail... tail)
{
    g(head); // do something to head
    f(tail...); // tr y again with tail
}
 
void f() { } // do nothing
cs

  variadic 템플릿을 구현하는 핵심은 여러분이 인자의 리스트를 전달할 때, 첫 번째 인자와 나머지 인자를 구분할 수 있다는 것입니다. 첫번째 인자(head)로 무언가를 하고 재귀적으로 나머지 인자(tail)와 함께 f()를 호출합니다. 생략 부호 ...은 목록의 나머지를 나타냅니다. 결국 당연하지만 tail은 마지막 함수 호출에서 빈 목록으로서 호출될 것입니다.

  f()를 다음과 같이호출할 수 있죠:

1
2
3
4
5
6
7
8
9
int main()
{
    cout << "first: ";
    f(1,2.2,"hello");
 
    cout << "\nsecond: "
    f(0.2,'c',"yuck!",0,1,2);
    cout << "\n";
}
cs

  이는 f(1, 2.2, "hello")를 호출할 것이며, f(2.2, "hello), f("hello"), f()를 호출할 것입니다. g(head)로는 무엇을 할 수 있을까요? 확실히, 실제 프로그램 내에서는 각 인자에게 우리가 원하는 무엇이든지 할 수 있을 것입니다. 예를 들어, 이를 그것의 인자 (here, head)를 출력하도록 작성할 수 있습니다.

1
2
3
4
5
template<typename T>
void g(T x)
{
    cout << x << " ";
}
cs

  출력은 다음과 같겠죠:

1
2
first: 1 2.2 hello
second: 0.2 c yuck! 0 1 2
cs

  이는 f()가 임의의 리스트 혹은 값들을 출력하는 printf()의 간단한 변형처럼 보입니다. 그들을 감싸는 추가적인 선언 세 줄로 말이죠.

  variadic 템플릿의 강점은 어떤 인자이든 그것들을 받아들일 수 있다는 것입니다. 반면 단점은 인터페이스의 타입 검사가 복잡한 템플릿 프로그램일 수 있다는 것입니다. 이는 다음에 이야기할 것입니다.


3.4.5 Aliases

  놀랍게도 가끔은 자료형이나 템플릿의 동의어를 도입하는 것이 유용할 때가 있습니다. 예를 들어 표준 헤더 <cstddef>는 size_t 별칭의 정의들을 포함합니다. 아마:

1
using size_t = unsigned int;
cs

  size_t의 실제 타입은 구현에 종속적입니다. 따라서 다른 size_t의 구현은 unsigned long일 수도 있죠. size_t의 별칭을 이용하는 것은 프로그래머가 포터블한 코드를 작성할 수 있도록 해줍니다.

  그들의 템플릿 인자와 연관된 타입의 별칭을 제공하는 것은 매개변수화된 타입들에게 아주 흔한 일입니다:

1
2
3
4
5
6
template<typename T>
class Vector {
public:
    using value_type = T;
    // ...
};
cs

  실제로, 모든 표준 라이브러리 컨테이너는 value_type을 그들 값의 타입의 이름처럼 제공합니다. 이는 이 컨벤션을 따르는 모든 컨테이너를 위해 작동할 코드를 작성할 수 있도록 합니다:

1
2
3
4
5
6
7
8
9
template<typename C>
using Element_type = typename C::value_type;
 
template<typename Container>
void algo(Container& c)
{
    Vector<Element_type<Container>> vec; // keep results here
    // ...
}
cs

(컨테이너가 어떤 자료형을 담고 있지는 모르나, Element_type<Container>를 통해 해당 컨테이너가 담는 자료형을 명시함으로써, Vector는 "Container가 담는 자료형"을 담을 수 있게 됩니다-옮긴 이)

  별칭 메커니즘은 몇몇 혹은 모든 템플릿 인자를 바인딩함으로써 새로운 템플릿을 정의하는데 사용될 수 있습니다:

1
2
3
4
5
6
7
8
9
template<typename Key, typename Value>
class Map {
    // ...
};
 
template<typename Value>
using String_map = Map<string,Value>;
 
String_map<int> m; // m is a Map<string,int>
cs

댓글

이 블로그의 인기 게시물

[코딩의탑] 4층: 툰 쉐이딩

[코딩의탑] 3층: 바다 렌더링

[코딩의탑] 5층: 포탈(Portal), 더 나아가기