Program Language/C & C++

[C++] Class Template

야곰야곰+책벌레 2022. 2. 10. 17:29
728x90
반응형

데이터 멤버

보통 클래스의 경우 클래스 템플릿에는 모든 유형의 데이터 멤버가 포함될 수 있다. non-static 멤버는 해당 저의 또는 생성자에서 초기화할 수 있다.

template<typename T>
struct X {
	int m1 = 7;
	T m2;
	X(const T& x) :m2{x} { }
};

X<int> xi {9};
X<string> xs {"Rapperswil"};

non-static 데이터 멤버는 const일 수 있지만 constexpr일 수는 없다.

멤버 함수

보통 클래스는 클래스 템플릿의 non-static 멤버 함수를 클래스 내부 또는 외부에서 정의할 수 있다.

template<typename T>
struct X {
	void mf1() { /* ... */ } // defined in-class
	void mf2();
};

template<typename T>
void X<T>::mf2() { /* ... */ } // defined out of class

마찬가지로 템플릿의 멤버 함수는 가상일 수도 있고 아닐 수도 있다. 그러나 가상 멤버 함수는 멤버 함수 템플릿도 될 수 없다.

멤버 유형 별칭

using 또는 typedef를 사용하여 도입된 멤버 유형 별칭은 클래스 템플릿 디자인에서 중요한 역할을 한다. 클래스 외부에서 쉽게 액세스 할 수 있는 방식으로 클래스의 관련 유형을 정의한다. 예를 들어 컨테이너의 반복자와 요소 유형을 별칭으로 지정한다.

template<typename T>
class Vector {
public:
	using value_type = T;
	using iterator = Vector_iter<T>; // Vector_iter is defined elsewhere
	// ...
};

템플릿 인수 이름 T는 템플릿 자체에서만 액세스 할 수 있으므로 다른 코드가 요소 유형을 참조하려면 별칭을 제공해야 한다.

유형 별칭은 클래스 디자이너가 공통 의미 체계를 사용하여 다른 클래스(및 클래스 템플릿)의 유형에 대한 공통 이름을 제공할 수 있도록 함으로써 일반 프로그래밍에서 중요한 역할을 한다. 멤버 별칭인 형식 이름은 종종 associated type이라고 한다. value_type 및 iterator 이름은 표준 라이브러리의 컨테이너 디자인에서 차용한다. 클래스에 원하는 멤버 별칭이 없으면 trait를 사용하여 보완할 수 있다.

static 멤버

클래스에 정의되지 않은 static 데이터 또는 함수 멤버는 프로그램에서 고유한 정의를 가져야 한다.

template<typename T>
struct X {
	static constexpr Point p {100,250}; // Point must be a literal type (§10.4.3)
	static const int m1 = 7;
	static int m2 = 8; // error : not const
    static int m3;
	static void f1() { /* ... */ }
	static void f2();
};

template<typename T> int X<T>::m1 = 88; // error : two initializers
template<typename T> int X<T>::m3 = 99;
template<typename T> void X::<T>::f2() { /* ... */ }

템플릿이 아닌 클래스의 경우 리터럴 유형의 const 또는 constexpr static 데이터 멤버는 클래스 내에서 초기화될 수 있으며 클래스 외부에서 정의할 필요가 없다.

static 멤버는 사용되는 경우에만 정의하면 된다.

template<typename T>
struct X {
	static int a;
	static int b;
};
int∗ p = &X<int>::a;

이것이 프로그램에서 X<int>에 대한 모든 언급이라면 X<int>::a에 대해서는 '정의되지 않음' 오류가 발생하지만 X<int>::b에 대해서는 그렇지 않다.

멤버 유형

보통 클래스의 경우 유형을 멤버로 정의할 수 있다. 평소와 같이 이러한 유형은 클래스 또는 열거형이 될 수 있다.

template<typename T>
struct X {
	enum E1 { a, b };
	enum E2; // error : under lying type not known
	enum class E3;
	enum E4 : char;
	struct C1 { /* ... */ };
	struct C2;
};

template<typename T>
enum class X<T>::E3 { a, b }; // needed

template<typename T>
enum class X<T>::E4 : char { x, y }; // needed

