Menu

C++ 템플릿: 제네릭 함수와 클래스 완벽 정리

C++ 템플릿을 사용하면 코드를 한 번만 작성해 모든 타입에서 동작시킬 수 있습니다. 함수 템플릿, 클래스 템플릿, 타입 추론, 그리고 그로 인해 발생하는 헷갈리는 컴파일러 오류를 살펴봅니다.

이 페이지에는 실행 가능한 에디터가 있습니다 - 편집하고 실행하면 결과를 바로 볼 수 있습니다.

한 번 작성하고, 모든 타입에 사용하기

이전 페이지에서는 std::sortvector<int>를 정렬했습니다. 하지만 std::sortvector<string>, vector<double>, 심지어 여러분이 직접 만든 구조체 배열도 정렬합니다. 그것도 각각을 위한 별도의 sort를 아무도 작성하지 않고서 말이죠. 이것은 마법도 아니고 오버로딩도 아닙니다. 템플릿 입니다. 컴파일러가 여러분이 건네는 어떤 타입이든 재사용하는 단 하나의 코드 조각이죠.

템플릿이 없다면 타입마다 같은 로직을 복사-붙여넣기 하는 신세를 면치 못할 것입니다. 다음은 같은 maximum 함수를 세 번 작성한 것으로, 바로 템플릿이 없애려고 존재하는 그 중복입니다.

int    maximum(int a, int b)       { return a > b ? a : b; }
double maximum(double a, double b) { return a > b ? a : b; }
string maximum(string a, string b) { return a > b ? a : b; }

본문은 동일합니다. 다른 것은 타입뿐입니다. 템플릿을 쓰면 "이것은 어떤 타입 T에서도 동작한다"고 말하고 한 번만 작성할 수 있습니다.

함수 템플릿

함수 앞에 template <typename T>를 붙이고, 원래 구체적인 타입이 들어갈 자리마다 T를 사용하면 함수가 템플릿이 됩니다.

maximum<int>maximum<double>라고 한 번도 쓰지 않았다는 점에 주목하세요. 컴파일러는 인자를 보고 T가 무엇이어야 하는지 알아냅니다. 이것이 템플릿 인자 추론 입니다. 서로 다른 타입으로 호출할 때마다 컴파일러는 뒤편에서 별도의 구체적인 함수를 인스턴스화(생성)합니다.

추론이 도움이 되지 않을 때는 꺾쇠 괄호를 사용해 타입을 명시적으로 적을 수 있습니다.

추론에는 흔한 함정이 숨어 있습니다. T하나의 타입이어야 하므로, 인자 타입을 섞으면 추론이 깨집니다.

maximum(3, 7.5);   // 오류: T가 int인가 double인가? 컴파일러는 추측하기를 거부합니다.

이는 명시적으로 적어서 고칠 수 있고 - maximum<double>(3, 7.5) - 또는 각 매개변수에 자신만의 타입 매개변수를 주어 고칠 수도 있는데, 다음에 그렇게 해 보겠습니다.

여러 개의 타입 매개변수

템플릿은 타입 하나에만 국한되지 않습니다. 필요한 만큼 쉼표로 구분해 나열하세요. 매개변수가 서로 다른 타입이 될 수 있는 함수는 이렇게 작성합니다.

반환 타입이 매개변수에 따라 달라질 때는 auto(C++14 이상)로 컴파일러가 알아내게 두세요. 이는 템플릿과 자연스럽게 어울립니다.

클래스 템플릿

템플릿은 함수만을 위한 것이 아닙니다. 클래스 전체도 템플릿으로 만들 수 있습니다. 표준 컨테이너가 바로 이 방식으로 동작합니다. vector<int>, map<string, int>, pair<A, B>는 모두 클래스 템플릿입니다. 자료 구조를 한 번 작성하면, 매개변수로 지정한 어떤 타입이든 저장합니다.

다음은 어떤 타입의 값이든 하나 담는 작은 제네릭 Box입니다.

함수 템플릿과의 핵심 차이: 클래스 템플릿에서는 보통 꺾쇠 괄호 안에 타입을 직접 적어 주어야 합니다 - Box<int> - 오래된 표준에서는 타입을 추론할 생성자 인자가 없기 때문입니다. (C++17에서 클래스 템플릿 인자 추론 이 추가되어 Box b(42);도 동작하지만, 명시적으로 적는 것은 언제나 안전하고 읽기에도 분명합니다.)

