최근 수정 시각 : 2024-03-01 19:17:44

C++/문법/자료형

파일:관련 문서 아이콘.svg   관련 문서: C++
,
,
,
,
,

파일:상위 문서 아이콘.svg   상위 문서: C++/문법
프로그래밍 언어 문법
{{{#!wiki style="margin: -16px -11px; word-break: keep-all"<colbgcolor=#0095c7><colcolor=#fff,#000> 언어 문법 C(포인터) · C++(자료형 · 특성 · 클래스 · 이름공간 · 상수 표현식) · C# · Java · Python · Kotlin · MATLAB · SQL · PHP · JavaScript
마크업 문법 HTML · CSS
개념과 용어 함수 · 인라인 함수 · 고차 함수 · 람다식 · 리터럴 · size_t · 상속 · 예외 · 조건문 · 참조에 의한 호출 · eval
기타 == · === · NaN · null · undefined · 모나드 · 배커스-나우르 표기법
프로그래밍 언어 예제 · 목록 · 분류 }}}

1. 개요2. 원시 자료형 (Primitives)3. 자료형 한정자 (Type Qualifier)
3.1. const3.2. volatile3.3. & 참조
3.3.1. const&
3.4. && 참조3.5. 포인터
4. using5. auto6. decltype
6.1. decltype(auto)6.2. decltype(expression)
7. 값 범주 (Value Category)
7.1. lvalue
7.1.1. 문자열 리터럴
7.2. prvalue7.3. xvalue7.4. glvalue (lvalue & xvalue)7.5. rvalue (prvalue & xvalue)

1. 개요

C++의 자료형에 관해 포괄적으로 설명하는 문서.

2. 원시 자료형 (Primitives)

C언어의 자료형을 그대로 사용할 수 있다. C17까지는 C언어에서 진리값(bool[1])을 사용하려면 <stdbool.h>를 삽입하고 _Bool, 0, 1을 써야 했으나 C23에서 bool, true, false가 정식으로 편입되면서 원시 자료형에서는 C와 C++의 차이가 아예 사라졌다. C에서 파생 자료형으로 제공되던 size_t, rsize_t, ptrdiff_t도 제공한다.

===# Truthy / Falsy #===
C++에서는 1이 참(True), 0이 거짓(False)로 대응되는 것이 기본적인 사용법이나, 0이 아닌 숫자가 대입된다면 사실상 전부 True로 인식되는 탓에, 모든 숫자가 1로 바뀌어 출력된다.
#!syntax cpp 
#include <iostream>

int main()
{
    bool a = 20, b = 0, c = -2;
    std::cout << a << ", " << b << ", " << c;
    return 0;
} 
해당 코드를 실행시켜보면 출력 결과가 1, 0, 1임을 확인할 수 있다.

3. 자료형 한정자 (Type Qualifier)

3.1. const

const Type identifier;
상수 (Constant)
다른 자료형 앞에 붙어 값이 불변함을 나타낸다. 가령 const int는 변하지 않는 정수 값임을 의미한다. 사용자 단에서는 변수 선언 시, 함수의 결과값을 const로 표시하여 그 값을 변경하지 말라는 지시 효과를 볼 수 있다. 함수 구현 시에는 매개 변수에 붙여 사용자의 실수를 줄이고 예측할 수 없는 값의 수정을 막아준다.
만약 포인터와 조합했을때는 포인터가 가리키는 값이 불변인지, 아니면 포인터 자체가 불변인지가 달라진다. 예를 들어 const int* ptr;const int의 포인터이므로, *ptr로 접근한 값은 const라서 수정할 수 없다. 그러나 ptr이라는 변수 자체는 더하고 빼고 등등 임의의 연산도 할 수 있다. int* const ptr;ptr 자체가 불변이지만 *ptr로 접근한 값은 그냥 int라서 바꿀 수 있다.

잘 사용되는 예로는 문자열 포인터가 있다. const char* string은 변하지 않는 문자값의 포인터를 의미한다. 그러나 포인터 자체가 아니라 문자의 값이 상수이기 때문에 변수 string에 1을 더하고 빼는 등 값을 변경할 수 있다. 표준 라이브러리의 <algorithm> 모듈에서는 이를 이용해 자료구조 뿐만 아니라 문자열에 대해서도 동일한 연산을 지원한다. 반면 char* const& stringstring이 가리키는 char* 값 하나는 *string = 'B'; 처럼 언제든지 값이 바뀔 수 있다. 그러나 string 변수는 불변이다.

