함수 템플릿

xx.xx·2024년 3월 4일
0

c++

목록 보기
8/8

함수 템플릿

21.5: 함수 템플릿 선언하기

  • 함수 템플릿은 구체화될 때마다 그 코드는 결과 객체 모듈에 나타냄
  • 동일한 템플릿 구체화가 여러 객체 파일에 있을 때, 링커는 중복을 제거하고 최종 프로그램에서 한 가지만 남기려 함
  • 템플릿을 선언만 하는 경우, 컴파일러는 템플릿의 정의를 반복해서 처리할 필요가 없으며, 이는 컴파일 속도의 향상에 기여
  • 템플릿을 선언만 해도 구체화되지 않으며, 필요한 구체화는 다른 곳에서 사용할 때 이루어짐
template <typename T>
T add(T a, T b);

선언만 하는 경우에는 프로그래머가 요구된 실체가 존재하는지 확인하는 책임

21.5.1: 구체화 선언

함수 템플릿을 사용하면 컴파일 속도와 링크 속도가 향상,

최종 링크 시에 실제 템플릿 구체화 존재를 확인하기 위해 명시적 구체화 선언 사용

  • 키워드 template으로 시작하고 템플릿의 매개변수 리스트는 생략
  • 함수 템플릿의 반환 유형과 이름 지정
  • 함수 템플릿의 이름 다음에 유형 지정 리스트 사용
    • 유형 이름을 옆꺽쇠로 둘러 싼 리스트
  • 함수 템플릿의 매개변수 리스트를 지정 후 ; 으로 끝남
// 함수 템플릿 정의
template <typename T>
void myFunction(T parameter) {
		// 내용
}
// 템플릿 구체화 선언
template void myFunction<int>(int parameter);

선언이지만 컴파일러는 함수 템플릿의 특별한 변형을 구체화해 달라는 요청으로 이해

명시적으로 구체화를 선언하면 프로그램이 요구하는 템플릿 함수의 실체를 모두 파일 하나에 모을 수 있음

이 파일은 보통의 소스 파일로서 템플릿 정의 헤더를 포함해야 하고 이어서 필요한 명시적인 구체화 선언 지정

소스 파일이기 때문에 다른 소스에 포함되지 않음

add 함수 템플릿에 대하여 요구한 구체화를 보여주는 예

#include "add.h"
#include <string>
using namespace std;

template int add<int>(int const &lvalue, int const &rvalue);
template double add<double>(double const &lvalue, double const &rvalue);
template string add<string>(string const &lvalue, string const &rvalue);

빠진 구체화 선언을 위 리스트에 추가하기만 하면 된다. 파일을 다시 컴파일하고 링크하면 완성

21.6: 함수 템플릿을 구체화하기

컴파일러가 정의를 읽기만 하면 코드가 되는 평범한 함수와 다르게 템플릿은 정의를 읽어도 구체화되지 않음

컴파일러가 템플릿을 구체화하기로 결정하는 상황 두 가지

  • 사용될 때 구체화 ( add 함수를 한 쌍의 size_t 값을 가지고 호출 할 때)
  • 함수 템플릿의 주소를 받을 때 구체화
char (*addptr)(char const &, char const &) = add;

컴파일러가 템플릿을 구체화하도록 촉발시키는 서술문의 위치를 그 템플릿의 구체화 시점

컴파일러가 템플릿 유형 매개변수를 언제나 명확하게 추론할 수 없다

#include <iostream>
#include "add.h"

size_t fun(int (*f)(int *p, size_t n));
double fun(double (*f)(double *p, size_t n));

int main()
{
  cout << fun(add);
}

후보 함수가 두 개이기 때문에 모호 : fun의 중복정의 버전 각각에 대하여 add 함수를 구체화할 수 있기 때문

함수 템플릿은 모호성이 없을 경우에만 구체화될 수 있다.

#include <iostream>
#include "add.h"

int fun(int (*f)(int const &lvalue, int const &rvalue));
double fun(double (*f)(double const &lvalue, double const &rvalue));

int main()
{
  cout << fun(static_cast<int (*)(int const &, int const &)>(add));
}

static_cast를 사용하여 해결할 수도 있으나 유형을 강제로 변환하는 것은 피하는 것이 좋다