template<typename T>
struct X<T>::C2 { /* ... */ }; // needed

멤버 열거의 클래스 외 정의는 기본 유형을 알고 있는 열거에만 허용된다.

평소와 같이 클래스가 아닌 열거형의 결거자는 열거형 범위에 배치된다. 즉, 멤버 열거의 경우 열거자는 해당 클래스의 범위에 있다.

멤버 템플릿

클래스 또는 클래스 템플릿에는 자체 템플릿인 멤버가 있을 수 있다. 이를 통해 만족스러운 수준의 제어와 유연성으로 관련 유형을 나타낼 수 있다. 예를 들어, 복소수는 일부 스칼라 유형의 값의 쌍으로 가장 잘 표현된다.

template<typename Scalar>
class complex {
	Scalar re, im;
public:
	complex() :re{}, im{} {} // default constructor
	template<typename T>
	complex(T rr, T ii =0) :re{rr}, im{ii} { }
    
	complex(const complex&) = default; // copy constr uctor
	template<typename T>
		complex(const complex<T>& c) : re{c.real()}, im{c.imag()} { }
	// ...
};

이를 통해 복잡한 유형 간에 수학적으로 의미 있는 변환을 허용하는 동시에 바람직하지 않은 축소 변환을 방지한다.

complex<float> cf; // default value
complex<double> cd {cf}; // OK: uses float to double conversion
complex<float> cf2 {cd}; // error : no implicit double->float conversion

complex<float> cf3 {2.0,3.0}; // error : no implicit double->float conversion
complex<double> cd2 {2.0F,3.0F}; // OK: uses float to double conversion

class Quad {
	// no conversion to int
};

complex<Quad> cq;
complex<int> ci {cq}; // error : no Quad to int conversion

복소수에 대한 이러한 정의가 주어지면 T2에서 T1을 구성할 수 있는 경우에만 complex<T2> 또는 T2 값 쌍에서 complex<T1>을 구성할 수 있다.

complex<double>에서 complex<float>로 축소 오류는 complex<float>의 템플릿 생성자가 인스턴스화될 때까지 그리고 멤버 intializer에서 {}초기화 구문을 사용했기 때문에 포착되지 않는다는 점을 주의하자. 해당 구문은 축소를 허용하지 않는다.

()구문을 사용하면 오류를 좁힐 수 있다.

template<typename Scalar>
class complex { // old style
	Scalar re, im;
public:
	complex() :re(0), im(0) { }
	template<typename T>
	complex(T rr, T ii =0) :re(rr), im(ii) { }
    
	complex(const complex&) = default; // copy constr uctor
	template<typename T>
		complex(const complex<T>& c) : re(c.real()), im(c.imag()) { }
	// ...
};

complex<float> cf4 {2.1,2.9}; // ouch! narrows
complex<float> cf5 {cd}; // ouch! narrows

이것이 초기화에 {} 표기법을 일관되게 사용하는 또 다른 이유라고 생각한다.

템플릿과 생성자

혼동 가능성을 최소화하기 위해 기본 복사 생성자를 명시적으로 추가했다. 그대로 두면 정의의 의미가 변경되지 않는다. 기술적인 이유로 템플릿 생성자는 복사 생성자를 생성하는데 사용되지 않으므로 명시적으로 선언된 복사 생성자가 없었다면 기본 복사 생성자가 생성되었을 것이다. 마찬가지로 복사 할당, 이동 생성자 및 이동 할당은 템플릿이 아닌 연산자로 정의해야 한다. 그렇지 않으면 기본 버전이 생성된다.

템플릿과 virtual

멤버 템플릿은 virtual일 수 없다.

class Shape {
	// ...
	template<typename T>
		virtual bool intersect(const T&) const =0; // error : vir tual template
};

이것은 명백히 룰 위반이다. 허용되면 virtual 기능을 구현하기 위한 기존 virtual 기능 테이블 기술을 사용할 수 없다. 링커는 누군가가 새 인수 유형으로 intersect()를 호출할 때마다 Shape 클래스의 가상 테이블에 새 항목을 추가해야 한다. 이러한 방식으로 ㄹ이커 구현을 복잡하게 만드는 것은 허용되지 않는 것으로 간주되었다. 특히 동적 연결을 처리하려면 가장 일반적으로 사용되는 것과는 다른 구현 기술이 필요하다.

