C++ auto란? 타입 추론 방법과 const 참조 사용법

C++에서는 변수를 선언할 때 일반적으로 변수의 타입을 직접 작성합니다.

int number = 10;
double speed = 5.5;
const char* npcName = "Toto";

하지만 변수의 초기화 값만 보아도 타입을 명확하게 알 수 있는 경우가 있습니다. 이때 auto를 사용하면 타입을 직접 작성하지 않고 컴파일러가 초기화 값을 바탕으로 타입을 결정하도록 할 수 있습니다.

auto number = 10;
auto speed = 5.5;
auto npcName = "Toto";

이번 글에서는 auto의 기본적인 동작 원리를 살펴보고, 포인터와 참조, const, 사용자 정의 타입에 사용할 때 타입이 어떻게 결정되는지 알아보겠습니다.

목차

  1. auto란?
  2. auto 변수는 반드시 초기화해야 한다
  3. auto는 실행 중에 타입이 바뀌는 기능일까?
  4. 문자열과 정수에 auto 사용하기
  5. 초기화 값에 따라 달라지는 타입
  6. auto와 포인터
  7. auto와 참조
  8. auto와 const
  9. 사용자 정의 타입과 auto
  10. 복잡한 타입을 간결하게 표현하기
  11. auto를 사용할 수 있는 위치
  12. auto는 언제 사용하는 것이 좋을까?
  13. 정리

auto란?

auto는 변수의 초기화 값을 바탕으로 컴파일러가 타입을 추론하도록 하는 자리 표시자입니다.

auto number = 10;

컴파일러는 정수 리터럴 10을 확인하고 number의 타입을 int로 결정합니다. 따라서 위 코드는 다음 선언과 같은 의미를 가집니다.

int number = 10;

실수 리터럴로 초기화하면 리터럴의 종류에 따라 실수 타입으로 추론됩니다.

auto firstValue = 3.14;   // double
auto secondValue = 3.14f; // float

auto 자체가 하나의 자료형인 것은 아닙니다. 컴파일러가 코드를 컴파일하면서 auto가 나타내는 실제 타입을 결정합니다.

auto는 타입을 없애는 기능이 아니라, 타입을 컴파일러가 작성하도록 맡기는 기능입니다. auto로 선언한 변수도 컴파일이 완료되면 명확한 하나의 타입을 가집니다.


auto 변수는 반드시 초기화해야 한다

일반 변수는 선언할 때 초기화하지 않을 수도 있습니다.

int number;
number = 10;

첫 번째 문장에서 int를 직접 작성했으므로 컴파일러는 초기화 값이 없어도 number의 타입을 알 수 있습니다.

반면 auto에는 실제 타입이 작성되어 있지 않습니다. 컴파일러가 타입을 결정하려면 변수의 초기화 값이 필요합니다.

// auto number; // 오류: 타입을 추론할 초기화 값이 없다.

따라서 auto로 변수를 선언할 때는 반드시 초기화해야 합니다.

auto number = 10;       // int
auto speed = 5.0f;      // float
auto isAlive = true;    // bool

각 변수의 타입은 오른쪽에 작성된 초기화 표현식을 바탕으로 결정됩니다.


auto는 실행 중에 타입이 바뀌는 기능일까?

auto를 사용한다고 해서 변수의 타입이 프로그램 실행 중에 자유롭게 바뀌는 것은 아닙니다.

auto number = 10; // int

number = 20;      // 가능
// number = "Hello"; // 오류

number는 정수 리터럴 10으로 초기화되었으므로 컴파일 시점에 int 타입으로 결정됩니다.

이후 다른 정수를 대입하는 것은 가능하지만 문자열을 대입할 수는 없습니다. number가 실행 중에 문자열 타입으로 바뀌지 않기 때문입니다.

auto value = 10; // 컴파일러가 int value = 10;으로 판단한다.