21.6.1: 구체화: `코드 비만' 없음

이 코드는 fun 함수를 통해 add 함수의 주소를 출력하는 간단한 예제

// source1.cc _ 함수 포인터와 void 포인터를 담는 구조체 정의
union PointerUnion
{
    int (*fp)(int const &, int const &);  // int-유형 인자를 받아들이는 함수 포인터
    void *vp;                             // void 포인터
};
#include <iostream>
#include "add.h"
#include "pointerunion.h"

// fun 함수 정의
void fun()
{
    PointerUnion pu = { add };  // PointerUnion 구조체를 선언하고, add 함수의 주소를 대입
    std::cout << pu.vp << '\n';  // 함수 포인터의 주소 출력
}
  1. PointerUnion 구조체:
    • union 키워드를 사용하여 PointerUnion이라는 구조체를 정의
    • 이 구조체는 두 가지 멤버
      • fp: int 형식의 두 인자를 받아들이는 함수 포인터
      • vp: void 포인터
  2. 헤더 파일 포함:
    • <iostream> 헤더: C++의 표준 입출력 스트림을 사용하기 위해 포함
    • "add.h" 헤더: add 템플릿 함수의 정의가 있는 헤더 파일을 포함
    • "pointerunion.h" 헤더: PointerUnion 구조체의 정의가 있는 헤더 파일을 포함
  3. fun 함수 정의:
    • fun 함수는 아무런 인자도 받지 않고 반환값도 없는 함수
    • PointerUnion 타입의 변수 pu를 선언하고, 이를 add 함수의 주소로 초기화
    • 함수 포인터의 주소를 출력

PointerUnion 구조체를 사용하여 함수 포인터와 void 포인터를 다루고 있음

source2.cc 파일은 source1.cc와 유사하게 fun 함수를 정의하고 있지만, add 템플릿에 대한 선언만 사용해 템플릿 선언

// source2.cc
#include <iostream>
#include "pointerunion.h"// 템플릿 선언: add 템플릿의 정의가 아닌 선언만 포함
template<typename Type>
Type add(Type const &, Type const &);

void fun()
{
    // PointerUnion 구조체를 선언하고, add 템플릿의 주소를 대입
    PointerUnion pu = { add };

    // 함수 포인터의 주소 출력
    std::cout << pu.vp << '\n';
}
  1. 헤더 파일 포함:
    • "pointerunion.h" 헤더: PointerUnion 구조체의 정의가 있는 헤더 파일 포함
  2. 템플릿 선언:
    • template<typename Type> Type add(Type const &, Type const &);와 같이 add 템플릿에 대한 선언만 포함
    • 템플릿 선언은 템플릿의 인터페이스를 제공하지만, 실제로 함수의 정의는 없음
  3. fun 함수 정의:
    • fun 함수는 아무런 인자도 받지 않고 반환값도 없는 함수
    • PointerUnion 타입의 변수 pu 선언하고, 이를 add 템플릿의 주소로 초기화
    • 함수 포인터의 주소를 출력

source2.ccadd 템플릿에 대한 선언만 제공, 해당 템플릿의 실체는 이 파일에서 정의되지 않음

이 파일은 다른 파일에서 정의된 add 템플릿의 실체를 사용하고 있음을 나타내는 역할

main.cc 파일은 add 템플릿에 대한 정의와 fun 함수를 정의

// main.cc
#include <iostream>
#include "add.h"
#include "pointerunion.h"
void fun();  // 함수 선언

int main()
{
    PointerUnion pu = { add };
    fun();
    std::cout << pu.vp << '\n';
}

설명:

  1. 헤더 파일 포함:
    • "add.h" 헤더: add 템플릿 함수의 정의가 있는 헤더 파일을 포함
    • "pointerunion.h" 헤더: PointerUnion 구조체의 정의가 있는 헤더 파일을 포함
  2. 함수 선언:
    • void fun();: fun 함수의 선언을 제공, 함수의 정의는 다른 파일에서 이루어짐
  3. main 함수 정의:
    • int main(): 프로그램의 시작점인 main 함수를 정의
    • PointerUnion 타입의 변수 pu를 선언하고, 이를 add 함수의 주소로 초기화
    • fun 함수를 호출
    • 함수 포인터의 주소를 출력

main.cc에서는 add 템플릿에 대한 정의가 제공되고 있으며, 이 정의된 함수의 주소를 PointerUnion 사용하여 출력

fun 함수를 호출하여 프로그램의 실행 흐름을 다룸

두 가지 다른 소스 파일로부터 생성된 오브젝트 모듈을 링크한 최종 프로그램

템플릿의 동작과 링커 동작에 대한 일부 특징

  1. 오브젝트 모듈 크기의 차이:
    • source1.osource2.o는 템플릿 함수 add의 구체화 여부에 따라 크기가 다름
    • source1.o에는 add 템플릿의 실체가 구체화되어 있으므로 크기가 큼
    • source2.o에는 add 템플릿의 선언만 있어서 크기가 작음
  2. 링크된 결과 프로그램의 크기와 동작:
    • main.osource1.o 링크한 경우, 두 모듈에 같은 템플릿 함수의 실체가 존재해 결과 프로그램은 해당 함수의 주소 두 번 출력
    • main.osource2.o 링크한 경우, 하나에만 템플릿 함수의 실체가 있어 결과 프로그램은 해당 함수의 주소 한 번만 출력
  3. 링커 동작:
    • 링커는 최종 프로그램으로부터 동일한 템플릿 객체를 제거
    • 템플릿 선언만 있는 source2.o와 템플릿 정의가 있는 source1.o를 링크하더라도 결과 프로그램은 하나의 템플릿 함수의 실체만 가짐
    • 단순히 템플릿을 선언하는 것만으로는 해당 템플릿의 객체가 생성되지 않음

이 실험을 통해 템플릿이 어떻게 동작하며, 링커가 최종 프로그램에 어떤 영향을 미치는지에 대한 통찰을 얻을 수 있음

21.7: 명시적인 템플릿 유형 사용하기

두 개의 중복 정의된 함수 버전이 서로 다른 유형의 인자를 기대하고 있었는데, 함수 템플릿을 구체화할 때 동일한 유형의 인자가 제공되면서 모호성 발생

해당 모호성을 해결하는 방법 중 하나는 static_cast를 사용하는 것이었으나, 강제 형변환은 가능하면 피하는 것이 좋음

명시적인 템플릿 유형 인자를 사용하여 이러한 모호성을 해결할 수 있음

이를 통해 컴파일러에게 함수 템플릿을 구체화할 때 사용해야 할 실제 유형을 알려줄 수 있습니다.

아래는 명시적인 템플릿 유형 인자를 사용하여 모호성을 해결하는 예제 코드

#include <iostream>
#include "add.h"
// int 유형의 함수를 기대하는 버전
int fun(int (*f)(int const &lvalue, int const &rvalue));

// double 유형의 함수를 기대하는 버전
double fun(double (*f)(double const &lvalue, double const &rvalue));

int main()
{
// 명시적인 템플릿 유형 인자를 사용하여 add<int>를 전달
std::cout << fun(add<int>) << '\n';
}

컴파일러에게 fun 함수를 호출할 때 사용해야 하는 실제 템플릿 유형을 알려주는 것이 가능

-(*f)는 함수 포인터를 나타냅니다. 여기서 f는 함수 포인터 변수이며, (*f)는 해당 함수 포인터가 가리키는 함수를 호출하는 것을 의미

21.8: 함수 템플릿 중복정의하기

 int main()
    {
        add(add(2, 3), 4);
    }

세 개의 개체의 합을 계산하고 싶을 경우 다음과 같이 사용할 수 있다.

객체 세 개를 자주 사용할 필요가 있다면 세 개의 인자를 받는 중복 정의 버전의 add 함수를 만드는 것이 좋다

 template <typename Type>
 Type add(Type const &lvalue, Type const &rvalue)
{
  return lvalue + rvalue;
}
template <typename Type>
Type add(Type const &lvalue, Type const &mvalue, Type const &rvalue)
{
 return lvalue + mvalue + rvalue;
}
 template <typename Type>
    Type add(std::vector<Type> const &vect)
    {
        return accumulate(vect.begin(), vect.end(), Type());
    }

std::accumulate를 사용하여 벡터의 시작과 끝 지정, 초기값으로 Type() 사용

//std::vector의 원소들의 합 계산하여 반환

원래의 함수 정의:

cppCopy code
template <typename Container, typename Type>
Type add(Container const &cont, Type const &init)
{
    return std::accumulate(cont.begin(), cont.end(), init);
}

//init 매개변수를 매개변수 리스트에서 빼버리고 기본 초기화 값을 사용할 수 있는데, 이렇게 변경하면 함수 호출 시 init 값을 생략할 수 있게 됩니다. 그러나 순서를 바꾸어서 Type을 맨 앞에 두면서 함수 호출 시에 Type을 명시적으로 지정해주는 형태가 됩니다.

변경된 함수 정의:

cppCopy code
template <typename Type, typename Container>
Type add(Container const &cont)
{
    return std::accumulate(cont.begin(), cont.end(), Type());
}

//순서를 바꾸어서 Type이 앞에 오면, 예를 들어 add<int>(vectorOfInts)처럼 템플릿 유형 매개변수 Type을 명시적으로 지정해주어야 합니다. 그렇지 않으면 컴파일러가 Type을 결정할 수 없는 상황이 발생합니다.

그리고 더 나아가, 템플릿 템플릿 매개변수를 사용하면 컴파일러가 컨테이너로부터 직접 Type을 결정할 수 있게 됩니다. 이 부분은 뒤에서 논의될 템플릿 템플릿 매개변수(Template Template Parameter)에 관한 내용입니다.

21.8.1: 중복정의 함수 템플릿을 사용하는 예제

int main()
{
  vector<int> v;
  add(3, 4);          // 1 
  add(v);             // 2
  add(v, 0);          // 3
}

1) 두 개 모두 int 그러므로 add<int> 구체화

2) std::vector를 기대하는 중복정의 함수 템플릿을 선택

add<int>(std::vector<int> const &)

3) 유형이 다른 두 개의 개체 기대하는 정의, std::vector가 begin과 end 지원

add<std::vector<int>, int>(std::vector<int> const &, int const &)

21.8.2: 함수 템플릿을 중복정의할 때의 모호성에 관하여

유형에 상관없이 인자를 받는다. 이 함수 템플릿은 operator+가 함수의 실제 사용된 유형 사이에 정의되어 있지만 그 말고는 문제가 없어 보일 경우에만 사용할 수 있다. 다음은 유형을 가리지 않고 인자를 받는 중복정의 버전이다.

template <typename Type1, typename Type2, typename Type3>
Type1 add(Type1 const &lvalue, Type2 const &mvalue, Type3 const &rvalue)
{
  return lvalue + mvalue + rvalue;
}

main()
{
	add(1, 2, 3);
}

컴파일러는 Type == int이라고 추론하고서 앞의 함수를 선택할 수 있지만 Type1 == int와 Type2 == int 그리고 Type3 == int라고 추론하고서 뒤의 함수를 선택할 수도 있다. 놀랍게도 컴파일러는 모호하다고 불평하지 않는다.

21.9: 유형을 우회하기 위하여 템플릿 특정화하기

  • 최초의 add 템플릿은 대부분 잘 작동하지만, char *와 같이 일부 유형에서는 무의미한 경우가 있음

    • 이 문제를 해결하기 위하여 템플릿의 명시적인 특정화 정의
  • 함수 템플릿이나 클래스 템플릿에 대해 특정한 유형이나 값에 대한 명시적인 정의를 제공하는 것

    • 일반적인 템플릿은 여러 유형이나 값에 대해 일반적으로 작동하는 코드를 제공
    • 특정한 상황에서는 이를 더 구체적으로 특정화
template <typename Type>
Type *add(Type const *t1, Type const *t2)
{
  std::cout << "Pointers\n";
  return new Type;
}

컴파일러는 인자의 일치성과 형 변환을 고려하여 가장 적절한 함수 템플릿 선택

명시적인 특정화는 특수한 경우에 사용되지만, 일반적인 경우에서는 원래의 템플릿이 선택되는 것이 일반적

~~

21.9.1: 너무 많은 특정화를 피하기

  • add 함수 템플릿에 문자를 가리키는 const 그리고 비const 포인터에 대하여 두 개의 특정화
  • 두 가지 명시적인 특정화 추가
    • 하나는 char const *에 대한 것이고 다른 하나는 char *에 대한 것
// 원래의 함수 템플릿
template <typename Type>
Type add(Type const &lvalue, Type const &rvalue)
{
return lvalue + rvalue;
}

template <> char *add<char *>(char *const &p1,char *const &p2)
{
  std::string str(p1);
  str += p2;
  return strcpy(new char[str.length() + 1], str.c_str());
}

template <> char const *add<char const *>(char const *const &p1,char const *const &p2)
{
  static std::string str;
  str = p1;
  str += p2;
  return str.c_str();
}

21.9.2: 특정화 선언하기

템플릿의 명시적 특정화를 선언할 때에는 보통 함수 템플릿과 비슷한 방식으로 선

몸체를 ;으로 대체

템플릿의 명시적인 특정화 선언 시, 컴파일러가 유형을 추론할 수 있으면 템플릿 유형 매개변수를 명시적으로 지정하지 않아도 됨

char (const) * 특정화의 경우라면 다음과 같이 선언

template <> char *add(char *const &p1, char *const &p2)

template <> char const *add(char const *const &p1, char const *const &p2);

게다가 template <>을 생략할 수 있다면 템플릿 문자(쌍반점)는 선언으로부터 제거될 것이다.

결과로 나온 선언은 이제 단순히 함수 선언에 불과하다. 이것은 에러가 아니다.

함수 템플릿과 평범한 (비-템플릿) 함수는 서로 중복정의할 수 있다.

평범한 함수는 함수 템플릿에 비해 유형 변환에 제한이 없다. 이 때문에 이따금씩 평범한 함수로 템플릿을 중복정의하기도 한다.

함수 템플릿의 명시적 특정화는 그냥 함수 템플릿의 또다른 중복정의 버전에 불과하다.

중복정의 버전은 완전히 다른 집합의 템플릿 매개변수를 정의할 수 있는 반면에, 특정화는 비-특정화된 변형과 똑 같은 템플릿 매개변수의 집합을 사용해야 한다.

컴파일러는 실제 템플릿 인자가 특정화로 정의된 유형에 부합하는 상황에 특정화를 사용한다 (인자 집합에 부합하여 매개변수가 가장 특정화된 집합이 사용된다는 규칙을 따른다).

다양한 매개변수 집합에 대하여 중복정의 버전의 함수를 (또는 함수 템플릿을) 사용해야 한다.

  • 템플릿 특수화에서의 인자 유추:
    명시적 특수화의 경우, 함수를 호출할 때 템플릿 인자를 명시적으로 지정하지 않아도 됨
    컴파일러는 함수의 매개변수로부터 템플릿 인자를 추론할 수 있음
  • template <> 생략: template <> 생략하면 컴파일러는 그냥 일반 함수 중복 정의로 처리 하지만 명시적으로 적어주는 것이 가독성 높이고 명시적으로 함수가 명시적인 특수화라는 것 나타냄
  • 중복 정의와의 관계:
    명시적 특수화는 단순히 함수 템플릿의 또 다른 버전으로 간주되며,
    중복 정의된 함수 템플릿과 동일한 매개변수 세트를 사용해야 함

명시적 특수화를 사용하면 특정한 유형에 대해 다른 구현을 제공할 수 있어 유용

21.9.3: 삽입 연산자를 사용할 때의 복잡성

명시적인 특정화와 중복정의를 다루어 보았으므로 이제 클래스가 std::string

변환 연산자를 정의할 때

변환 연산자는 rvalue로 사용될 것이라고 보장된다. 이것은 string 변환 연산자가 정의된 클래스의 객체를 string 객체에 할당할 수 있다는 뜻이다. 그러나 string 변환 연산자를 정의한 객체를 스트림에 삽입하려고 시도하면 컴파일러는 부적절한 유형을 ostream에 삽입하려 한다고 불평한다.

반면에 이 클래스가 int 변환 연산자를 정의하고 있으면 문제없이 삽입된다.

이렇게 구분하는 이유는 operator<<가 기본 유형을 (예를 들어 int 유형을) 삽입할 때는 평범한 (자유) 함수로 정의되어 있지만 string을 삽입할 때는 함수 템플릿으로 정의되어 있기 때문이다. 그러므로 string 변환 연산자를 정의한 클래스의 객체를 삽입하려고 할 때 컴파일러는 ostream 객체로 삽입하는 삽입 연산자의 모든 중복정의 버전을 방문한다.

기본 유형의 변환이 없으므로 기본 유형의 삽입 연산자는 사용할 수 없다. 템플릿 인자에 대한 변환은 변환 연산자를 찾아 보도록 컴파일러에게 허용하지 않기 때문에 string 변환 연산자를 정의한 우리의 클래스는 ostream에 삽입할 수 없다.

그런 클래스의 객체를 ostream 객체에 삽입한다고 하더라도 클래스는 (string 인자에서 클래스의 객체를 rvalue로 사용하는 데 요구되는 string 변환 연산자 말고도) 반드시 자신만의 중복정의 삽입 연산자를 정의해야 한다.

클래스가 std::string변환 연산자를 정의할 때, 해당 객체를 ostream에 삽입하려고 하면

컴파일러는 부적절한 유형을 ostream에 삽입하려 한다고 함

이는 operator<<가 기본 유형을 삽입할 때는 평범한 함수로 정의되어 있지만,

string을 삽입할 때는 함수 템플릿으로 정의되어 있기 때문

그러므로 string 변환 연산자를 정의한 클래스의 객체를 삽입하려고 할 때

컴파일러는 ostream 객체로 삽입하는 삽입 연산자의 모든 중복정의 버전을 방문

이 클래스의 객체를 ostream 객체에 삽입하더라도 클래스는 반드시 자체적인 중복정의 삽입 연산자를 정의해야 함

0개의 댓글