사실 상수 포인터, 상수를 가리키는 포인터는 현재 C++에서는 고려할 필요가 없다. C++17에서 string_view, span의 도입으로 웬만한 포인터 사용을 대체할 수 있기 때문이다 [2]. 이해하기 어렵다면 다른 자료형 앞에 붙어야 의미가 있다는 것을 기억하자.

3.2. volatile

volatile Type identifier;
휘발성 (Volatile)
다른 자료형 앞에 붙어 캐시에 대해 최적화를 막고 항상 값이 보이도록 한다. 이를 이해하려면 운영체제와 컴파일러에 대해 이해가 필요하다. 컴파일러는 바이너리를 구축하는 과정에서 인라이닝, 캐싱, 파이프라인 분기 예측, 상수식 평가 등을 동원해 가장 빠른 바이너리를 만든다. 최적화를 하는 과정에서 몇가지 변수와 상수 표현식으로 선언된 상수는 없어진다. 컴파일 시간에 평가할 수 있는 구문은 미리 작성한다. 그리고 평가되지 않을 구문 역시 아예 코드에서 배제된다. 심지어는 메서드의 존재 자체가 없어지고 모든 쓰임새가 반환값으로 대체될 수 있다. 예를 들어 어떤 동일한 for문이 여러 곳에서 반복된다면 for문의 내용을 진짜 반복하는 대신에 결과를 미리 계산해놓고 가져다 쓸 것이다.

이 과정은 거의 대부분의 경우 이득을 가져다 주지만, 변수의 존재 삭제와 구문 단축이 문제가 된다. while(bool 변수);로 문맥의 흐름을 막았다고 해보자. 그럼 while문 안의 bool 변수가 다른 스레드에서 false로 바뀌면 무한 루프를 빠져나갈까? 정답은 그렇지 않다. 계속 갇혀있다. 왜냐하면 while안의 변수는 최적화를 위해 메모리를 읽지 않고 컴파일 순간 정해진 캐시의 값을 계속 가져오기 때문이다. 이를 다른 스레드에서 보이지 않는 값이 되었다고 한다. 또다른 예로는 프로그램 내내 같은 주소를 가리키는 포인터의 값을 읽을 필요가 있다고 해보자. 이러면 컴파일러는 컴파일 당시에 해당 포인터가 가리켰던 값만을 가져온다. 그래서 해당 포인터가 가리키는 값이 변경되어도 프로그램에선 바뀐 값을 알지 못한다. 바로 이럴때 volatile을 붙여서 캐시말고 항상 메모리에서 가져오도록 만들 수 있다. 즉 휘발성이라는 말은 현재 변수에서 읽어올 값이 임시적이라는 의미다.

참고로 volatile은 컴파일러의 캐시 최적화, 인위적인 코드 패치 순서 수정[3]을 막을 뿐이므로 다중 스레드를 사용할 때는 여전히 값을 읽기 순서에 따른 동기화 문제가 발생할 수 있다.

3.3. & 참조

Type& lvalue = identifier;
좌측값 참조자 (L-Value Reference)
type& identifier = value;와 같이 &를 다른 자료형 뒤에 붙여 값이 참조 형태임을 나타낸다. 가령 int&는 다른 정수를 참조하는 변수임을 의미한다. 함수의 매개변수에서도 똑같이 사용한다. 변수에 값을 할당하거나, 수정할 때 다른 접두사, 접미사 없이 변수의 이름만 사용할 수 있다. 참조한다는 것은 해당 변수가 스스로 값을 가지지 못하고, 다른 변수의 본체를 가리킨다는 뜻이다. 마치 포인터처럼 말이다.