즉, auto는 동적 타입을 제공하는 기능이 아닙니다. 타입을 작성하는 과정을 생략할 수 있도록 도와주는 C++의 정적 타입 추론 기능입니다.


문자열과 정수에 auto 사용하기

다음은 문자열 리터럴과 정수 리터럴을 사용해 auto 변수를 초기화하는 예제입니다.

#include <iostream>

int main()
{
    auto npcName = "Toto";
    auto number = 10;

    std::cout << npcName << '\n';
    std::cout << number << '\n';

    std::cin.get();
    return 0;
}

number는 정수 리터럴 10으로 초기화했으므로 int로 추론됩니다.

auto number = 10; // int

문자열 리터럴 "Toto"의 원래 타입은 문자열의 길이를 포함하는 const char 배열입니다. 하지만 값을 받는 일반적인 auto 변수로 초기화하면 배열이 첫 번째 요소를 가리키는 포인터로 변환됩니다.

auto npcName = "Toto"; // const char*

따라서 위 예제에서 npcNameconst char* 타입으로 추론됩니다.

출력 결과

Toto
10

문자열 리터럴로 초기화한 auto 변수가 std::string으로 자동 변환되는 것은 아닙니다. std::string 객체가 필요하다면 해당 타입의 객체를 명시적으로 만들어 초기화해야 합니다.

#include <string>

auto firstName = "Toto";                // const char*
auto secondName = std::string("Toto");  // std::string

초기화 값에 따라 달라지는 타입

auto로 결정되는 타입은 변수의 이름이나 사용하는 목적이 아니라 초기화 표현식의 타입에 따라 달라집니다.

auto count = 10;          // int
auto largeCount = 10LL;   // long long
auto ratio = 0.5;         // double
auto speed = 0.5f;        // float
auto isVisible = true;    // bool
auto grade = 'A';         // char

예를 들어 0.5는 기본적으로 double 리터럴이고, 접미사 f가 붙은 0.5ffloat 리터럴입니다.

따라서 두 변수는 값이 비슷해 보여도 서로 다른 타입으로 추론됩니다.

auto firstValue = 0.5;  // double
auto secondValue = 0.5f; // float

auto를 사용할 때도 정수와 실수 리터럴의 타입 및 접미사 규칙을 이해해야 하는 이유입니다.


auto와 포인터

auto는 포인터 변수를 선언할 때도 사용할 수 있습니다.

int userMode = 4;

auto pointer = &userMode; // int*

&userMode의 타입은 int*이므로 pointerint*로 추론됩니다.

포인터라는 사실을 선언에 명확히 나타내고 싶다면 auto*를 사용할 수 있습니다.

auto* pointer = &userMode; // int*

이 예제에서는 autoauto* 모두 결과적으로 int* 타입을 만듭니다. 다만 auto*는 해당 변수가 포인터라는 사실을 코드에 직접 드러냅니다.

#include <iostream>

int main()
{
    int userMode = 4;

    auto* pUserMode = &userMode;

    std::cout << "pUserMode : Value - " << *pUserMode
        << ", Address - " << pUserMode << '\n';

    std::cin.get();
    return 0;
}

pUserMode에는 userMode의 주소가 저장됩니다. 역참조 연산자 *를 사용하면 해당 주소에 저장된 정수 값을 가져올 수 있습니다.

출력 결과

메모리 주소는 프로그램을 실행할 때마다 달라질 수 있습니다.

pUserMode : Value - 4, Address - 0000009A4E2FF9F4

auto와 참조

일반적인 auto는 초기화 표현식의 값을 사용해 새로운 변수를 만듭니다.

int userMode = 4;

auto copiedMode = userMode; // int

copiedModeuserMode와 별개의 변수입니다. 따라서 copiedMode의 값을 변경해도 userMode에는 영향을 주지 않습니다.

copiedMode = 5;

// userMode는 4
// copiedMode는 5

원본 변수의 참조를 만들려면 auto&를 사용해야 합니다.

