표준 라이브러리 컨테이너와 템플릿을 사용해서 이런 프로그램을 짠다고 생각해보자.
using namespace std::string_literals;
int main() {
이 프로그램을 컴파일하면 최종적으로 std::cout << std::string이 불려서 "hello" 가 출력이 될 거야.
그런데 프로그램을 조금 바꿔서 이렇게 적어버리면 어떨까?
using namespace std::string_literals;
int main() {
이 프로그램을 컴파일하면 최종적으로 std::cout << std::set<int>를 부르려 하겠지만 그런 함수는 존재하지 않아. 당연히 컴파일 오류가 나겠지?
에러 메시지 (초스압 주의)
std::cout << obj << "\n";
~~~~~~~~~ ^ ~~~
main.cpp:25:3: note: in instantiation of function template specialization 'print<std::set<int>>' requested here
print(std::set<int>{1, 3, 5});
^
/usr/local/bin/../include/c++/v1/cstddef:116:3: note: candidate function template not viable: no known conversion from 'std::ostream' (aka 'basic_ostream<char>') to 'std::byte' for 1st argument
operator<< (byte __lhs, _Integer __shift) noexcept
^
/usr/local/bin/../include/c++/v1/ostream:793:1: note: candidate function template not viable: no known conversion from 'const std::set<int>' to 'char' for 2nd argument
operator<<(basic_ostream<_CharT, _Traits>& __os, char __cn)
^
/usr/local/bin/../include/c++/v1/ostream:826:1: note: candidate function template not viable: no known conversion from 'const std::set<int>' to 'char' for 2nd argument
operator<<(basic_ostream<char, _Traits>& __os, char __c)
^
/usr/local/bin/../include/c++/v1/ostream:833:1: note: candidate function template not viable: no known conversion from 'const std::set<int>' to 'signed char' for 2nd argument
operator<<(basic_ostream<char, _Traits>& __os, signed char __c)
^
/usr/local/bin/../include/c++/v1/ostream:840:1: note: candidate function template not viable: no known conversion from 'const std::set<int>' to 'unsigned char' for 2nd argument
operator<<(basic_ostream<char, _Traits>& __os, unsigned char __c)
^
/usr/local/bin/../include/c++/v1/ostream:854:1: note: candidate function template not viable: no known conversion from 'const std::set<int>' to 'const char *' for 2nd argument
operator<<(basic_ostream<_CharT, _Traits>& __os, const char* __strn)
^
/usr/local/bin/../include/c++/v1/ostream:900:1: note: candidate function template not viable: no known conversion from 'const std::set<int>' to 'const char *' for 2nd argument
operator<<(basic_ostream<char, _Traits>& __os, const char* __str)
^
/usr/local/bin/../include/c++/v1/ostream:907:1: note: candidate function template not viable: no known conversion from 'const std::set<int>' to 'const signed char *' for 2nd argument
operator<<(basic_ostream<char, _Traits>& __os, const signed char* __str)
^
/usr/local/bin/../include/c++/v1/ostream:915:1: note: candidate function template not viable: no known conversion from 'const std::set<int>' to 'const unsigned char *' for 2nd argument
operator<<(basic_ostream<char, _Traits>& __os, const unsigned char* __str)
^
/usr/local/bin/../include/c++/v1/ostream:1100:1: note: candidate function template not viable: no known conversion from 'const std::set<int>' to 'const std::error_code' for 2nd argument
operator<<(basic_ostream<_CharT, _Traits>& __os, const error_code& __ec)
^
/usr/local/bin/../include/c++/v1/ostream:1138:31: note: candidate function template not viable: no known conversion from 'const std::set<int>' to 'wchar_t' for 2nd argument
basic_ostream<char, _Traits>& operator<<(basic_ostream<char, _Traits>&, wchar_t) = delete;
^
/usr/local/bin/../include/c++/v1/ostream:1141:31: note: candidate function template not viable: no known conversion from 'const std::set<int>' to 'const wchar_t *' for 2nd argument
basic_ostream<char, _Traits>& operator<<(basic_ostream<char, _Traits>&, const wchar_t*) = delete;
^
/usr/local/bin/../include/c++/v1/ostream:1159:31: note: candidate function template not viable: no known conversion from 'const std::set<int>' to 'char8_t' for 2nd argument
basic_ostream<char, _Traits>& operator<<(basic_ostream<char, _Traits>&, char8_t) = delete;
^
/usr/local/bin/../include/c++/v1/ostream:1165:31: note: candidate function template not viable: no known conversion from 'const std::set<int>' to 'const char8_t *' for 2nd argument
basic_ostream<char, _Traits>& operator<<(basic_ostream<char, _Traits>&, const char8_t*) = delete;
^
/usr/local/bin/../include/c++/v1/ostream:1172:31: note: candidate function template not viable: no known conversion from 'const std::set<int>' to 'char16_t' for 2nd argument
basic_ostream<char, _Traits>& operator<<(basic_ostream<char, _Traits>&, char16_t) = delete;
^
/usr/local/bin/../include/c++/v1/ostream:1175:31: note: candidate function template not viable: no known conversion from 'const std::set<int>' to 'char32_t' for 2nd argument
basic_ostream<char, _Traits>& operator<<(basic_ostream<char, _Traits>&, char32_t) = delete;
^
/usr/local/bin/../include/c++/v1/ostream:1178:31: note: candidate function template not viable: no known conversion from 'const std::set<int>' to 'const char16_t *' for 2nd argument
basic_ostream<char, _Traits>& operator<<(basic_ostream<char, _Traits>&, const char16_t*) = delete;
^
/usr/local/bin/../include/c++/v1/ostream:1181:31: note: candidate function template not viable: no known conversion from 'const std::set<int>' to 'const char32_t *' for 2nd argument
basic_ostream<char, _Traits>& operator<<(basic_ostream<char, _Traits>&, const char32_t*) = delete;
^
/usr/local/bin/../include/c++/v1/ostream:786:1: note: candidate template ignored: deduced conflicting types for parameter '_CharT' ('char' vs. 'std::set<int>')
operator<<(basic_ostream<_CharT, _Traits>& __os, _CharT __c)
^
/usr/local/bin/../include/c++/v1/__random/uniform_int_distribution.h:259:1: note: candidate template ignored: could not match 'uniform_int_distribution' against 'set'
operator<<(basic_ostream<_CharT, _Traits>& __os,
^
/usr/local/bin/../include/c++/v1/ostream:847:1: note: candidate template ignored: could not match 'const _CharT *' against 'std::set<int>'
operator<<(basic_ostream<_CharT, _Traits>& __os, const _CharT* __str)
^
/usr/local/bin/../include/c++/v1/ostream:1083:1: note: candidate template ignored: could not match 'basic_string' against 'set'
operator<<(basic_ostream<_CharT, _Traits>& __os,
^
/usr/local/bin/../include/c++/v1/ostream:1091:1: note: candidate template ignored: could not match 'basic_string_view' against 'set'
operator<<(basic_ostream<_CharT, _Traits>& __os,
^
/usr/local/bin/../include/c++/v1/ostream:1108:1: note: candidate template ignored: could not match 'shared_ptr' against 'set'
operator<<(basic_ostream<_CharT, _Traits>& __os, shared_ptr<_Yp> const& __p)
^
/usr/local/bin/../include/c++/v1/ostream:1075:11: note: candidate template ignored: requirement 'integral_constant<bool, false>::value' was not satisfied [with _Stream = std::ostream &, _Tp = std::set<int>]
_Stream&& operator<<(_Stream&& __os, const _Tp& __x)
^
/usr/local/bin/../include/c++/v1/ostream:1120:1: note: candidate template ignored: could not match 'unique_ptr' against 'set'
operator<<(basic_ostream<_CharT, _Traits>& __os, unique_ptr<_Yp, _Dp> const& __p)
^
/usr/local/bin/../include/c++/v1/ostream:1127:1: note: candidate template ignored: could not match 'bitset' against 'set'
operator<<(basic_ostream<_CharT, _Traits>& __os, const bitset<_Size>& __x)
^
/usr/local/bin/../include/c++/v1/ostream:1144:34: note: candidate template ignored: could not match 'wchar_t' against 'char'
basic_ostream<wchar_t, _Traits>& operator<<(basic_ostream<wchar_t, _Traits>&, char16_t) = delete;
^
/usr/local/bin/../include/c++/v1/ostream:1147:34: note: candidate template ignored: could not match 'wchar_t' against 'char'
basic_ostream<wchar_t, _Traits>& operator<<(basic_ostream<wchar_t, _Traits>&, char32_t) = delete;
^
/usr/local/bin/../include/c++/v1/ostream:1150:34: note: candidate template ignored: could not match 'wchar_t' against 'char'
basic_ostream<wchar_t, _Traits>& operator<<(basic_ostream<wchar_t, _Traits>&, const char16_t*) = delete;
^
/usr/local/bin/../include/c++/v1/ostream:1153:34: note: candidate template ignored: could not match 'wchar_t' against 'char'
basic_ostream<wchar_t, _Traits>& operator<<(basic_ostream<wchar_t, _Traits>&, const char32_t*) = delete;
^
/usr/local/bin/../include/c++/v1/ostream:1162:34: note: candidate template ignored: could not match 'wchar_t' against 'char'
basic_ostream<wchar_t, _Traits>& operator<<(basic_ostream<wchar_t, _Traits>&, char8_t) = delete;
^
/usr/local/bin/../include/c++/v1/ostream:1168:34: note: candidate template ignored: could not match 'wchar_t' against 'char'
basic_ostream<wchar_t, _Traits>& operator<<(basic_ostream<wchar_t, _Traits>&, const char8_t*) = delete;
^
/usr/local/bin/../include/c++/v1/ostream:222:20: note: candidate function not viable: no known conversion from 'const std::set<int>' to 'std::ostream &(*)(std::ostream &)' for 1st argument
basic_ostream& operator<<(basic_ostream& (*__pf)(basic_ostream&))
^
/usr/local/bin/../include/c++/v1/ostream:226:20: note: candidate function not viable: no known conversion from 'const std::set<int>' to 'basic_ios<std::basic_ostream<char, std::char_traits<char>>::char_type, std::basic_ostream<char, std::char_traits<char>>::traits_type> &(*)(basic_ios<std::basic_ostream<char, std::char_traits<char>>::char_type, std::basic_ostream<char, std::char_traits<char>>::traits_type> &)' (aka 'basic_ios<char, std::char_traits<char>> &(*)(basic_ios<char, std::char_traits<char>> &)') for 1st argument
basic_ostream& operator<<(basic_ios<char_type, traits_type>&
^
/usr/local/bin/../include/c++/v1/ostream:231:20: note: candidate function not viable: no known conversion from 'const std::set<int>' to 'std::ios_base &(*)(std::ios_base &)' for 1st argument
basic_ostream& operator<<(ios_base& (*__pf)(ios_base&))
^
/usr/local/bin/../include/c++/v1/ostream:234:20: note: candidate function not viable: no known conversion from 'const std::set<int>' to 'bool' for 1st argument
basic_ostream& operator<<(bool __n);
^
/usr/local/bin/../include/c++/v1/ostream:235:20: note: candidate function not viable: no known conversion from 'const std::set<int>' to 'short' for 1st argument
basic_ostream& operator<<(short __n);
^
/usr/local/bin/../include/c++/v1/ostream:236:20: note: candidate function not viable: no known conversion from 'const std::set<int>' to 'unsigned short' for 1st argument
basic_ostream& operator<<(unsigned short __n);
^
/usr/local/bin/../include/c++/v1/ostream:237:20: note: candidate function not viable: no known conversion from 'const std::set<int>' to 'int' for 1st argument
basic_ostream& operator<<(int __n);
^
/usr/local/bin/../include/c++/v1/ostream:238:20: note: candidate function not viable: no known conversion from 'const std::set<int>' to 'unsigned int' for 1st argument
basic_ostream& operator<<(unsigned int __n);
^
/usr/local/bin/../include/c++/v1/ostream:239:20: note: candidate function not viable: no known conversion from 'const std::set<int>' to 'long' for 1st argument
basic_ostream& operator<<(long __n);
^
/usr/local/bin/../include/c++/v1/ostream:240:20: note: candidate function not viable: no known conversion from 'const std::set<int>' to 'unsigned long' for 1st argument
basic_ostream& operator<<(unsigned long __n);
^
/usr/local/bin/../include/c++/v1/ostream:241:20: note: candidate function not viable: no known conversion from 'const std::set<int>' to 'long long' for 1st argument
basic_ostream& operator<<(long long __n);
^
/usr/local/bin/../include/c++/v1/ostream:242:20: note: candidate function not viable: no known conversion from 'const std::set<int>' to 'unsigned long long' for 1st argument
basic_ostream& operator<<(unsigned long long __n);
^
/usr/local/bin/../include/c++/v1/ostream:243:20: note: candidate function not viable: no known conversion from 'const std::set<int>' to 'float' for 1st argument
basic_ostream& operator<<(float __f);
^
/usr/local/bin/../include/c++/v1/ostream:244:20: note: candidate function not viable: no known conversion from 'const std::set<int>' to 'double' for 1st argument
basic_ostream& operator<<(double __f);
^
/usr/local/bin/../include/c++/v1/ostream:245:20: note: candidate function not viable: no known conversion from 'const std::set<int>' to 'long double' for 1st argument
basic_ostream& operator<<(long double __f);
^
/usr/local/bin/../include/c++/v1/ostream:246:20: note: candidate function not viable: no known conversion from 'const std::set<int>' to 'const void *' for 1st argument; take the address of the argument with &
basic_ostream& operator<<(const void* __p);
^
/usr/local/bin/../include/c++/v1/ostream:255:20: note: candidate function not viable: no known conversion from 'const std::set<int>' to 'basic_streambuf<std::basic_ostream<char, std::char_traits<char>>::char_type, std::basic_ostream<char, std::char_traits<char>>::traits_type> *' (aka 'basic_streambuf<char, std::char_traits<char>> *') for 1st argument
basic_ostream& operator<<(basic_streambuf<char_type, traits_type>* __sb);
^
/usr/local/bin/../include/c++/v1/ostream:261:20: note: candidate function not viable: no known conversion from 'const std::set<int>' to 'nullptr_t' (aka 'std::nullptr_t') for 1st argument
basic_ostream& operator<<(nullptr_t)
^
1 error generated.
이따구로 말야...
"아니 그냥 std::cout << std::set<int> 가 없다고 하면 되는 거 아냐?" 라고 생각할 수 있을 텐데, 이게 그렇게 간단한 문제가 아냐. 왜냐면 C++에는 타입의 묵시적 (implicit) 변환이란 게 있기 때문에 타입이 일치하는 overload만 찾는 거로는 부족하거든.
에러 메시지를 조금 자세히 뜯어보면 내가 무슨 말을 했는지 조금 더 정확히 알 수 있을 거야.
std::cout << std::set<int> 이라는 코드를 어떻게든 컴파일하려고 operator<< 의 오만가지 overload를 뒤져보지만, 그 어떤 overload를 적용해봐도 암시적 변환만으로 왼쪽에 ostream이 오고 오른쪽에 std::set<int>가 오는 경우를 처리할 수 없었던 거지. 결국 "내가 진짜 이만큼 다 찾아봤는데 네 코드는 컴파일 가능한 overload가 없어" 라고 불평하는 거야.
여기서 더 복잡해지는 케이스는 이런 경우가 있는데
int main() {
이 프로그램은 컴파일하면 std::string을 원소로 갖는 std::vector의 std::inner_product를 연산하려다가 std::string 사이 곱셈이 정의돼 있지 않으니까 컴파일 오류가 나게 돼.
에러 메시지
In file included from main.cpp:3:
In file included from /usr/local/bin/../include/c++/v1/numeric:157:
/usr/local/bin/../include/c++/v1/__numeric/inner_product.h:29:50: error: invalid operands to binary expression ('const std::string' and 'const std::string')
__init = _VSTD::move(__init) + *__first1 * *__first2;
~~~~~~~~~ ^ ~~~~~~~~~
main.cpp:27:15: note: in instantiation of function template specialization 'std::inner_product<std::__wrap_iter<const std::string *>, std::__wrap_iter<const std::string *>, int>' requested here
return std::inner_product(v1.cbegin(), v1.cend(), v2.cbegin(), 0);
^
main.cpp:35:14: note: in instantiation of function template specialization 'inner<std::string>' requested here
auto ret = inner(v1, v2);
^
(이하 생략)
메시지를 보면 in instantiation of 이라는 표현이 자주 나오는데, 조금 더 복잡한 프로그램은 에러 하나에서 이 문장만 5~6번씩 반복되는 경우도 있어. 대충 해석해보면 "템플릿에 매개변수를 치환해보는 중에" 정도 뜻이 되려나....
사실 C++ 템플릿은 제대로 된 C++ 코드가 아니고, C++ 컴파일러는 템플릿 코드를 이해하지도 못해. C++의 템플릿은 단어의 뜻 그대로 틀, 또는 코드를 어떻게 생성할지 적어놓은 규칙에 불과해. 여기에 템플릿 인자를 주어서 instantiate해야 비로소 제대로 된 C++ 코드가 되고 컴파일러가 분석이든 컴파일이든 할 수 있게 되는 거야.
이 문장이 반복된다는 건 내부에 몇 겹으로 돼 있는 템플릿에 하나하나 타입을 집어넣어 보다가 한참 깊게 들어가서야 "아 이게 더이상 안 되네!" 하고 터진다는 거겠지? 문제는 그 과정을 굳이... 는 아니지만 우리에게 전부 다 보여주다 보니 정작 실제로 고쳐야 하는 부분이 어디인지 처음 봐선 감도 안 올 정도로 에러 메시지가 길어진다는 거야.
여기서 팁을 하나 주자면 in instantiation of~ 가 처음 나오기 바로 전 메시지가 우리 입장에서는 핵심이야. 프로그래머가 건드리는 부분은 대개 가장 바깥 쪽 템플릿 인자니까, 템플릿 내부 구현이 어딘가 잘못된 게 아닌 이상 여기를 올바르게 고쳐주면 대부분의 문제는 잘 해결될 거야.
하지만 C++ 프로그래머들 입장에서도 이런 에러 메시지는 엄청난 뇌절이라고 느껴졌기 때문에 C++20에서는 concept라는 새로운 문법이 추가됐어.
concept는 말하자면 템플릿 인자에 제약 조건을 붙여주는 기능이야. concept를 사용하면 복잡다단한 템플릿 인자 치환 오류를 간단명료한 이름으로 표현할 수 있기 때문에 프로그래머 입장에서 훨씬 편하게 템플릿 디버깅을 할 수 있게 돼.
using namespace std::string_literals;
template<typename T> requires Printable<T>
int main() {
위에서 적은 print() 예제에 적절한 concept를 하나 구현한 뒤에, print() 에는 해당 concept를 만족한 타입만 템플릿 인자로 올 수 있도록 수정했어.
main.cpp:31:3: error: no matching function for call to 'print'
print(std::set<int>{1, 3, 5});
^~~~~
main.cpp:17:3: note: candidate template ignored: constraints not satisfied [with T = std::set<int>]
T print(const T& obj) noexcept {
^
main.cpp:16:31: note: because 'std::set<int>' does not satisfy 'Printable'
template<typename T> requires Printable<T>
^
main.cpp:13:15: note: because 'std::cout << a' would be invalid: invalid operands to binary expression ('std::ostream' (aka 'basic_ostream<char>') and 'std::set<int>')
std::cout << a; // is cout defined?
^
1 error generated.
지금까지 봐온 에러 메시지에 비하면 눈물나게 간결하지?
심지어 정확히 어떤 제약 조건 (concept)에 걸렸는지까지 알려주니까 디버깅도 훨씬 편해져.
concept는 C++20에 와서야 추가된 기능이고, 메이저 컴파일러들은 (gcc, clang, MSVC 등) concept 자체는 꽤 빠르게 지원했지만 concept를 원활하게 쓰기 위한 concept 라이브러리는 지원하기 불과 작년~올해쯤에야 구현이 돼서 아직 이 기능을 쓸 수 있는 컴파일러가 많지는 않아. 아마 사용하는 OS에서 지원하는 거의 최신 컴파일러가 아닌 이상 이 기능을 본격적으로 활용하기는 꽤 어렵지 않을까 싶어.
그래도 concept를 쓰면 템플릿을 활용한 프로그래밍을 이전보다 훨씬 수월하게 할 수 있으니까, 관심 있는 챈럼이라면 한번 베타테스터 느낌으로 파보는 것도 좋을 거 같아!