그러면 C의 포인터랑 대체 다른 것이 뭔가? 라면 일단 일반 사용자 단에서는 변수를 사용할 때 (*handle)이나 handle->...을 사용하지 않아도 된다. 메모리에 대해 조금 알고 있다면 포인터처럼 임의 주소 참조나 보안 문제가 없을 거라는 예상을 할 수 있다. 그러나 깊게 파고들어가면 아주 이상한 특징이 있다. 참조자는 값이 아니다. 라는 것을 명심해야 한다. 참조 변수 자체는 어떤 주소, 고유한 값을 가지지 않는다. 즉 참조 변수는 스스로 존재할 수 없다[4]. 바로 이름만 가진 변수다. 참조 변수는 명백하게 존재하고 여기저기 갖다 쓸 수 있지만 참조 변수가 가리키는 어떤 변수만 조작할 수 있을 뿐, 정작 참조 변수 자체는 건드릴 방법이 없다. 이것이 포인터와의 차이점이다. 결론적으로 참조 변수는 다른 변수의 별칭(Alias)이라고 볼 수 있다. 그래서 참조형을 사용할 때는 어떠한 값의 복사나 수정 오버헤드도 일어나지 않는다. 어떠한 조작 없이 그냥 원래 변수에 다른 식별자를 붙여주었을 뿐이기 때문이다! 만약 & 연산자를 변수 앞에 붙여 주소를 얻으면 참조 변수의 주소가 아니라, 가리키는 변수의 주소가 나온다. 주소를 가져오는 표준 라이브러리의 std::addressof 함수를 사용해도 마찬가지다.

그러나 참조형을 사용할 때 주의점이 있다. 임시 값과 리터럴 값을 저장할 수가 없다. 왜냐하면 임시 값과 리터럴 값은 어떤 변수에 저장하기 전까진 이름이 없어서 스스로 존재할 수 없는 값이기 때문이다. 당연히 고유 주소도 없으며 수정할 수 없는 값들이다. 때문에 const가 아닌 좌측 참조 변수는 임시 값 및 리터럴 값을 받을 수 없다.

3.3.1. const&

const Type& const_lvalue = identifier;
포인터와 같은 원리로, const int&와 같이 const를 참조 자료형 앞에 붙여 매개변수가 가리키는 값을 수정하지 말라고 지시를 내릴 수 있다. 그렇지만 좌측 참조자의 진정한 의의는 C++안에 있는 모든 유형의 값을 전부 담을 수 있다는 것이다. 이는 일반적으로 생각하는 클래스같은 복합 자료형에만 적용되지 않는다. const&는 독특하게 어떤 값이던 대입할 수 있다. 가령 const int& power = 9000;, struct A{}; const A& aaa = {};처럼 그 어떤 값도 담을 수 있다. 복사 불가능한 클래스, 이동 불가능한 클래스, 크기가 4GB짜리인 배열 등도 아무런 문제가 없다. 어떻게 가능한 것일까?

<C++ 예제 보기>
#!syntax cpp
int original_v1 = 1'000'000;
int original_v2 = 7'000'000;
int original_v3 = 9'000'000;

int& ref_v1 = original_v1;
int& ref_v2 = original_v2;
int& ref_v3 = original_v3;

ref_v1 = 1000; // original_v1의 값이 1000이 된다.
int* handle_v1 = &ref_v1; // original_v1의 주소를 가져온다.

ref_v1 = original_v2; // 가리키는 변수가 바뀌지 않는다. original_v1의 값이 7000000이 된다.
ref_v1 = ref_v2; // 둘 다 가리키는 변수가 바뀌지 않는다. original_v1의 값이 7000000이 된다.
*handle_v1 = original_v3;// 가리키는 변수가 바뀌지 않는다. original_v1의 값이 9000000이 된다.