auto& refUserMode = userMode; // int&

refUserMode는 새로운 정수 객체가 아니라 userMode를 가리키는 참조입니다. 따라서 참조를 통해 값을 변경하면 원본 변수에도 반영됩니다.

#include <iostream>

int main()
{
    int userMode = 4;

    auto* pUserMode = &userMode;

    std::cout << "pUserMode : Value - " << *pUserMode
        << ", Address - " << pUserMode << '\n';

    auto& refUserMode = userMode;
    refUserMode = 5;

    std::cout << "userMode : Value - " << userMode
        << " | refUserMode : Value - " << refUserMode << '\n';

    std::cin.get();
    return 0;
}

출력 결과

pUserMode : Value - 4, Address - 0000009A4E2FF9F4
userMode : Value - 5 | refUserMode : Value - 5

refUserMode5를 대입한 뒤 userMode의 값도 5로 변경된 것을 확인할 수 있습니다.

선언 추론된 타입 동작
auto value = userMode; int 원본 값을 복사해 별도의 변수를 만듦
auto& value = userMode; int& 원본 변수를 참조함
auto* value = &userMode; int* 원본 변수의 주소를 저장함

auto와 const

const가 적용된 변수를 auto와 함께 사용할 때는 값으로 복사하는지, 참조로 받는지에 따라 추론 결과가 달라집니다.

const int maxUserCount = 100;

auto copiedCount = maxUserCount;
auto& refCount = maxUserCount;

copiedCountmaxUserCount의 값을 복사해서 만든 별도의 변수입니다. 값으로 추론하는 일반적인 auto에서는 원본 타입의 최상위 const가 제거됩니다.

auto copiedCount = maxUserCount; // int

따라서 copiedCount의 값을 변경할 수 있습니다.

copiedCount = 200; // 가능

반면 auto&는 원본의 참조를 만들기 때문에 원본에 적용된 const가 유지됩니다.

auto& refCount = maxUserCount; // const int&

// refCount = 200; // 오류

maxUserCountconst int이므로 이를 참조하는 refCount를 통해 값을 변경할 수 없습니다.

새로운 auto 변수 자체를 상수로 만들고 싶다면 선언 앞에 const를 작성할 수 있습니다.

const auto minUserCount = 1; // const int

다음 예제에서 각 변수의 타입과 수정 가능 여부를 확인할 수 있습니다.

#include <iostream>

int main()
{
    const int maxUserCount = 100;

    auto copiedCount = maxUserCount; // int
    auto& refCount = maxUserCount;   // const int&
    const auto minUserCount = 1;     // const int

    copiedCount = 200;

    // refCount = 200;     // 오류
    // minUserCount = 10;  // 오류

    std::cout << copiedCount << '\n';
    std::cout << refCount << '\n';
    std::cout << minUserCount << '\n';

    std::cin.get();
    return 0;
}

출력 결과

200
100
1
선언 추론된 타입 수정 가능 여부
auto copiedCount = maxUserCount; int 복사본 수정 가능
auto& refCount = maxUserCount; const int& 참조를 통한 수정 불가
const auto minUserCount = 1; const int 수정 불가

일반적인 auto는 값을 복사하면서 최상위 const를 제거합니다. 원본을 복사하지 않고 참조하거나 원본의 const를 유지해야 한다면 auto& 또는 const auto& 사용을 고려해야 합니다.


사용자 정의 타입과 auto

autointfloat 같은 기본 타입에만 사용하는 기능이 아닙니다. 클래스와 구조체처럼 사용자가 정의한 타입에도 사용할 수 있습니다.

#include <iostream>
#include <string>

class Character
{
public:
    Character(const std::string& name)
        : name(name)
    {
    }

    void PrintName() const
    {
        std::cout << name << '\n';
    }

private:
    std::string name;
};

int main()
{
    auto character = Character("Toto");
    character.PrintName();

    std::cin.get();
    return 0;
}