오류는 엄청나게 클 것입니다 - 그 이유

이 부분에서 누구나 걸려 넘어지므로, 분명하게 말해 둘 가치가 있습니다. 템플릿은 실제 타입으로 인스턴스화 될 때에만 완전히 검사됩니다. <를 사용하는 템플릿을 작성해도 그 자체로는 문제없이 컴파일됩니다. 오류는 <가 없는 타입으로 인스턴스화하는 바로 그 순간에만 나타납니다.

template <typename T>
T maximum(T a, T b) {
    return a > b ? a : b;   // T가 >를 지원해야 함
}

struct Point { int x, y; };

// maximum(Point{1,2}, Point{3,4});
// 오류: Point에는 operator >가 없음. 메시지는 Point를 지목하면서
// 이 함수 전체를 인용하며, 종종 여러 줄에 걸칩니다.

컴파일러가 전체 타입을 템플릿에 대입하고 생성된 코드 내부에서 실패를 보고하기 때문에, 단 하나의 실수가 라이브러리 내부를 언급하는 출력의 벽을 만들어 낼 수 있습니다. 두 가지 생존 요령:

  • 첫 번째 오류를 읽으세요. 마지막 오류가 아닙니다. 뒤따르는 오류는 보통 첫 번째 오류의 여파입니다.
  • 메시지에서 여러분 자신의 타입 이름(여기서는 Point)을 훑어보세요. 그것이 어떤 인스턴스화가 잘못되었는지 알려 줍니다.

진짜 해결책은 여러분의 타입이 템플릿이 필요로 하는 것을 무엇이든 지원하도록 만드는 것입니다. maximum의 경우 그것은 Pointoperator> 오버로딩하기를 뜻하며, 이는 이후 페이지의 주제입니다. 현대 C++20의 컨셉(concepts) 은 이런 오류를 더 이른 시점으로 옮기고 읽기 쉽게 만들 수 있지만, 그 아래에 깔린 치환 모델은 동일합니다.

다음: 클래스

여러분은 방금 템플릿에 집중하면서 Box 클래스 템플릿 - 비공개 데이터, 생성자, 멤버 함수를 갖춘 클래스 - 을 만들었습니다. 다음 페이지는 속도를 늦추고 클래스 를 제대로 가르칩니다. 데이터를 그것을 다루는 함수와 어떻게 묶는지, publicprivate이 실제로 무엇을 통제하는지, 그리고 멤버 함수가 객체 자신의 상태에 어떻게 접근하는지 말이죠. 실제 C++에서는 템플릿과 클래스가 끊임없이 결합되므로, 클래스를 탄탄히 익혀 두면 제네릭 코드를 작성하기가 훨씬 수월해집니다.

자주 묻는 질문

C++에서 템플릿이란 무엇인가요?

템플릿은 함수나 클래스를 한 번만 작성해 두면, 사용하는 타입마다 컴파일러가 그에 맞는 버전을 생성해 주는 청사진입니다. template <typename T>라고 쓴 다음, 실제 타입 대신 T를 사용합니다. 컴파일러는 구체적인 버전을 찍어내는데, 이를 인스턴스화(instantiation) 라고 합니다.

C++ 템플릿에서 typenameclass의 차이는 무엇인가요?

템플릿 매개변수 목록에서 template <typename T>template <class T>완전히 같은 뜻 입니다. 오늘날에는 보통 typename이 선호되는데, T는 클래스뿐 아니라 어떤 타입이든 될 수 있어 더 솔직하게 읽히기 때문입니다. 어떤 키워드를 고르든 생성되는 코드에는 아무 영향이 없습니다.

C++ 템플릿 오류 메시지는 왜 이렇게 긴가요?

템플릿은 작성될 때가 아니라 실제 타입으로 인스턴스화 될 때 검사됩니다. 어떤 타입이 여러분이 사용한 연산(정렬용 < 등)을 지원하지 않으면, 인스턴스화된 타입 전체가 그대로 펼쳐진 채 라이브러리 코드 깊숙한 곳에서 오류가 나타나 몇 페이지나 되는 출력이 생깁니다. 첫 번째 오류를 읽고 그 안에서 여러분의 타입 이름을 찾으세요.

Coddy programming languages illustration

Coddy로 코딩 배우기

시작하기