cv 한정자

cv 한정자 (const / volatile)

선언되는 객체가 상수인지 휘발성인지를 지정하기 위해 constvolatile 한정자를 써요. 이들은 어떤 타입 지정자에도 나타날 수 있고, 선언 문법의 decl-specifier-seq에도 포함돼요.

출처: cppreference

본문

선언되는 객체 또는 명명되는 타입의 상수성(constness)이나 휘발성(volatility)을 지정하기 위해 어떤 타입 지정자(선언 문법의 decl-specifier-seq 포함)에도 나타날 수 있어요.

  • const — 타입이 상수(constant)임을 정의해요.
  • volatile — 타입이 휘발성(volatile)임을 정의해요.

설명 (Explanation)

함수 타입이나 참조 타입이 아닌 모든 (아마도 불완전한) 타입은 다음 네 가지 서로 다르지만 관련된 타입의 그룹에 속해요:

  • cv-unqualified 버전
  • const-한정 버전
  • volatile-한정 버전
  • const-volatile-한정 버전

같은 그룹의 이 네 타입은 같은 표현(representation)과 정렬 요구 사항을 가져요.

배열 타입은 요소 타입과 같은 cv 한정을 갖는 것으로 간주돼요.

const 및 volatile 객체

객체가 처음 생성될 때 사용된 cv 한정자(decl-specifier-seq의 일부, 선언의 선언자 일부, 또는 new-expression의 type-id 일부일 수 있음)가 객체의 상수성이나 휘발성을 다음과 같이 결정해요:

  • const 객체
    • 타입이 const-한정된 객체이거나,
    • const 객체의 비mutable 부분 객체예요.

이런 객체는 수정할 수 없어요: 직접 수정하려 하면 컴파일 타임 오류이고, 간접적으로(예: 비-const 타입의 참조나 포인터를 통해 const 객체를 수정) 수정하면 undefined behavior예요.

  • volatile 객체
    • 타입이 volatile-한정된 객체이거나,
    • volatile 객체의 부분 객체이거나,
    • const-volatile 객체의 mutable 부분 객체예요.

volatile-한정 타입의 glvalue 표현식을 통한 모든 접근(읽기 또는 쓰기 연산, 멤버 함수 호출 등)은 최적화 목적에서 가시적인 부수 효과로 취급돼요 (즉 단일 실행 스레드 내에서 volatile 접근은 최적화로 제거되거나 그 volatile 접근보다 시퀀스 지정되거나 그 뒤에 시퀀스 지정되는 다른 가시적 부수 효과와 재정렬될 수 없음. 이는 volatile 객체를 시그널 처리기와의 통신에 적합하게 만들지만 다른 실행 스레드와의 통신에는 부적합하게 해요, std::memory_order 참고). 비-volatile 타입의 glvalue(예: 비-volatile 타입의 참조나 포인터)를 통해 volatile 객체에 접근하려는 시도는 undefined behavior예요.

  • const volatile 객체
    • 타입이 const-volatile-한정된 객체이거나,
    • const volatile 객체의 비mutable 부분 객체이거나,
    • volatile 객체의 const 부분 객체이거나,
    • const 객체의 비mutable volatile 부분 객체예요.

const 객체이면서 volatile 객체인 것처럼 동작해요.

각 cv 한정자(constvolatile)는 어떤 cv 한정자 시퀀스에서도 최대 한 번 나타날 수 있어요. 예를 들어 const constvolatile const volatile은 유효한 cv 한정자 시퀀스가 아니에요.

mutable 지정자

  • mutable — 포함하는 객체가 const로 선언되어 있어도 그렇게 선언된 클래스 멤버의 수정을 허용해요 (즉 그 클래스 멤버는 mutable).

비정적 클래스 멤버 중 비참조 비-const 타입의 선언에 나타날 수 있어요:

class X
{
    mutable const int* p; // OK
    mutable int* const q; // ill-formed
    mutable int&       r; // ill-formed
};

mutable은 그 멤버가 클래스의 외부적으로 보이는 상태에 영향을 주지 않는다는 것을 지정하는 데 쓰여요 (뮤텍스, 메모 캐시, 지연 평가, 접근 계측에 자주 쓰임).

class ThreadsafeCounter
{
    mutable std::mutex m; // The "M&M rule": mutable and mutex go together
    int data = 0;
public:
    int get() const
    {
        std::lock_guard<std::mutex> lk(m);
        return data;
    }
    