Character("Toto") 표현식은 Character 객체를 만듭니다. 따라서 character의 타입도 Character로 추론됩니다.

auto character = Character("Toto"); // Character

위 선언은 다음과 같은 의미입니다.

Character character = Character("Toto");

auto로 선언했더라도 character는 일반적인 Character 객체입니다. 따라서 Character 클래스가 제공하는 멤버 함수를 동일하게 호출할 수 있습니다.

출력 결과

Toto

복잡한 타입을 간결하게 표현하기

auto는 타입 이름이 길거나 복잡한 경우에 특히 유용합니다.

예를 들어 std::vector<int>의 반복자 타입을 직접 작성하면 다음과 같습니다.

#include <vector>

std::vector<int> numbers = { 10, 20, 30 };

std::vector<int>::iterator iterator = numbers.begin();

numbers.begin()이 반환하는 타입은 이미 컴파일러가 알고 있으므로 auto를 사용해 선언을 줄일 수 있습니다.

auto iterator = numbers.begin();

컨테이너의 타입이 변경되더라도 반복자 선언을 함께 수정하지 않아도 된다는 장점도 있습니다.

#include <list>

std::list<int> numbers = { 10, 20, 30 };

auto iterator = numbers.begin();

직접 반복자 타입을 작성했다면 std::vector<int>::iteratorstd::list<int>::iterator로 변경해야 합니다. auto를 사용한 선언은 초기화 표현식에 맞춰 타입이 다시 추론됩니다.

범위 기반 for문에서도 auto를 자주 사용합니다.

#include <iostream>
#include <vector>

int main()
{
    std::vector<int> numbers = { 10, 20, 30 };

    for (auto number : numbers)
    {
        std::cout << number << '\n';
    }

    return 0;
}

각 요소를 복사하지 않고 참조하려면 auto&를 사용할 수 있습니다.

for (auto& number : numbers)
{
    number *= 2;
}

요소를 복사하지 않으면서 수정하지도 않을 경우에는 const auto&를 사용할 수 있습니다.

for (const auto& number : numbers)
{
    std::cout << number << '\n';
}
선언 동작
auto number 각 요소를 복사함
auto& number 각 요소를 참조하며 수정할 수 있음
const auto& number 각 요소를 복사하지 않고 읽기 전용으로 참조함

auto를 사용할 수 있는 위치

auto는 지역 변수를 선언할 때 자주 사용하지만 지역 변수에만 사용할 수 있는 것은 아닙니다.

초기화 값을 제공한다면 전역 변수에도 사용할 수 있습니다.

auto globalCount = 10; // int

하지만 일반적인 비정적 멤버 변수의 타입을 auto로 선언할 수는 없습니다.

class Character
{
private:
    // auto health = 100; // 오류: 비정적 멤버 변수에 사용할 수 없다.
};

auto를 함수의 매개변수나 반환형에 사용할 수 있는 범위는 사용하는 C++ 표준에 따라서도 달라집니다.

  • C++11에서는 후행 반환 형식과 함께 함수 반환형에 auto를 사용할 수 있습니다.
  • C++14부터는 함수 본문의 return 표현식을 바탕으로 반환형을 추론할 수 있습니다.
  • C++14부터 제네릭 람다의 매개변수에 auto를 사용할 수 있습니다.
  • C++20부터 일반 함수의 매개변수에도 auto를 사용해 축약 함수 템플릿을 작성할 수 있습니다.

다음은 C++14에서 제네릭 람다의 매개변수에 auto를 사용한 예입니다.

auto add = [](auto first, auto second)
{
    return first + second;
};

auto integerResult = add(10, 20);
auto realResult = add(1.5, 2.5);

다만 이러한 활용은 변수의 타입을 추론하는 기본적인 auto 문법을 이해한 뒤 템플릿과 람다를 학습하면서 자세히 살펴볼 수 있습니다.