const int original_c = 1'000'000;
const int& ref_c = original_c; // 상수 변수는 상수 참조형으로 받아야 한다.
C++에서 참조 변수를 쓴다는 것은 변수에 다른 이름을 붙여주는 것이라고 설명했다. 그런데 9000같은 값은 이름이 없는 리터럴 값이다. 원래 이 값들은 우측값 (Right-Value)이라고 부르는 값이였다. 전통적으로 C언어에서는 임시 값이 선언, 대입, 비교문에서 식의 오른쪽에 놓이는 경향이 있어서 이렇게 지칭되어 왔다 [5]. 그런데 C에서 포인터로 행하던 복사 없는 일관성있는 참조가 C++의 & 참조 하나만으로는 불가능하다는 문제가 있었다. 만약 어떤 함수에서 정수, 구조체 변수를 참조한다고 했을때 포인터 방식에서는 포인터 매개변수를 써서 오버로딩을 통해 적어도 일관성있는 함수들을 만들 수 있다. 주소가 필요하기 때문에 값을 담을 중계 변수를 하나 만들어야 하겠지만 말이다. 그런데 참조 변수를 만들어서 문제 많은 포인터를 대체하려고 했더니, 여전히 똑같은 짓을 해야한다는 문제가 있었다. 아직도 정수 그대로를 함수에 전달할 수 없었다. 그래서 const& 한정자는 임시 객체에 이름을 붙여주고 임시 값과 리터럴 값을 참조할 수 있게 되었다. 앞서 설명했듯이 여전히 & 참조 변수는 이것이 불가능하다. 당장 int& value = 9000; 따위는 오류가 발생한다.

3.4. && 참조

Type&& rvalue;
const Type&& const_rvalue;
우측값 참조자 (R-Value Reference)
C++11에서 추가된 핵심 문법이다. 컴파일러에게 메모리의 중복 할당을 방지하며 재사용을 지시하고, 값의 깊은 복사를 막는 역할을 한다. 그래서 &&를 다른 말로 이동 연산자라고도 부른다. &&& 참조형처럼 여전히 스스로 존재할 수 없는 존재다. &&변수의 이름 자체도 아무런 의미가 없다. 사용자가 직접 && 자료형을 명시하던가, 아니면 함수의 도움 없이는 C++에서 &&는 항상 &로, const&&는 항상 const&로 연역된다. &&&보다 더 불안정해서, 존재가 바스라지는 존재다.

C언어에서 깊은 복사를 막기 위해 메모리 풀링, 포인터 사용을 오랜 세월 해왔다. C++의 & 참조자 역시 최대한 얕은 복사로의 유도를 했을 뿐, 어쩔 수 없이 임시 객체 생성, 변수의 중복 선언 등으로 인한 오버헤드가 있었다. 시스템 자원의 중복도 큰 문제가 되었다. 가령 스레드, 뮤텍스, GDI 객체, 핸들은 시스템에서 생성되고 관리된다. 사용자는 운영체제 호출을 통해 간접적으로 제어할 수 있다. 그런데 생성, 제어는 그렇다 치고 이 중복된 자원들이 파괴되는 경우가 있을 것이다. 이때 다른 곳에서 핸들이 파괴된 일을 모르면 잘못된 운영체제 호출이 발생하고, 이 오류는 단순한 런타임 오류와는 궤를 달리할 것이다.

기존에는 한정자 없는 생성자 또는 const& 생성자 뿐이었고, 이를 본질적으로 구분할 수 없다는 문제가 있었다. 생성할 때 변수에 넣지 않고 CThread work_thread{CThread{ th_id, x, y }}; 처럼 시스템 자원 객체를 바로 전달받아도 복사 생성자에서 필연적으로 const CThread& 임시 객체가 생성되버린다. 이때 보이지 않는 const CThread& 객체는 work_thread에 시스템 자원을 순순히 넘겨주는 것처럼 보여도, 만약 소멸자에서 시스템 자원을 해제하도록 했다면 work_thread는 생성하자마자 죽은 객체가 된다. 이를 막으려면 두가지 방법이 있다. 임시 객체인지 표시하는 플래그를 넣던가, 자원을 해제하는 전역 함수를 별도로 만들어야 하는데, 모두 최적화, 깔끔함 둘 다 만족시키지 못한다. &&는 &를 하나 더 붙여 컴파일러와 사용자에게 복사, 참조와는 구분하게 하고 중복 자원의 문제도 깔끔하게 해결한다. 객체를 생성하는 방법을 하나 더 제시함으로써 많은 문제가 해결된 것이다.

사실 &&는 입문 시기에는 직접 쓸 필요가 없다. 컴파일러가 알아서 복사, 이동 생성자를 만들어주니까. 그러나 진도를 조금만 넘겨도 혜성처럼 등장하고, 고급 단계에서 이해하지 못하면 C++의 알 수 없는 기전에 좌절할 수 있다. C++의 표준 라이브러리에서는 <utility> 모듈의 std::move라는 함수로 간편한 이동 연산을 제공한다. 또는 사용자가 직접 static_cast<T&&>(value)로 지시할 수 있다.

