본문 바로가기

auto 키워드와 타입 추론

초기화 식에서 타입을 추론하는 auto의 decay·const·참조 규칙을 익히고 명시적 타입과의 가독성 균형을 판단합니다.

C++11부터 C++20까지 언어에는 코드를 더 짧고 안전하게 쓰게 하는 기능이 많이 더해졌으며, 이런 기능을 흔히 현대 C++(modern C++)이라고 부릅니다. 이 장에서는 그 가운데 자주 쓰는 auto, 람다, 스마트 포인터, 이동 의미론, C++20의 주요 기능을 차례로 다룹니다.

이 글은 컴파일러가 초기화 식으로 변수의 타입을 정하게 하는 auto를 다룹니다. std::vector<std::pair<std::string, int>>::iterator 같은 긴 타입을 직접 적지 않아도 되지만, auto가 const와 참조를 어떻게 다루는지 알아야 의도하지 않은 복사를 피할 수 있습니다. 식의 타입을 그대로 얻는 decltype도 함께 봅니다.

auto의 기본 사용법

auto로 변수를 선언하면 컴파일러가 초기화 식의 타입으로 변수의 타입을 정합니다. 타입을 정할 초기화 식이 필요하므로 auto 변수는 반드시 초기화해야 하며, auto x;는 컴파일 오류입니다.

auto_basic.cpp
#include <map>
#include <string>
#include <vector>

int main() {
    auto i = 10;                     // int
    auto d = 3.14;                   // double
    auto f = 1.0f;                   // float
    auto big = 1LL;                  // long long
    auto s = "Hello";                // const char*
    auto name = std::string("Alice"); // std::string

    std::vector<int> numbers{1, 2, 3};
    auto it = numbers.begin();       // std::vector<int>::iterator

    std::map<std::string, int> scores{{"Alice", 90}};
    for (const auto& [key, score] : scores) { // C++17 구조적 바인딩
        (void)key;
        (void)score;
    }
}

auto의 타입 추론은 컴파일할 때 끝나므로 실행 비용이 없습니다. 반복자처럼 타입 이름이 길고, 초기화 식만 보아도 무엇인지 분명한 곳에 특히 잘 맞습니다. for (const auto& [key, score] : scores)는 C++17의 구조적 바인딩(structured binding)으로, 맵의 원소인 키와 값의 쌍을 두 이름으로 나누어 받습니다.

리터럴의 타입은 접미사로 정해지므로, auto로 받을 때는 1은 int, 1.0은 double, 1.0f는 float라는 점을 기억해야 합니다. 타입이 짧고 그 자체로 의미를 드러내는 경우에는 int count = 0;처럼 타입을 직접 적는 편이 읽기 쉬울 수 있습니다.

auto가 const와 참조를 다루는 방식

auto로 값을 선언하면, 초기화 식에서 참조와 맨 바깥의 const는 빠지고 배열은 포인터로 바뀝니다. 그래서 const int나 int&로 초기화해도 auto 변수는 독립된 int 사본이 됩니다. 참조나 const를 유지하려면 auto&나 const auto&로 선언합니다.

auto_state.cpp
#include <initializer_list>
#include <iostream>
#include <type_traits>

int main() {
    int source = 10;
    auto copied = source;
    auto& alias = source;

    const int fixed = 30;
    auto& readOnly = fixed;
    const int* const originalPointer = &fixed;
    auto pointerCopy = originalPointer;

    int arr[] = {1, 2, 3};
    auto pointer = arr;
    auto& wholeArray = arr;

    auto one{1};
    auto list = {1};

    static_assert(std::is_same_v<decltype(copied), int>);
    static_assert(std::is_same_v<decltype(alias), int&>);
    static_assert(std::is_same_v<decltype(readOnly), const int&>);
    static_assert(std::is_same_v<decltype(pointerCopy), const int*>);
    static_assert(std::is_same_v<decltype(pointer), int*>);
    static_assert(std::is_same_v<decltype(wholeArray), int (&)[3]>);
    static_assert(std::is_same_v<decltype(one), int>);
    static_assert(std::is_same_v<decltype(list), std::initializer_list<int>>);

    copied = 99;
    alias = 20;
    pointer[1] = 7;
    std::cout << source << ' ' << copied << ' ' << wholeArray[1] << '\n';
}
20 99 7

