Generic Programming을 한창하다보면 특수한 몇가지 경우만 코드를 따로 생성하고 싶은 경우가 있다
혹은 Code는 달라도 하는일은 동일해서 이름을 통일하고 싶은 경우가 있다
이런 난감한 경우를 위해 C++은 template Specialization이란걸 제공한다
표현이 좀 긴데 그냥 명시적으로 특수화하겠다는 뜻이다
즉, template argument들의 값을 모두 명시적으로 타나낸 경우를 의미한다
https://en.cppreference.com/w/cpp/language/template_specialization
다음 코드를 살펴보자
template <typename T>
class foo {
//...
};
template <>
class foo<int> {
//...
};
foo
class는 template parameter를 가지고 있다
그런데 int
type에 대해서만 특수화를 하고 싶은 경우 위와 같이 template parameter를 비우고, Specialization하는 곳에 명시적으로 달아놓으면 된다
Function template이나, member function template 등등 모두 비슷하게 적용된다
다만 class에도 template이 달려있고, member function에도 template이 달려있다면 member function template만 Specialization하는 것은 불가능하다
template <typename T>
void foo_func() {
//...
}
template <>
void foo_func<char>() {
//...
}
template <typename T>
struct foo {
template <typename TT>
void foo_mem_fn(TT);
};
template <typename T>
template <typename TT>
void foo<T>::foo_mem_fn(TT) {
//...
}
// template <typename T> ERROR!
// template <>
// void foo<T>::foo_mem_fn<int>(int) {
// //...
// }
template <>
template <>
void foo<int>::foo_mem_fn<int>(int) {
//...
}
template <>
struct foo<double> {
//...
};
여기서 주의할점은 template <>
을 적어줘야 한다는 점이다
이걸 안적으면 Compiler는 template어 없다고 생각하고 Error을 출력한다
이렇게만 설명하고 넘어가기엔 설명이 너무 없어보이니 예시를 하나 생각해냈다
아래와 같은 add
함수가 있다고 해보자
template <typename T>
T add(const T& _1, const T& _2) {
return _1 + _2;
}
여기서 +
연산자를 사용할 수 있다면 상관이 없다
그런데 만약 그렇지 못한다면 어떻게 될까
#include <iostream>
template <typename T>
T add(const T& _1, const T& _2) {
return _1 + _2;
}
int main() {
const char* _1 = "Hello ";
const char* _2 = "World!";
std::cout << add<const char*>(_1, _2);
}
// Result
<source>:6:15: error: invalid operands to binary expression ('const char *const' and 'const char *const')
return _1 + _2;
~~ ^ ~~
<source>:13:18: note: in instantiation of function template specialization 'add<const char *>' requested here
std::cout << add<const char*>(_1, _2);
^
1 error generated.
빨간줄을 맞이한다..PTSD
만약 Type이 내가 작성한 Class라면 + operator overloading
을 통해 해결할수도 있다
하지만 이걸 Template Specialization으로 해결할수도 있다
#include <iostream>
#include <string.h>
template <typename T>
T add(const T& _1, const T& _2) {
return _1 + _2;
}
template <>
const char* add<const char*>(const char* const& _1,
const char* const& _2) {
std::size_t i = 0, len_1 = strlen(_1);
std::size_t total_len = len_1 + strlen(_2);
char* str = new char[total_len];
for (; i < len_1; i++) str[i] = _1[i];
for (; i < total_len; i++) str[i] = _2[i - len_1];
return str;
}
int main() {
const char* _1 = "Hello ";
const char* _2 = "World!\n";
const char* result = add<const char*>(_1, _2);
std::cout << result;
delete result;
}
당장에 생각나는게 string literal 밖에 없어서 이렇게 만들어봤다
뭐 이렇게 사용하란 뜻은 아니고, 이렇게도 표현이 가능하구나 정도로 보면된다
사람이라는게 어떻게 모든걸 다 명확하게 특수화할 수 있는가
때론 부분적으로만 특수화하고 싶거나 해야할때가 있다
이럴때 필요한게 Partial Template Specialization이다
https://en.cppreference.com/w/cpp/language/partial_specialization
이름이 상당히 거창한데, 사실 그냥 Specialization이다
위랑 차이가 있다면 template argument를 몇개 남겨놨다 뿐이다
template <typename _1, typename _2, typename _3>
struct foo {
//...
};
template <typename _1>
struct foo<_1, int, int> {
//...
};
이걸보고 나면 왜 template <>
을 명시해주어야하는지 조금은 이해가간다
이제 그런 생각을 들수 있다
과연 저기서 template parameter의 순서는 어떻게 되는 것인가
template parameter의 순서는 primary template
을 따른다
여기서 primary template
은 가장 처음에 명시했던 class를 가리킨다
즉 _1, _2, _3
의 순서로 들어간다는 뜻이다
따라서 Partial Specialization된 class의 template parameter 순서는 중요하지 않다
#include <iostream>
template <typename _1, typename _2, typename _3>
struct foo {
//...
};
template <typename a, typename b>
struct foo<b, int, a> {
void print() {
std::cout << typeid(a).name() << "\n"
<< typeid(b).name() << "\n";
}
};
int main() {
foo<double, int, float> ele;
ele.print();
}
// Result: f d
왜 중요하지 않은가는 Instantiation을 보면 알 수 있다
위 예시에서 a
에 float
가 들어갔고, b
에 double
이 들어갔다
즉, 위에 template으로 선언되어있는 부분은 "내가 이런 Template Parameter를 쓸거야"하는거다
애초에 CppReference에서 Name Lookup과 관련해서 다음과 같이 설명한다
Partial template specializations는 name lookup시 검색되지 않는다. Primary template만 name lookup시 검색된다
CppReference
사실 생각해보면 당연한것이, template spcialization은 단어 그대로 특수화에 대한 것이지 primary template과 다르면 안된다
하여튼 이런 template spcialization에서 한발자국 더 뻗으면 이런것도 가능하다
template <typename T>
struct foo {};
template <typename _1, typename _2, typename _3>
struct foo<_1(_2, _3)> {
//...
};
template <typename _1, typename _2>
struct foo<void(_1, _2)> {
//...
};
std::function
에서 영감을 얻어 작성해본 예시이다
물론 std::function
은 훨씬 복잡하고 무엇보다 이렇게 생기지 않았다
이제 여기에 STL을 조금 섞어주면 조금은 실용적인 코드를 만들 수 있다
CS에서 국밥마냥 등장하는 행렬도 아래와 같이 만들 수 있다
물론 아주 간단한 구현으로 실제로는 저렇게 해놓으면 써먹기가 애매하다
#include <iostream>
#include <array>
template <typename T, int Row, int Col>
using Matrix = std::array<std::array<T, Col>, Row>;
template <typename T> using Matrix3 = Matrix<T, 3, 3>;
template <typename T> using Matrix4 = Matrix<T, 4, 4>;
using Matrix3f = Matrix3<float>;
//...
int main() {
Matrix3f mat {{
{{ 1, 0, 0}},
{{ 0, 1, 0}},
{{ 0, 0, 1}}
}};
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++)
std::cout << mat[i][j];
std::cout << std::endl;
}
}
여기서 한가지 궁금증이 생긴다
function template은 partial specialization이 불가능한 것일까
template <typename _1, typename _2>
void foo(_1 a, _2 b) {
//...
}
template <typename _1>
void foo<int, _1>(int a, _1 b) {
//...
}
// Output
<source>:26:6: error: function template partial specialization is not allowed
void foo<int, _1>() {
^ ~~~~~~~~~
1 error generated.
아쉽지만 그렇다
근데 사실 잘 생각해보면 필요가 없다
왜냐하면 function overloading이 있기 때문이다
template <typename _1, typename _2>
void foo(_1 a, _2 b) {
//...
}
template <typename _1>
void foo(int a, _1 b) {
//...
}
auto main() -> int {
foo<double, double>(1., 1.);
foo<double>(1, 1.);
}
위 예시를 본다면 이런생각이 들 수 있다
"저건 template parameter 개수가 다르잖아"
맞다, 근데 틀리다
아직은 뒤로 미루고 있는 template deduction을 적용하면 이야기 달라진다
template <typename _1, typename _2>
void foo(_1 a, _2 b) {
//...
}
template <typename _1>
void foo(int a, _1 b) {
//...
}
auto main() -> int {
foo(1, 1.);
foo(1., 1.);
}
code를 쓰는 입장에서는 저게 어떠한 function을 invoke할지 관심이 없어도 된다
다시 한번말하지만 "없어도 된다"이다
하는일이 달라진다면 알아야한다
만약 그대로 template specialization을 적용하고 싶을 수 있다
구글링을 하다보니 스오플에서 재미난 방법을 제시했다
이 방법을 조금만 번형하면 아래와 같이 만들 수 있다
https://stackoverflow.com/questions/5101516/why-function-template-cannot-be-partially-specialized
#include <iostream>
struct foo_impl {
private:
template <typename _1, typename _2>
struct __foo_impl {
void operator()(_1 a, _2 b) {
std::cout << "_1, _2" << std::endl;
}
};
template <typename _1>
struct __foo_impl<int, _1> {
void operator()(int a, _1 b) {
std::cout << "int, _2" << std::endl;
}
};
public:
template <typename _1, typename _2>
void operator()(_1 a, _2 b) {
__foo_impl<_1, _2>()(a, b);
}
};
template <typename _1, typename _2>
void foo(_1 a, _2 b) {
foo_impl().operator()<_1, _2> (a, b);
}
int main() {
foo<double, float>(1., 2.f);
foo<int, float>(1, 2.f);
}
이게 뭔 조악한 code냐고 할 수 있는데 그냥 이렇게 해보고 싶었다
뭐 이걸 조금만 번형하면 type-erasure이 되는것도 있고, 캡슐화를 하고 싶어서 그런것도 있다
하여튼 이런식으로 한다면 function template partial specialization이 되는 것처럼..? 보인다
아 혹시라도 위 코드가 불필요한 overhead가 발생하는건 아닌지 궁금해할 수 있다
function에서 function에서 function을 호출하고 있기 때문이다
아 그리고 instance가 두개 생겨나는건 덤이고
Clang 16.0.0 --std=c++2b -O3 -march=skylake
main: # @main
push r14
push rbx
push rax
mov rbx, qword ptr [rip + std::cout@GOTPCREL]
lea rsi, [rip + .L.str]
mov edx, 6
mov rdi, rbx
call std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert
...
무자비한 Clang한테는 그딴거 없다
그냥 바로 std::cout
을 호출한다
애초에 저렇게 생겨먹은걸 cpp에서 function object라 한다
그리고 cpp에서 Lambda가 대표적인 function object인데, 뭐 당연하지만 최적화 잘해준다
GCC의 경우에는 더 잔인하게 compile 해버린다
GCC 13.1 --std=c++2b -O3 -march=skylake
.LC0:
.string "_1, _2"
.LC1:
.string "int, _2"
main:
sub rsp, 8
mov edx, 6
mov esi, OFFSET FLAT:.LC0
mov edi, OFFSET FLAT:_ZSt4cout
call std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert
mov edi, OFFSET FLAT:_ZSt4cout
call std::basic_ostream<char, std::char_traits<char> >& std::endl
mov edx, 7
mov esi, OFFSET FLAT:.LC1
mov edi, OFFSET FLAT:_ZSt4cout
call std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert
mov edi, OFFSET FLAT:_ZSt4cout
call std::basic_ostream<char, std::char_traits<char> >& std::endl
xor eax, eax
add rsp, 8
ret
Clang에서는 그래도 jump를 썼는데 이놈은 그냥 모든걸 21개의 assembly line으로 끝내버린다
이렇듯 해당부분은 Compiler가 잘 이해하여 optimization 해주는 부분인만큼 걱정할 필요가 없다
사실 개념자체가 그렇게 어렵지는 않다
따라서 굳이 이 부분을 열심히 팔 이유는 없다고 본다
사실 그냥 몇번 써보다 보면 손에 익는다
우리가 고민해야하는 부분은 어떻게 설계를 하는가이다
위 add
의 예시를 한번 생각해보자
과연 저기서 string literal
에 대한 add 함수를 만들어야 했을까
누군가는 그렇다고 할 것이고, 누군가는 concat
으로 함수 이름을 바꿔야 한다고 할 것이다
또 다른 누군가는 다른 방법을 제시할 것이다
이 부분은 개인의 판단이지 옳고그름이 아니다
우리가 고민해야하는 부분은 이런부분이다