    void inc()
    {
        std::lock_guard<std::mutex> lk(m);
        ++data;
    }
};

get()const 멤버 함수라도 data를 읽으려면 뮤텍스를 잠가야 해요. 그 뮤텍스 mmutable로 선언하지 않으면 const 함수 안에서 m을 잠글 수 없어요.

변환 (Conversions)

cv 한정자에는 제한이 증가하는 순서로 부분 순서가 있어요. 타입은 다음보다 더 또는 덜 cv-한정되었다고 말할 수 있어요:

  • unqualified < const
  • unqualified < volatile
  • unqualified < const volatile
  • const < const volatile
  • volatile < const volatile

cv-한정 타입에 대한 참조와 포인터는 더 cv-한정된 타입에 대한 참조와 포인터로 암시적으로 변환될 수 있어요. 자세한 내용은 한정 변환(qualification conversions)을 참고해요.

cv-한정 타입에 대한 참조나 포인터를 덜 cv-한정된 타입에 대한 참조나 포인터로 변환하려면 const_cast를 써야 해요.

참고 (Notes)

비지역 비-volatile 비템플릿(since C++14)비-inline(since C++17) 변수로, extern으로 선언되지 않은 것의 선언에 쓰인 const 한정자는 그 변수에 내부 연결(internal linkage)을 부여해요. 이는 파일 스코프 const 변수가 외부 연결을 갖는 C와 다르다는 점이에요.

C++ 언어 문법은 mutable을 타입 한정자가 아니라 storage-class-specifier로 취급하지만, 저장 클래스나 연결에는 영향을 주지 않아요.

참고 (since)
volatile의 일부 용도는 deprecated된다: 내장 증가/감소 연산자의 피연산자로서의 volatile 타입 lvalue; 내장 직접 대입의 왼쪽 피연산자로서의 volatile 타입 lvalue(직접 대입 표현식이 미평가 문맥이거나 폐기된 값 표현식이 아닌 한); 함수 매개변수 타입이나 반환 타입으로서의 volatile 객체 타입; 구조적 바인딩 선언에서의 volatile 한정자. (since C++20)

키워드 (Keywords)

const, volatile, mutable

예제 (Example)

#include <cstdlib>

int main()
{
    int n1 = 0;          // non-const object
    const int n2 = 0;    // const object
    int const n3 = 0;    // const object (same as n2)
    volatile int n4 = 0; // volatile object
    
    const struct
    {
        int n1;
        mutable int n2;
    } x = {0, 0};        // const object with mutable member
    
    n1 = 1;   // OK: modifiable object
//  n2 = 2;   // error: non-modifiable object
    n4 = 3;   // OK: treated as a side-effect
//  x.n1 = 4; // error: member of a const object is const
    x.n2 = 4; // OK: mutable member of a const object isn't const
    
    const int& r1 = n1; // reference to const bound to non-const object
//  r1 = 2; // error: attempt to modify through reference to const
    const_cast<int&>(r1) = 2; // OK: modifies non-const object n1
    
    const int& r2 = n2; // reference to const bound to const object
//  r2 = 2; // error: attempt to modify through reference to const
//  const_cast<int&>(r2) = 2; // undefined behavior: attempt to modify const object n2
    
    [](...){}(n3, n4, x, r2); // see also: [[maybe_unused]]
    
    std::system("g++ -O3 -Wa,-adhln ./main.cpp"); // may issue asm on POSIX systems
}

가능한 출력 (Possible output):

# typical machine code produced on an x86_64 platform
# (only the code that contributes to observable side-effects is emitted)
main:
    movl    $0, -4(%rsp) # volatile int n4 = 0;
    movl    $3, -4(%rsp) # n4 = 3;
    xorl    %eax, %eax   # return 0 (implicit)
    ret

n2는 const라 직접 수정이 안 되지만, n1은 비-const 객체라서 const_cast로 참조를 통해 수정하는 게 허용돼요. 반대로 정말 const 객체인 n2const_cast로 수정하면 undefined behavior예요. n4의 volatile 대입은 부수 효과로 취급돼서 최적화로 제거되지 않아요.

더 알아보기 (Learn more)

  • C 문서의 const 한정자.
  • C 문서의 volatile 한정자.