3.5. 포인터

Type* ptr;
Type*& reference_of_ptr = ptr;

const Type* ptr_to_const;
const Type*& reference_of_ptr_to_const = ptr_to_const;

Type *const constant_ptr;
Type *const& reference_of_constant_ptr = constant_ptr;

const Type *const constant_ptr_to_const;
const Type *const& reference_of_constant_ptr_to_const = constant_ptr_to_const;
*를 자료형 뒤에 붙이면 어떤 주소를 담고 있는 자료형임을 나타낼 수 있다. 참조형의 경우 참조형 자체의 변수는 얻을 수 없으며 참조형이 가리키는 원본 변수의 주소를 담아야 한다. 정확하게는 포인터의 참조형만을 받을 수 있고 참조형의 포인터는 불가능하다. 작성할 때는 *&&& 앞에 붙여야 한다. 그 외에는 C언어에서의 사용법과 다르지 않다.

4. using

using Alias = Type;

template<typename... Ts>
using TemplateAlias = Type<Ts...>;
키워드 using을 사용하여 자료형의 별칭(Alias)을 선언할 수 있다. C의 typedef 구문을 대체하는 구문으로써 가독성 상승을 비롯하여 템플릿을 사용할 수 있게 되었다. 별칭의 이름으로 주어지는 식별자는 사용자가 구현한 클래스나 템플릿과 마찬가지로 새로운 자료형으로 분류된다. 즉 이 구문은 변수를 선언하는 것처럼 형식을 선언하는 것이라고 볼 수 있다. 실제로 표준에서도 자료형을 선언한다고 표현한다. 그러나 참조 변수와 같은 맥락으로 이해해야 한다. 기존의 자료형에 다른 이름을 붙이는 것이기 때문에, 기존의 자료형을 사용하더라도 using으로 선언한 자료형과 호환된다. 템플릿도 마찬가지라서 void Function(std::vector<int>& vec);이라는 함수가 있다면, 이 함수에는 using IntVector = std::vector<int>;와 그냥 std::vector<int> 모두를 인자로 전달할 수 있다.

5. auto

auto identifier = value;
C++에서 일반 사용자가 가장 유용하게 사용할 수 있는 기능 중에 하나다. C++에서 극히 희귀한 문법적 설탕 요소이며 자료형을 일일이 명시해야하는 문제를 해결하기 위해 도입되었다. 라이브러리를 직접 작성하는 것이 아니라면 클라이언트 단에서는 적극적으로 사용해도 큰 문제는 없다. 사용법은 변수와 함수에서 자료형을 작성할 때 대신 auto를 기입하면 된다. 컴파일러가 해당 변수, 함수의 반환형을 추론하여 바이너리에 알아서 반영해준다. 이때 auto의 추론 형식은 가능한 반환하는 값의 원본에 영향을 주지 않도록 추론된다. 함수의 반환값을 받을 때 바로 알 수 있다. 반환형에 한정자가 붙은 함수를 auto로 값을 받으면 *를 제외한 한정자가 무시된다.

주의할 점은 구조체의 멤버 변수, 함수의 선언 시에는 사용이 불가능하다는 것이다. auto가 어떤 자료형인지는 컴파일 시점에 결정되는데 auto 자체는 마치 참조 변수마냥 원래 쓰일 자료형의 별칭이기 때문이다. 참조 변수처럼 auto 혼자서는 존재할 수 없다. 따라서 클래스의 필드 자료형은 모두 명시해야 한다.

<C++ 예제 보기>
#!syntax cpp
class Counter
{
public:
    constexpr Counter() noexcept = default;
    constexpr ~Counter() noexcept = default;

    constexpr Counter(const size_t& number) noexcept
        : myNumber(number)
     {}

     constexpr size_t& GetNumber() noexcept { return myNumber; }
     constexpr const size_t& GetNumber() const noexcept { return myNumber; }
     constexpr auto GetNumber2() noexcept { return myNumber; } // 한정자 없는 size_t 반환
     constexpr auto GetNumber2() const noexcept { return myNumber; } // 한정자 없는 size_t 반환

protected:
    size_t myNumber = 0;
}