static_assert는 선언된 타입이 예상과 다르면 컴파일을 멈추므로, 이 프로그램이 컴파일된다는 것은 주석 대신 타입이 확인되었다는 뜻입니다. 아래 그림처럼 copied는 source를 복사한 별도의 int여서 99를 넣어도 source는 그대로이고, alias는 source의 참조여서 20을 넣으면 source가 20이 됩니다. pointer는 배열의 사본이 아니라 첫 원소를 가리키는 포인터이므로, pointer[1]을 바꾸면 wholeArray[1]도 7이 됩니다.

auto는 initializer와 declarator를 함께 읽어 타입을 추론한다

그림이 source=20/copied=99인 최종 상태인데 본문 정수 실험은 copied와 alias 변경을 순서대로 고정한다. 선언→복사본 변경→원본 변경으로 차이를 드러낼 수 있다.

C++ · TYPE DEDUCTION

auto는 초기화 식과 선언 형태를 함께 읽는다

같은 원본도 값·참조·중괄호 초기화에 따라 다른 타입을 만든다. 각 예제의 선언 타입과 원본 수명을 함께 확인한다.

복사본과 같은 원본을 보는 참조source를 복사한 copied는 별도 정수이고 alias는 같은 source를 참조한다. source 최종20, copied 최종99. 배열 포인터 추론과 별도로 정수 사례를 보여 준다.복사참조source최종 값 20copied: int별도 정수 · 최종 99alias: int&source의 다른 이름
복사본과 같은 원본을 보는 참조source를 복사한 copied는 별도 정수이고 alias는 같은 source를 참조한다. source 최종20, copied 최종99. 배열 포인터 추론과 별도로 정수 사례를 보여 준다.복사참조source최종 값 20copied: int별도 정수 · 99alias: int&source의 다른 이름

그림은 본문 정수 실험의 최종 상태이다. copied는 별도 정수, alias는 같은 source를 본다. 배열에서 추론한 포인터는 배열을 복제하지 않는다.

값 선언 · auto

값으로 선언하면 최상위 const와 참조를 유지하지 않는다.

const int cx = 100;
auto a = cx; // int

포인터가 가리키는 대상의 const까지 없애는 규칙은 아니다.

참조 선언 · auto& / const auto&

원본을 참조하며 원본의 const 또는 선언한 const를 유지한다.

auto& r = cx; // const int&
int x = 10;
const auto& cr = x;

참조를 사용하는 동안 대상 수명이 살아 있어야 한다.

배열 · 값과 참조의 차이

값 선언은 배열이 포인터로 변환되고, 참조 선언은 배열 타입을 유지한다.

int arr[] = {1, 2, 3};
auto p = arr; // int*
auto& a = arr; // int (&)[3]

함수도 값 선언에서 함수 포인터로 변환될 수 있다.

copy-list · = { ... }

중괄호 원소에서 같은 원소 타입을 추론해야 한다.

auto list = {1, 2, 3};
// initializer_list<int>

{1, 2.0}처럼 추론한 원소 타입이 다르면 실패한다.

direct-list · {one}

현대 규칙에서 단일 원소 타입을 추론한다. 여러 원소는 허용하지 않는다.

auto n{1}; // int
// auto bad{1, 2}; // 오류

copy-list의 = 유무가 추론 결과를 바꾼다.

값 범주 · auto&&

일반 표현식 초기화에서는 lvalue를 참조 축약하고 rvalue에 rvalue 참조를 만든다.

auto&& l = x; // int&
auto&& r = 100; // int&&

r처럼 이름 있는 참조 변수 표현식 자체는 lvalue이다.

typeid().name()은 최상위 const·참조 차이를 구별하지 못한다. decltype과 타입 특성으로 정확한 선언 타입을 확인한다.

이 예의 조건

  • 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
  • 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