Nesting 사용

일반적으로 정보는 가능한 지역적으로 유지하는 것이 좋다. 그렇게 하면 이름을 찾기 쉽고 프로그램의 다른 항목을 방해할 가능성이 줄어든다. 이러한 사고 방식으로 인해 유형이 멤버로 정의된다. 그렇게 하는 것이 좋은 생각인 경우가 많다. 그러나 클래스 템플릿 멤버의 경우 매개변수화가 멤버 유형에 적합한지 고려해야 한다. 공식적으로 템플릿의 구성원은 템플릿의 모든 인수에 의존한다. 멤버의 동작이 실제로 모든 템플릿 인수를 사용하지 않는 경우 불행한 부작용이 있을 수 있다. 유명한 예로 연결 목록의 연결 유형이 있다.

template<typename T, typename Allocator>
class List {
private:
	struct Link {
		T val;
		Link∗ succ;
		Link∗ prev;
	};
	// ...
};

여기서 Link는 List의 구현 세부 사항이다. 따라서 최고의 유형의 완벽한 예인 것 같다. List의 범위에서 정의되고 비공개로 유지된다. 이것은 인기 있는 디자인이며 일반적으로 매우 잘 작동한다. 그러나 놀랍게도 nonlocal Link 유형을 사용하는 경우에 비해 성능 비용이 발생할 수 있다. Link의 어떤 멤버도 Allocator 매개변수에 의존하지 않고 List<double, My_allocator>가 필요하다고 가정한다. 이제 List<double, My_allocator>::Link와 List<double, Your_allocator>::Link는 다른 유형이므로 이를 사용하는 코드는 (영리한 최적화 도구 없이는) 동일할 수 없다. 즉, List의 두 템플릿 중 하나만 사용할 때 Link를 구성원으로 만드는 것은 코드 부풀림을 의미한다. 이것은 Link가 구성원이 아닌 디자인을 고려하도록 이끈다.

template<typename T, typename Allocator>
class List;

template<typename T>
class Link {
	template<typename U, typename A>
		friend class List;
	T val;
	Link∗ succ;
	Link∗ prev;
};

template<typename T, typename Allocator>
class List {
	// ...
};

링크의 모든 구성원을 비공개로 설정하고 목록 액세스 권한을 부여했다. Link라는 이름을 non-local로 만드는 것을 제외하고는 Link가 List의 구현 세부 사항이라는 디자인 의도를 유지한다.

그러나 중첩 클래스가 구현 세부 사항으로 간주되지 않으면 어떻게 될까? 즉, 다양한 사용자를 위한 연관 유형이 필요한 경우 어떻게 할까?

template<typename T, typename A>
class List {
public:
	class Iterator {
		Link<T>∗ current_position;
	public:
		// ... usual iterator operations ...
	};
    
	Iterator<T,A> begin();
	Iterator<T,A> end();
	// ...
};

여기서 멤버 유형 List<T,A>::Iterator는 두 번째 템플릿 인수 A를 사용하지 않는다. 그러나 Iterator는 멤버이고 따라서 형식적으로 A에 의존하기 때문에 (컴파일러는 이와 반대되는 것을 알지 못한다.) 할당자를 사용하여 구성한 방법과 독립적으로 List를 처리하는 함수를 작성할 수 없다.

void fct(List<int>::Iterator b, List<int>::Iterator e) // error : List takes two arguments
{
	auto p = find(b,e,17);
	// ...
}

void user(List<int,My_allocator>& lm, List<int,Your_allocator>& ly)
{
	fct(lm.begin(),lm.end());
	fct(ly.begin(),ly.end());
}

대신 할당자 인수에 의존하는 함수 템플릿을 작성해야 한다.

void fct(List<int,My_allocator>::Iterator b, List<int,My_allocator>::Iterator e)
{
	auto p = find(b,e,17);
	// ...
}

그러나 그것은 User()를 깨뜨린다.

void user(List<int,My_allocator>& lm, List<int,Your_allocator>& ly)
{
	fct(lm.begin(),lm.end());
	fct(ly.begin(),ly.end()); // error : fct takes List<int,My_allocator>::Iterators
}