int main()
{
    Counter counter_v{ 300 };
    constexpr Counter counter_c{ 500 };

    auto cnt1 = counter_v.GetNumber(); // size_t
    const auto cnt2 = counter_v.GetNumber(); // const size_t
    const auto& cnt3 = counter_v.GetNumber(); // counter_v.myNumber 필드의 불변 참조 변수다. 

    size_t cnt4 = counter_v.GetNumber();
    size_t& cnt5 = counter_v.GetNumber(); // cnt5를 통해 counter_v.myNumber 필드를 수정할 수 있다.
    const size_t& cnt6 = counter_v.GetNumber(); // counter_v.myNumber 필드의 불변 참조 변수다. 
    
    auto cnt7 = counter_c.GetNumber(); // size_t
    auto& cnt8 = Counter{ 700 }.GetNumber(); // 형식 한정자 오류!
    const auto& cnt8 = counter_c.GetNumber(); // const size_t& 이며 counter_c.myNumber 필드의 참조 변수다.
}
이를 극복하기 위해 *, &, &&, const, volatile를 직접 붙일 수 있다. 그러나 auto를 쓰는 이유가 뭔지 생각해본다면 조금 아쉬운 면이 있다. 실제 코드에서는 const& 또는 때때로 헷갈림 방지를 위해 * 말고는 더 붙일 일이 없을 것이다.

6. decltype

6.1. decltype(auto)

decltype(auto) identifier = value;
<C++ 예제 보기>
#!syntax cpp
int main()
{
    Counter counter_v{ 300 };
    constexpr Counter counter_c{ 500 };

    decltype(auto) cnt1 = counter_v.GetNumber(); // size_t& counter_v.myNumber 필드의 참조 변수다. 
    decltype(auto) cnt2 = counter_c.GetNumber(); // const size_t& counter_c.myNumber 필드의 참조 변수다. 

    const decltype(auto)& cnt3 = counter_v.GetNumber(); // 오류! decltype(auto)는 한정자를 붙일 수 없다.
}

앞서 설명한 auto는 조금 부족한 면이 있었다. auto 혼자서는 온전한 자료형을 얻을 수 없기 때문이다. 불필요한 복사가 발생하는 문제도 있다. 해결책으로는 const&를 붙이는 방법이 있지만 번거롭고 반복적인 작업일 뿐이다. 이에 도입된 decltype(auto)는 참조형까지 전부 완전한 자료형을 가져온다. auto 대신에 사용하면 된다. 참고로 decltype(auto)는 그 자체로 완성된 자료형을 가져오므로 형식 한정자를 더 붙일 수 없다. 원한다면 using을 써서 자료형을 붙여야 한다.

6.2. decltype(expression)

using Alias = decltype(expr);
인자로 전달한 표현식을 묵시적으로 평가한 결과의 자료형을 얻는다. 가령 decltype(50 + 400U)unsigned int를 반환한다. 여기서 묵시적으로 평가했다는 말은 표현식이 실제로 실행되지는 않는다는 뜻이다. 오직 반환형만을 알 수 있다. 예를 들어서 decltype((int*)malloc(50000000))같은 무지막지한 식이라도 얌전히 int*를 반환할 것이다. 그리고 여기서 알 수 있듯이 표현식이 반드시 상수 표현식일 필요는 없다. 성능 문제가 있을까 싶지만 사용자 단에서 반환형이 무엇인지 알 수 없더라도, C++ 타입 시스템은 반드시 컴파일 시점에 모든 자료형을 확정짓는다.

이것이 유용한 예로는 정수와 큰 정수의 연산, 실수와 정수 사이의 연산, 비용이 큰 연산을 하기 전에 자료형을 가져와서 미리 준비를 할때다. 서로 다른 정수 사이에는 더 바이트 수가 큰 정수로 승급하는 규칙이 있고, 실수와 정수 사이에는 실수로 변환되는 규칙이 있다. 비용이 큰 연산에는 직렬화, 문자열 포맷 등이 있다. 직렬화를 하려면 원본 자료형을 더 작은 자료형으로 변환하거나, 일정한 크기의 메모리에 변환된 값들을 쓰는 과정이 필요하다. 이때 실제로 변환 작업을 하기 전에 먼저 필요한 메모리를 할당하는 수가 있다. 표준 라이브러리에서 사용하는 예로, 시간 라이브러리 <chrono>에서 서로 다른 시간 단위를 서로 연산할 때, 불필요한 시간 변환 작업을 줄이기 위해 decltype(lhs + rhs)using을 사용하여 반환형을 가져온다. 이것 역시 auto 대신에 사용하면 된다.