원 도표의 제목·노드·관계·조건·주석
범위원문
관계·조건 설명source를 복사한 copied는 별도 정수이고 alias는 같은 source를 참조한다. source 최종 20, copied 최종 99. 배열 포인터 추론과 별도로 정수 사례를 보여 준다.
본문·주석참조를 사용하는 동안 대상이 살아 있어야 한다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
값·참조 준비

copied는 사본이며 alias는 source의 별명이다.

  • copied: source의 독립 값
  • alias: source와 같은 객체
  • auto 값 선언과 auto& 선언은 서로 다른 관계를 만든다.
사본 변경

copied를 99로 바꾼다.

  • copied: 99
  • source: 사본 변경으로 영향 없음
  • 별도 객체에 대입하므로 원래 source를 변경하지 않는다.
참조 변경

alias를 통해 source를 20으로 바꾼다.

  • source: 20
  • copied: 99
  • 참조 대입은 같은 원본 객체를 바꾸며 독립 사본은 그대로다.

auto는 initializer와 declarator를 함께 읽어 타입을 추론한다

그림이 source=20/copied=99인 최종 상태인데 본문 정수 실험은 copied와 alias 변경을 순서대로 고정한다. 선언→복사본 변경→원본 변경으로 차이를 드러낼 수 있다.

C++ · TYPE DEDUCTION

auto는 초기화 식과 선언 형태를 함께 읽는다

같은 원본도 값·참조·중괄호 초기화에 따라 다른 타입을 만든다. 각 예제의 선언 타입과 원본 수명을 함께 확인한다.

복사본과 같은 원본을 보는 참조source를 복사한 copied는 별도 정수이고 alias는 같은 source를 참조한다. source 최종20, copied 최종99. 배열 포인터 추론과 별도로 정수 사례를 보여 준다.복사참조source최종 값 20copied: int별도 정수 · 최종 99alias: int&source의 다른 이름
복사본과 같은 원본을 보는 참조source를 복사한 copied는 별도 정수이고 alias는 같은 source를 참조한다. source 최종20, copied 최종99. 배열 포인터 추론과 별도로 정수 사례를 보여 준다.복사참조source최종 값 20copied: int별도 정수 · 99alias: int&source의 다른 이름

그림은 본문 정수 실험의 최종 상태이다. copied는 별도 정수, alias는 같은 source를 본다. 배열에서 추론한 포인터는 배열을 복제하지 않는다.

값 선언 · auto

값으로 선언하면 최상위 const와 참조를 유지하지 않는다.

const int cx = 100;
auto a = cx; // int

포인터가 가리키는 대상의 const까지 없애는 규칙은 아니다.

참조 선언 · auto& / const auto&

원본을 참조하며 원본의 const 또는 선언한 const를 유지한다.

auto& r = cx; // const int&
int x = 10;
const auto& cr = x;

참조를 사용하는 동안 대상 수명이 살아 있어야 한다.

배열 · 값과 참조의 차이

값 선언은 배열이 포인터로 변환되고, 참조 선언은 배열 타입을 유지한다.

int arr[] = {1, 2, 3};
auto p = arr; // int*
auto& a = arr; // int (&)[3]

함수도 값 선언에서 함수 포인터로 변환될 수 있다.

copy-list · = { ... }

중괄호 원소에서 같은 원소 타입을 추론해야 한다.

auto list = {1, 2, 3};
// initializer_list<int>

{1, 2.0}처럼 추론한 원소 타입이 다르면 실패한다.

direct-list · {one}

현대 규칙에서 단일 원소 타입을 추론한다. 여러 원소는 허용하지 않는다.

auto n{1}; // int
// auto bad{1, 2}; // 오류

copy-list의 = 유무가 추론 결과를 바꾼다.

값 범주 · auto&&

일반 표현식 초기화에서는 lvalue를 참조 축약하고 rvalue에 rvalue 참조를 만든다.

auto&& l = x; // int&
auto&& r = 100; // int&&

r처럼 이름 있는 참조 변수 표현식 자체는 lvalue이다.

typeid().name()은 최상위 const·참조 차이를 구별하지 못한다. decltype과 타입 특성으로 정확한 선언 타입을 확인한다.

