C와 C++의 전통적인 방식에서는 모든 변수를 사용하기 전에 명시적으로 타입을 선언해야 했습니다. 이는 코드 작성 시 불필요한 반복 작업을 초래하고, 특히 복잡한 템플릿 코드나 반복자(iterator)를 다룰 때 생산성을 저하시켰습니다.
C++11은 이러한 문제를 해결하기 위해 auto와 decltype 키워드를 도입하여 컴파일러가 변수의 타입을 자동으로 추론하도록 만들었습니다. 이를 통해 C++는 현대 프로그래밍 언어처럼 변수 타입에 대한 부담을 줄일 수 있게 되었습니다.
1. auto 키워드의 타입 추론
auto는 변수의 초기값을 기반으로 타입을 추론합니다. 기본적인 사용 예시는 다음과 같습니다.
int original = 10;
auto deduced = original; // deduced는 int 타입
auto는 반복자(iterator)나 범위 기반 for 루프에서 특히 유용하게 사용됩니다.
#include <set>
#include <vector>
std::set<int> mySet;
// ...
for (auto iter = mySet.begin(); iter != mySet.end(); ++iter) {
// iter는 std::set<int>::iterator 타입
}
std::vector<int> myVec;
// ...
for (auto value : myVec) {
// value는 int 타입
}
auto의 타입 추론 규칙은 함수 템플릿의 타입 추론과 거의 동일합니다. 단, 중괄호 초기화 리스트(brace-enclosed initializer list)는 예외입니다.
auto initList = {1, 2, 3}; // initList는 std::initializer_list<int> 타입
template<typename T>
void process(T param) {
// ...
}
process({1, 2, 3}); // 컴파일 오류: T 타입을 추론할 수 없음
auto 사용 시 주의해야 할 주요 규칙은 다음과 같습니다.
auto가 참조 타입(예:auto&또는auto&&)이 아니라면, 추론 과정에서 객체의 참조 속성과 객체 자체의const또는volatile한정자는 무시됩니다.
예를 들어 살펴보겠습니다.
int plainInt = 5;
const int constInt = 5;
int& refInt = plainInt;
const char* const constPtr = "example";
auto var1 = plainInt; // var1 타입: int
auto var2 = constInt; // var2 타입: int
auto var3 = refInt; // var3 타입: int
auto var4 = constPtr; // var4 타입: const char*
위 코드를 Visual Studio 2022에서 실행한 결과는 주석과 같습니다. var4의 경우, constPtr은 const char를 가리키는 포인터 상수입니다. 포인터 자체의 const 속성(두 번째 const)은 타입 추론 시 무시되므로, var4의 타입은 const char*가 됩니다. auto&와 auto&&에 대한 자세한 내용은 보편 참조(universal reference) 관련 문서를 참고하시기 바랍니다.
2. decltype 키워드의 타입 추론
decltype은 표현식(expression)의 타입을 그대로 추론합니다.
auto x = 10; // x는 int
auto y = 20; // y는 int
decltype(x + y) z; // z는 int 타입
if (std::is_same<decltype(x), int>::value) {
std::cout << "x의 타입은 int입니다." << std::endl;
}
3. 후행 반환 타입(trailing return type) 추론
C++11에서는 함수의 반환 타입을 후행 반환 타입 문법을 통해 추론할 수 있습니다.
template<typename T, typename U>
auto addValues(T a, U b) -> decltype(a + b) {
return a + b;
}
이를 응용하면, std::future와 함께 비동기 작업의 결과 타입을 유연하게 추론할 수 있습니다. 이는 스레드 풀(thread pool) 구현에서 자주 사용됩니다.
#include <future>
#include <functional>
template <typename Func, typename... Args>
auto enqueueTask(Func&& f, Args&&... args)
-> std::future<decltype(std::forward<Func>(f)(std::forward<Args>(args)...))>
{
using ResultType = decltype(f(args...));
auto taskPtr = std::make_shared<std::packaged_task<ResultType()>>(
std::bind(std::forward<Func>(f), std::forward<Args>(args)...)
);
auto taskFuture = taskPtr->get_future();
auto wrapper = [taskPtr]() { (*taskPtr)(); };
scheduleTask(std::move(wrapper));
return taskFuture;
}
위 함수는 std::future 객체(약속 객체)를 반환합니다. 반환된 객체에 get() 메서드를 호출하면 함수 f의 실행 결과를 얻을 수 있으며, 이 결과의 타입은 decltype(std::forward<Func>(f)(std::forward<Args>(args)...))에 의해 결정됩니다.
C++14부터는 함수 반환 타입에 auto를 직접 사용할 수 있어, 후행 반환 타입 문법이 불필요해졌습니다.