fct 템플릿을 만들고 각 할당자에 대해 별도의 전문화를 생성할 수 있다. 그러나 Iterator를 사용할 때마다 새로운 specialization이 생성되므로 상당한 코드 팽창이 발생할 수 있다. 다시, 링크를 클래스 템플릿 밖으로 이동하여 문제를 해결한다.

template<typename T>
struct Iterator {
	Link<T>∗ current_position;
};

template<typename T, typename A>
class List {
public:
	Iterator<T> begin();
	Iterator<T> end();
	// ...
};

이렇게 하면 유형에 관한 한 동일한 첫 번째 템플릿 인수를 가진 모든 List에 대한 반복자를 서로 교환할 수 있다. 이제 user()가 정의된 대로 동작한다. 

Friends

템플릿 클래스는 함수를 friend로 지정할 수 있다. 행렬 및 벡터 예제를 고려해 보자. 일반적으로 Matrix와 Vector는 모두 템플릿이다.

template<typename T> class Matrix;
template<typename T>
class Vector {
	T v[4];
public:
	friend Vector operator∗<>(const Matrix<T>&, const Vector&);
	// ...
};

template<typename T>
class Matrix {
	Vector<T> v[4];
public:
	friend Vector<T> operator∗<>(const Matrix&, const Vector<T>&);
	// ...
};

friend가 템플릿 함수임을 명확히 하기 위해 friend 함수 이름 뒤에 <>가 필요하다. <>가 없으면 템플릿이 아닌 기능이 가정된다. 그런 다음 곱셈 연산자를 정의하여 Vector 및 Matrix의 데이터에 직접 액세스할 수 있다.

template<typename T>
Vector<T> operator∗(const Matrix<T>& m, const Vector<T>& v)
{
	Vector<T> r;
	// ... use m.v[i] and v.v[i] for direct access to elements ...
	return r;
}

Friend는 템플릿 클래스가 정의된 범위에 영향을 미치지 않으며 템플릿이 사용되는 범위에도 영향을 미치지 않는다. 대신 friend 함수 및 연산자는 인수 유형을 기반으로 한 조회를 사용하여 찾을 수 있다. 멤버 함수와 마찬가지로 friend 함수는 사용되는 경우에만 인스턴스화된다.

다른 클래스와 마찬가지로 클래스 템플릿은 다른 클래스를 friend로 지정할 수 있다.

class C;
using C2 = C;

template<typename T>
class My_class {
	friend C; // OK: C is a class
	friend C2; // OK: C2 is an alias for a class
	friend C3; // error : no class C3 in scope
	friend class C4; // OK: introduces a new class C4
};

당연히 흥미로운 경우는 friend가 템플릿 인수에 의존하는 경우다.

template<typename T>
class my_other_class {
	friend T; // my argument is my friend!
	friend My_class<T>; // My_class with the corresponding argument is my friend
	friend class T; // error : redundant ‘‘class’’
};

여느 때와 마찬가지로 friendship은 유전되지도 않고 전이되지도 않는다. 예를 들어 My_class<int>가 friend고 C가 My_class<int>의 friend임에도 불구하고 C는 My_other_class<int>의 friend가 되지 않는다.

템플릿을 클래스의 friend로 직접 만들 수 없지만 friend 선언을 템플릿으로 만둘 수는 있다.

template<typename T, typename A>
class List;

template<typename T>
class Link {
	template<typename U, typename A>
		friend class List;
	// ...
};

불행히도 Link<X>가 List<X>의 friend여야만 한다고 말할 수 있는 방법은 없다.

Friend 클래스는 밀접하게 관련된 개념의 작은 클러스터를 나타낼 수 있도록 설계되었다. 복잡한 friendship 패턴은 거의 확실히 디자인 오류다.

728x90
반응형

'Program Language > C & C++' 카테고리의 다른 글

[C++] Function Template  (0) 2022.02.17
[C++] Console 한 줄 Refresh  (0) 2022.02.16
[C++] Simple String 템플릿  (0) 2022.02.10
[C++] std::chrono로 시간 측정하기  (0) 2022.01.21
[C++] std::list 정렬하기  (0) 2022.01.21