이 예의 조건

  • 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
  • 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.

관계도의 보충 설명

원 도표의 제목·노드·관계·조건·주석
범위원문
관계·조건 설명source를 복사한 copied는 별도 정수이고 alias는 같은 source를 참조한다. source 최종 20, copied 최종 99. 배열 포인터 추론과 별도로 정수 사례를 보여 준다.
본문·주석참조를 사용하는 동안 대상이 살아 있어야 한다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
값·참조 준비

copied는 사본이며 alias는 source의 별명이다.

  • copied: source의 독립 값
  • alias: source와 같은 객체
  • auto 값 선언과 auto& 선언은 서로 다른 관계를 만든다.
사본 변경

copied를 99로 바꾼다.

  • copied: 99
  • source: 사본 변경으로 영향 없음
  • 별도 객체에 대입하므로 원래 source를 변경하지 않는다.
참조 변경

alias를 통해 source를 20으로 바꾼다.

  • source: 20
  • copied: 99
  • 참조 대입은 같은 원본 객체를 바꾸며 독립 사본은 그대로다.

auto&는 원본이 const이면 그 const를 유지하므로 readOnly는 const int&가 됩니다. pointerCopy는 포인터 자체의 맨 바깥 const가 빠졌지만, const 포인터와 포인터 상수에서 본 가리키는 대상의 const는 남아 const int*입니다. 범위 기반 for에서 for (auto s : names)처럼 쓰면 원소마다 사본이 만들어지므로, 읽기만 할 때는 const auto&, 바꿀 때는 auto&를 씁니다.

중괄호 초기화와 auto&&

중괄호로 초기화하면 형태에 따라 타입이 달라집니다. auto one{1};은 int이지만, 등호를 쓴 auto list = {1};은 std::initializer_list<int>가 되고, auto bad{1, 2};처럼 등호 없이 값을 여러 개 넣으면 컴파일 오류입니다. 의도한 타입이 분명하지 않을 때는 중괄호를 auto와 함께 쓰지 않는 편이 안전합니다.

auto&&는 초기화 식이 이름 있는 변수처럼 주소를 가진 값(lvalue)이면 int&가 되고, 100 같은 임시 값(rvalue)이면 int&&가 됩니다. 이렇게 두 경우를 모두 받는 참조를 전달 참조(forwarding reference)라고 하며, move 의미론과 rvalue 참조에서 자세히 다룹니다.

더 깊이: auto의 추론 규칙과 중괄호에 얽힌 역사

auto의 타입 추론은 함수 템플릿의 인수 추론과 같은 규칙을 씁니다. auto x = expr;는 template <typename T> void f(T)에 expr를 넘겼을 때 T가 추론되는 방식과 같아서, 값으로 받는 매개변수처럼 참조와 맨 바깥의 const가 빠지고 배열은 포인터가 됩니다. 두 규칙이 갈리는 곳은 중괄호입니다. 템플릿 매개변수가 std::initializer_list가 아니면 {1, 2} 같은 인자로는 T를 추론하지 못하지만, auto x = {1, 2};는 auto 자리를 std::initializer_list<U>로 보고 추론하므로 std::initializer_list<int>가 됩니다.

등호 없는 중괄호에 대한 규칙은 C++11 이후 한 번 바뀌었습니다. 처음 규칙에서는 auto x{1};도 std::initializer_list<int>가 되어, 중괄호로 초기화한 auto 변수가 정수가 아닌 목록이 되는 혼란이 있었습니다. N3922는 등호 없는 중괄호에 원소가 하나면 그 원소의 타입으로 추론하고 여럿이면 오류로 하도록 바꾸었으며, 위원회는 이를 C++14의 결함으로 보고 고치라는 방향을 정했습니다. 그래서 오래된 자료나 컴파일러에서는 auto x{1};의 타입이 지금과 다르게 설명되어 있을 수 있습니다.

decltype과 반환 타입 추론