7. 값 범주 (Value Category)

C++의 독특한 점이라면 모든 요소를 극한으로 분류했다는 점이다. 값의 종류도 마찬가지다. 이 이란 메모리 상의 어떤 값을 보여주는 요소로써, 식별자(identifier), 예약어(Keyword), 문장부호(Punctuation), 특성(Attribute), 구문(Statement), 템플릿(Template)을 제외한 모든 것이다. 일반적으로 생각되는 변수, 상수로 구별하는 것을 넘어 어디서 어떻게 생겨나고, 어디서 사라지는지, 어떻게 정의되는지, 어떻게 변화되는지, 등 프로그램에서의 메모리 흐름을 완벽히 추종하는 항목이다.

C++ 언어 요소 중에서도 매우 심화되는 내용으로써 이것 역시 필수적으로 알아야하는 내용이 아니다. 굳이 알아야 한다면 오히려 메타 프로그래밍보다는 일반화 프로그래밍 단계에서 필요한 내용이다. 일반화 프로그래밍에서 &, const&, &&의 구분, 리터럴 표현, 임시 객체를 이용한 최적화가 필요한 경우 조금 공부해도 손해는 없다.

7.1. lvalue

Left Referenced Value
  • 이름이 있는 값.
  • const가 아니라면 수정할 수 있는 값.
  • 생성자나 함수에 이름을 전달하면 한정자 없는 경우, &, const& 순으로 오버로딩을 우선시함
  • 임시 객체의 함수에서 반환된 경우 &&로 취급
  • 이름이 있는 객체의 함수에서 반환된 경우 & 또는 const& 로 취급
  1. 문자열 리터럴
  2. 변수와 상수
  3. 람다 표현식이 아닌 함수의 식별자
  4. 참조형을 반환하는 함수 구문
  5. (const) & 배열의 인덱스 참조 구문
  6. static_cast<T&>, static_cast<const T&>와 같이 좌측값 참조 형변환 구문

이름(식별자)이 있고 이에 따라 주소도 갖고 있는 값이다.

7.1.1. 문자열 리터럴

#!syntax cpp
void Function1(const char* string);

void Function2(const char* const& string);

template<size_t Length>
void Function3(const char (&string)[Length]);

void Function4(auto string);

int main()
{
    auto str1 = "wasd"; // 문자열 리터럴은 반드시 const char*
    const auto str2 = "ijkl"; // const char* const
    constexpr auto str3 = "zxcv"; // const char* const&

    Function1("Hello, world! (1)"); // string은 null로 끝나는 문자열
    Function2("Hello, world! (2)"); // string은 null로 끝나는 문자열
    Function3("Hello, world! (3)"); // string은 char의 크기가 18인 문자열
    Function4("Hello, world! (4)"); // string은 null로 끝나는 문자열, const char*

    return 0;
}
C++의 문자열 리터럴은 lvalueconst (&char)[Size]로 연역된다. 컴파일러에 따라 다르지만, 문자열 리터럴은 실제로 변수에 전달되기 전에는 메모리에 존재하지 않는 값이다. 또는 최적화를 위해 프로그램의 문자열 리터럴을 한데 모은 문자열 풀(Pool)을 구성하고 가져다 쓰기도 한다. C언어 까지는 char[Size] 또는 여기서 연역된 char*였으나, 이는 실제로 수정할 수 없는 리터럴에 대한 오해를 사게 만들었다. 현재 C++의 문자열 리터럴은 C에서 계승되는 포인터-배열 연역 법칙인 const char*, const&const char*를 받는 const char* const&, 그리고 const lvalueconst char[Size] 혹은 const (&char)[Size]로 나타내진다. 참고로 const char[Size]는 문자열 배열 안의 값은 복사가 일어나진 않지만, 문자열 배열 식별자의 포인터는 복사가 된다. 사소한 사항이고, 성능에 영향도 거의 없지만, 어쨌든 &로 받는 걸 추천한다.