auto는 언제 사용하는 것이 좋을까?

auto를 사용하면 반복자나 템플릿 관련 타입처럼 긴 타입 이름을 간결하게 표현할 수 있습니다. 또한 초기화 표현식과 변수 타입이 항상 일치하도록 만들 수 있습니다.

auto iterator = numbers.begin();

반면 초기화 값만으로 변수의 타입이나 의미를 파악하기 어려운 경우에는 직접 타입을 작성하는 편이 코드를 읽기 쉬울 수 있습니다.

auto result = Calculate();

위 코드만으로는 result가 정수인지, 실수인지, 사용자 정의 객체인지 알기 어렵습니다. 함수 이름과 주변 문맥이 충분한 정보를 제공하지 않는다면 명시적인 타입 선언이 가독성에 도움이 될 수 있습니다.

특히 숫자 리터럴을 사용할 때는 의도한 타입으로 추론되는지 확인해야 합니다.

auto ratio = 1.0;  // double
auto speed = 1.0f; // float

auto는 모든 타입을 무조건 감추기 위한 기능이라기보다, 타입을 직접 작성하는 것보다 컴파일러가 추론하도록 하는 편이 더 명확하고 안전한 상황에서 사용하는 것이 좋습니다.

  • 반복자처럼 타입 이름이 길고 복잡한 경우
  • 초기화 표현식만으로 타입을 명확하게 알 수 있는 경우
  • 람다 표현식처럼 직접 작성하기 어려운 타입을 저장하는 경우
  • 컨테이너 요소를 순회하면서 auto, auto&, const auto&로 복사 및 참조 의도를 표현하는 경우

반대로 추론 결과가 불분명하거나 자료형 자체가 코드의 의미를 전달하는 중요한 정보라면 명시적인 타입을 사용하는 것도 좋은 선택입니다.


정리

  • auto를 사용하면 초기화 표현식을 바탕으로 컴파일러가 변수의 타입을 결정합니다.
  • auto는 실제 자료형이 아니라 타입 추론을 위한 자리 표시자입니다.
  • auto 변수는 컴파일러가 타입을 추론할 수 있도록 반드시 초기화해야 합니다.
  • auto로 결정된 타입은 프로그램 실행 중에 다른 타입으로 바뀌지 않습니다.
  • 정수와 실수 리터럴의 종류 및 접미사에 따라 서로 다른 타입으로 추론될 수 있습니다.
  • 문자열 리터럴로 일반적인 auto 변수를 초기화하면 const char*로 추론됩니다.
  • auto*를 사용하면 포인터라는 사실을 선언에 명확하게 나타낼 수 있습니다.
  • 일반적인 auto는 값을 복사하고, auto&는 원본을 참조합니다.
  • 일반적인 auto로 값을 복사하면 원본 타입의 최상위 const가 제거됩니다.
  • auto&로 상수 변수를 참조하면 원본의 const가 유지됩니다.
  • const auto를 사용하면 추론된 타입의 상수 변수를 만들 수 있습니다.
  • auto는 기본 타입뿐만 아니라 클래스와 구조체 같은 사용자 정의 타입에도 사용할 수 있습니다.
  • 반복자처럼 타입 이름이 긴 경우 auto를 사용하면 선언을 간결하게 작성할 수 있습니다.
  • 범위 기반 for문에서는 복사와 참조 여부에 따라 auto, auto&, const auto&를 구분해서 사용해야 합니다.
  • auto의 사용 가능한 위치와 기능은 C++ 표준에 따라 일부 차이가 있습니다.
  • 추론되는 타입이 불분명하거나 타입 자체가 중요한 정보를 전달한다면 명시적인 타입을 작성하는 편이 좋을 수 있습니다.

이전 글: C++ 타입 별칭이란? typedef와 using의 차이점 알아보기

다음 글: C++ 포인터란? 주소와 역참조부터 동적 메모리 할당까지

댓글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다