decltype(식)은 식의 타입을 그대로 돌려줍니다. auto와 달리 참조와 const를 빼지 않으므로, 위 예제의 static_assert처럼 선언된 타입을 정확히 확인할 때 씁니다. decltype(x)처럼 이름만 넣으면 그 변수를 선언한 타입이 되고, decltype((x))처럼 괄호를 한 번 더 두르면 식으로 취급되어 int&가 된다는 차이도 있습니다.

C++14부터는 함수의 반환 타입도 auto로 적어, return 식의 타입으로 정하게 할 수 있습니다.

auto_return.cpp
#include <iostream>
#include <string>
#include <utility>
#include <vector>

auto makeRecord() {
    return std::make_pair(std::string("key"), std::vector<double>{1.1, 2.2});
}

int main() {
    auto record = makeRecord();
    std::cout << record.first << ' ' << record.second[0] << '\n';
}
key 1.1

makeRecord의 반환 타입은 std::pair<std::string, std::vector<double>>로 정해집니다. 반환 타입을 auto로 두면 return 식이 값이 되도록 참조와 const가 빠지므로, 참조를 그대로 돌려주려면 decltype(auto)를 씁니다. 다만 함수의 선언만 보고는 반환 타입을 알 수 없으므로, 헤더에 공개하는 함수에는 반환 타입을 적어 두는 편이 읽는 사람에게 친절합니다.

정리

  • auto는 초기화 식으로 변수의 타입을 정하며, 반드시 초기화해야 하고 실행 비용은 없습니다.
  • 값으로 선언한 auto는 참조와 맨 바깥 const를 빼고 배열을 포인터로 바꾸므로, 독립된 사본이 됩니다.
  • 참조나 const를 유지하려면 auto&나 const auto&를 쓰며, 범위 기반 for에서도 같습니다.
  • auto x{1}은 int, auto x = {1}은 std::initializer_list<int>이며, auto&&는 전달 참조입니다.
  • decltype은 식의 타입을 참조와 const까지 그대로 돌려주며, C++14부터 반환 타입에도 auto를 쓸 수 있습니다.
  • 타입이 길고 초기화 식으로 분명한 곳에 auto를 쓰고, 짧은 타입은 직접 적는 편이 읽기 쉬울 수 있습니다.

연습 문제

1. 선언 형태와 바뀌는 대상

const int limit = 6; auto a = limit; auto& b = limit; int values[] = {9, 8}; auto p = values; auto& all = values;를 선언한 뒤 a = 12; p[0] = 4;를 실행합니다. a, b, p, all의 타입과 마지막의 limit, a, all[0] 값은 무엇인가요? b = 12;는 컴파일되나요?

해설 보기

a는 int, b는 const int&, p는 int*, all은 int (&)[2]입니다. a는 사본이므로 limit은 6, a는 12이고, p[0]은 원래 배열의 원소를 바꾸므로 all[0]은 4입니다. b는 const 정수의 참조이므로 b = 12;는 컴파일 오류입니다.

2. 범위 기반 for의 사본

std::vector<std::string> names{"a", "b"};에 for (auto s : names) { s += "!"; }를 실행한 뒤 names[0]은 무엇인가요? auto를 auto&로 바꾸면 어떻게 되나요?

해설 보기

auto s는 원소마다 만든 사본이므로 사본에만 !가 붙고, names[0]은 a 그대로입니다. auto& s로 바꾸면 s가 원소 자체를 참조하므로 names[0]이 a!가 됩니다. 읽기만 할 때는 사본의 비용을 피하도록 const auto&를 씁니다.

3. 중괄호와 auto

auto x = {1, 2};, auto y{1, 2};, auto z{1};은 각각 어떻게 되나요?

해설 보기

x는 등호를 쓴 중괄호 초기화여서 std::initializer_list<int>가 됩니다. y는 등호 없는 중괄호에 값이 둘이어서 컴파일 오류이고, z는 값이 하나여서 int입니다. 같은 중괄호라도 등호와 값의 개수에 따라 결과가 달라지므로, auto와 중괄호를 함께 쓸 때는 의도한 타입을 확인합니다.

다음 글 람다 표현식에서는 이름 없는 함수 객체를 그 자리에서 만드는 람다의 문법과 캡처, 그리고 캡처한 변수의 수명을 다룹니다.