7.2. prvalue

Pure Right Value
* 이름이 없고 메모리 상에도 존재하지 않는 값
* & 연산자로 주소를 얻을 수 없는 값
* 즉시 사용하지 않으면 사라지는 임시 값 [6]
* 다른 이름이 있는 변수에 할당하기 전까지는 수정할 수 없는 값
* 선언만 된 미완성 클래스이면 안됨
* 추상 클래스면 안됨
* 부모 포인터로 자식 포인터를 받을 수 없음
* 참조형 또는 포인터가 아니면 const volatile이 될 수 없음 [7]
* &일 수 없음
* 함수의 인자로 전달하면 const&보다 && 오버로딩을 우선시함
* 함수의 인자로 전달하면 &&로 취급
  1. 문자열을 제외한 모든 리터럴
  2. 람다 표현식, requires 제약조건식
  3. 반환형이 좌측 참조형이 아닌 함수 구문
  4. this
  5. 식별자, 자료형, 열거형, 컨셉트의 이름;
  6. (long)value, static_cast<T>와 같이 참조가 아닌 형변환 구문

숫자, 문자 등 일반적으로 사용되는 리터럴을 의미한다. 그리고 함수안에서 만든 구조체나 클래스의 인스턴스를 반환하는 것도 prvalue다. 가령 함수 안에서 선언한 객체를 반환하는 것도 원래대로라면 참조 대상 소실이 일어나야한다. 그러나 사실은 함수에서 반환하고 난 직후에는 prvalue로 취급하여서, 그 함수의 결과를 변수에 받았을 때 비로소 온전한 객체가 된다.

7.3. xvalue

eXpired Value
* 이름이 없어 메모리 상에만 존재하는 값
* & 연산자로 주소를 얻을 수 없는 값
* 현재 문맥이 끝나면 사라지는 임시 값 [8]
* const가 아니라면 수정할 수 있는 값
* 선언만 된 미완성 클래스일 수 있음
* 추상 클래스일 수 있음
* 부모 포인터로 자식 포인터를 받을 수 있음
* 함수의 인자로 전달하면 &보다 && 오버로딩을 우선시함.
* 함수의 인자로 전달하면 클래스는 &&로 취급, 클래스가 아니면 const&로 취급
  1. 클래스 객체의 멤버 참조 구문
    1. class.member
    2. class[index]
  2. 임시 객체의 멤버 참조 구문
    1. int result = A().Get();
  3. && 배열의 인덱스 참조 구문
  4. std::move, static_cast<T&&>와 같은 우측값 형변환 구문
  5. 문맥을 종료할 수 있는 구문: return, co_return, throw [9]

마찬가지로 사용하지 않으면 사라지는 값은 맞으나, prvalue처럼 식별자를 갖기 전까지는 코드 상에서만 보이는 값이 아닐 수도 있다. 기존에 이름이 있었던 값이나 포인터의 형변환이 포함된다. 가상 클래스나 상속된 클래스 끼리의 형변환도 xvalue로 취급한다.

7.4. glvalue (lvalue & xvalue)

Generalized Left Value

7.5. rvalue (prvalue & xvalue)

Right Value


[1] bool은 논리 자료형을 의미하는 'Boolean'의 줄임말이다.[2] 그러나 C++14 이하의 버전을 사용해야만 하는데 문자열 처리를 구현해야 하면 공부해서 나쁠 건 없다.[3] MSVC[4] 이를 객체(Object)가 아니다라고 한다.[5] 예를 들어 if (handle == NULL) 같은 경우[6] 이 구문이 쓰이지 않으면 최적화될때 사라진다[7] 함수의 반환형이 const int라면 const가 항상 무시되고 int인 것과 같다[8] 이 구문이 쓰이지 않으면 최적화될때 사라진다[9] Return Value Optimization을 위한 조건이다


파일:CC-white.svg 이 문서의 내용 중 전체 또는 일부는 문서의 r247에서 가져왔습니다. 이전 역사 보러 가기
파일:CC-white.svg 이 문서의 내용 중 전체 또는 일부는 다른 문서에서 가져왔습니다.
[ 펼치기 · 접기 ]
문서의 r247 (이전 역사)
문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)