LearnCPP - 5

Justin·2026년 2월 13일

LearnCPP.com

목록 보기
4/22
post-thumbnail

상수 (Constants)

  • 프로그래밍에서 상수는 프로그램 실행 중에 변경할 수 없는 값을 의미합니다.
  • C++는 두 가지 유형의 상수를 지원합니다.

이름 있는 상수 (Named constants)

  • 식별자와 연결된 상수 값입니다.
  • 때때로 심볼릭 상수(symbolic constants)라고도 불립니다.

리터럴 상수 (Literal constants)
식별자와 연결되지 않은 상수 값입니다.


이름 있는 상수의 종류

  • C++에서 이름 있는 상수를 정의하는 방법은 세 가지가 있습니다.
  • 상수 변수가 가장 흔하게 사용되는 이름 있는 상수입니다.
  • 1. 상수 변수 (Constant variables)
  • 2. 치환 텍스트가 있는 객체 유사 매크로
  • 3. 열거형 상수

상수 변수 (Constant variables)

  • 상수 변수는 초기화된 후에는 값을 변경할 수 없는 변수를 말합니다.

상수 변수 선언하기

  • 상수 변수를 선언하려면 객체의 타입 옆에 const 키워드를 붙이면 됩니다.
  • Const 변수는 정의할 때 반드시 초기화해야 하며, 그 이후에는 대입을 통해 값을 변경할 수 없습니다.
  • 함수 매개변수const 키워드를 사용하여 상수로 만들 수 있습니다.
  • 함수 매개변수를 상수로 만들면 함수 내부에서 매개변수의 값이 변경되지 않도록 컴파일러의 도움을 받을 수 있습니다.
  • 하지만 현대 C++에서는 값 매개변수굳이 const 로 만들지 않습니다.
  • 왜냐하면 함수가 매개변수의 값을 바꾸더라도 호출자에게는 영향을 주지 않기 때문입니다. 함수 종료 시 파괴되는 복사본일 뿐이니까요.
  • 함수의 반환값의 경우에도, 값으로 const 객체를 반환하는 것은 별 의미가 없습니다. 어차피 파괴될 임시 복사본이기 때문입니다.
  • 또한 const 값을 반환하면 이동 시맨틱과 관련된 특정 컴파일러 최적화를 방해하여 성능 저하를 일으킬 수 있습니다.

변수를 상수로 만들어야 하는 이유

  • 버그 발생 가능성을 줄입니다.
  • 컴파일러 최적화 기회를 제공합니다.
  • 프로그램의 전체적인 복잡성을 줄여줍니다.
  • 코드를 분석하거나 디버깅할 때, const 변수는 값이 변하지 않는다는 것을 알기 때문에 값이 실제로 변하는지, 어떤 값으로 변하는지, 그 값이 올바른지 걱정할 필요가 없습니다.

대체 텍스트가 있는 객체형 매크로 (Object-like macros)

  • 챕터 2에서 대체 텍스트가 있는 객체형 매크로에 대해 논의했습니다.
#include <iostream>

#define MY_NAME "Alex"

int main()

{

    std::cout << "My name is: " << MY_NAME << '\n';

    return 0;

}
  • 전처리기가 이 코드가 포함된 파일을 처리할 때, MY_NAMEAlex로 대체합니다.
  • MY_NAME은 이름이고 대체 텍스트는 리터럴 상수의 문자열 리터럴이므로, 이는 이름 있는 상수의 한 형태가 됩니다.

그렇다면 왜 이름 있는 상수로 대체 텍스트가 있는 객체형 매크로를 사용하면 안 될까요?

  • 적어도 세 가지의 문제가 있습니다.

    1. 가장 큰 문제는 매크로가 일반적인 C++ 스코프(scoping) 규칙을 따르지 않는다는 점입니다.
      매크로가 한 번 #defined 되면, 해당 파일의 나머지 부분에서 나오는 모든 매크로 이름이 치환됩니다.
      다른 곳에서 같은 이름을 변수명 등으로 사용하고 있다면 원치 않는 치환이 발생하여 이상한 컴파일 오류로 이어질 수 있습니다.
    1. 매크로를 사용한 코드는 디버깅하기가 더 어렵습니다.
    1. 매크로 치환은 C++의 다른 문법들과 다르게 동작하므로 부주의한 실수를 하기가 쉽습니다.

타입 한정자 (Type qualifiers)

  • 타입 한정자는 타입에 적용되어 그 타입의 동작 방식을 수정하는 키워드입니다.
  • const 키워드가 바꾸는 것은 수정 가능 여부 입니다.
  • 상수 변수를 선언할 때 사용하는 constconst 타입 한정자(const type qualifier) 라고 합니다.
  • C++23 기준으로, C++에는 두 가지 타입 한정자만 존재합니다. const volatile

선택적 읽기 (Optional reading)

  • volatile 한정자는 객체의 값이 언제든지(컴파일러가 예측할 수 없는 방식으로) 변경될 수 있음을 컴파일러에게 알리는 데 사용됩니다.
  • 거의 사용되지 않는 이 한정자는 특정 유형의 최적화를 비활성화합니다.
  • 기술 문서에서는 constvolatile 한정자를 종종 cv-qualifiers(cv-한정자)라고 부릅니다.

리터럴 접미사 (Literal suffixes)

  • 리터럴의 기본 타입이 원하는 타입이 아니라면, 접미사(suffix)를 추가하여 리터럴의 타입을 변경할 수 있습니다.
  • 다음은 자주 사용되는 접미사들입니다.
데이터 타입접미사의미
정수형u 또는 Uunsigned int
정수형l 또는 Llong
정수형ul, uL, Ul, UL, lu, lU, Lu, LUunsigned long
정수형ll 또는 LLlong long
정수형ull, uLL, Ull, ULL, llu, llU, LLu, LLUunsigned long long
정수형z 또는 Zstd::size_t의 부호 있는(signed) 버전 (C++23)
정수형uz, uZ, Uz, UZ, zu, zU, Zu, ZUstd::size_t (C++23)
부동 소수점f 또는 Ffloat
부동 소수점l 또는 Llong double
문자열sstd::string
문자열svstd::string_view

문자열 리터럴 (String literals)

  • 프로그래밍에서 문자열은 텍스트를 표현하는 데 사용되는 순차적인 문자의 집합입니다.
  • C 언어로부터 물려받았기 때문에 종종 C 문자열또는 C 스타일 문자열이라고 불립니다.
  • 모든 C 스타일 문자열 리터럴에는 암시적인 널 종결자(implicit null terminator)가 있습니다.
  • C 스타일 문자열 리터럴프로그램 시작 시 생성되어 프로그램 전체 실행 기간 동안 존재함이 보장되는 const 객체입니다.
  • C 스타일 문자열 리터럴과 달리, std::stringstd::string_view 리터럴은 임시 객체를 생성합니다.
  • 이 임시 객체들은 생성된 전체 표현식(full expression)이 끝날 때 파괴되므로 즉시 사용되어야 합니다.

8진수(Octal)와 16진수(Hexadecimal) 리터럴

  • 8진수 리터럴을 사용하려면 리터럴 앞에 0을 접두사로 붙여야 합니다.
  • 8진수는 거의 사용되지 않으므로, 사용을 피하는 것을 권장합니다.
int x{ 012 };
  • 16진수 리터럴을 사용하려면 리터럴 앞에 0x를 접두사로 붙여야 합니다.
int x{ 0xF };

2진수 표현에 16진수 사용하기 (Using hexadecimal to represent binary)

  • 16진수 한 자리는 16개의 서로 다른 값을 가질 수 있으므로, 16진수 한 바퀴는 4비트(1111)를 포괄한다고 말할 수 있습니다.
  • 결과적으로, 16진수 두 자리 쌍을 사용하면 1바이트(8비트)를 정확하게 표현할 수 있습니다.
16진0123456789ABCDEF
2진수0000000100100011010001010110011110001001101010111100110111101111
  • 2진수 값 0011 1010 0111 1111 1001 1000 0010 0110을 가진 32비트 정수를 생각해 봅시다.
  • 길이와 숫자의 반복 때문에 읽기가 쉽지 않습니다.
  • 16진수로는 이 값을 3A7F 9826으로 표현할 수 있으며, 이는 훨씬 간결합니다.
  • 이러한 이유로 16진수 값은 메모리 주소메모리 상의 원시 데이터(raw data)를 표현하는 데 자주 사용됩니다.

2진수 리터럴 (Binary literals)

  • C++14 이전에는 2진수 리터럴에 대한 지원이 없었습니다.
  • 하지만 16진수 리터럴이 유용한 대안을 제공했습니다.
#include <iostream>

int main()
{
    int bin{};        // 16비트 int라고 가정

    bin = 0x0001;     // 변수에 2진수 0000 0000 0000 0001 할당
    bin = 0x0002;     // 변수에 2진수 0000 0000 0000 0010 할당
    bin = 0x0004;     // 변수에 2진수 0000 0000 0000 0100 할당
    bin = 0x0008;     // 변수에 2진수 0000 0000 0000 1000 할당
    bin = 0x0010;     // 변수에 2진수 0000 0000 0001 0000 할당
    bin = 0x0020;     // 변수에 2진수 0000 0000 0010 0000 할당
    bin = 0x0040;     // 변수에 2진수 0000 0000 0100 0000 할당
    bin = 0x0080;     // 변수에 2진수 0000 0000 1000 0000 할당
    bin = 0x00FF;     // 변수에 2진수 0000 0000 1111 1111 할당
    bin = 0x00B3;     // 변수에 2진수 0000 0000 1011 0011 할당
    bin = 0xF770;     // 변수에 2진수 1111 0111 0111 0000 할당

    return 0;
}
  • C++14부터는 0b 접두사를 사용하여 직접 2진수 리터럴을 사용할 수 있습니다.
#include <iostream>

int main()
{
    int bin{};        // 16비트 int라고 가정

    bin = 0b1;        // 변수에 2진수 0000 0000 0000 0001 할당
    bin = 0b11;       // 변수에 2진수 0000 0000 0000 0011 할당
    bin = 0b1010;     // 변수에 2진수 0000 0000 0000 1010 할당
    bin = 0b11110000; // 변수에 2진수 0000 0000 1111 0000 할당

    return 0;
}

자릿수 구분자 (Digit separators)

  • 긴 리터럴은 읽기 어려울 수 있기 때문에, C++14에서는 작은따옴표 ' 를 자릿수 구분자로 사용하는 기능이 추가되었습니다.
  • 자릿수 구분자는 순수하게 시각적인 용도이며 리터럴 값에는 아무런 영향을 주지 않습니다.
int bin { 0b1011'0010 };
long value { 2'132'673'462 };

10진수, 8진수, 16진수로 값 출력하기

  • 기본적으로 C++은 값을 10진수로 출력합니다.
  • 하지만 std::dec std::oct std::hex 입출력 조정자를 사용하여 출력 형식을 변경할 수 있습니다.
  • 한 번 적용된 입출력 조정자는 다시 변경될 때까지 향후 출력에 계속 적용된다는 점에 유의하세요.
  • std::dec 10진수
  • std::oct 8진수
  • std::hex 16진수
#include <iostream>

int main()
{
    int x{ 12 };

    std::cout << x << '\n';                // 10진수 (기본값)
    std::cout << std::hex << x << '\n';    // 16진수
    std::cout << x << '\n';                // 이제 16진수로 출력됨
    std::cout << std::oct << x << '\n';    // 8진수
    std::cout << std::dec << x << '\n';    // 다시 10진수로 복귀
    std::cout << x << '\n';                // 10진수

    return 0;
}

실행 결과:  

12
c
c
14
12
12

2진수로 값 출력하기

  • std::cout에는 2진수 출력 기능이 내장되어 있지 않기 때문에, 2진수로 값을 출력하는 것은 조금 더 어렵습니다.
  • 다행히 C++ 표준 라이브러리에는 이를 대신해 줄 std::bitset이라는 타입이 있습니다.
  • std::bitset을 사용하려면 std::bitset 변수를 정의하고 저장할 비트 수를 알려주어야 합니다.
  • 비트 수는 반드시 컴파일 시간 상수여야 합니다.

컴파일 시간 상수(compile-time constant)
프로그램을 실행하기 전, 즉 컴파일(번역)하는 순간에 값이 이미 확정되어 있는 상수를 의미합니다.
std::bitset은 정수형 값(10진수, 8진수, 16진수, 2진수 등 모든 형식 포함)으로 초기화할 수 있습니다.

#include <bitset>   // std::bitset 사용을 위해
#include <iostream>

int main()
{
    // std::bitset<8>은 8비트를 저장하겠다는 의미입니다
    std::bitset<8> bin1{ 0b1100'0101 }; // 2진수 1100 0101에 대한 2진수 리터럴
    std::bitset<8> bin2{ 0xC5 };        // 2진수 1100 0101에 대한 16진수 리터럴

    std::cout << bin1 << '\n' << bin2 << '\n';
    std::cout << std::bitset<4>{ 0b1010 } << '\n'; // 임시 std::bitset을 생성하고 출력

    return 0;
}

실행 결과:
  
11000101
11000101
1010

// 위 코드에서 다음 줄은:
std::cout << std::bitset<4>{ 0b1010 } << '\n'; 
// 임시 std::bitset 객체를 생성해서 바로 출력하는 코드

Format / Print 라이브러리를 사용해 2진수 출력하기

  • C++20과 C++23에서는 새로운 Format 라이브러리(C++20)와 Print 라이브러리(C++23)를 통해 2진수를 출력하는 더 좋은 옵션을 제공합니다.
  • #include <format> // C++20
  • #include <print> // C++23
#include <format>   // C++20
#include <iostream>
#include <print>    // C++23

int main()
{
    std::cout << std::format("{:b}\n", 0b1010);     // C++20, {:b}는 인자를 2진수로 출력
    std::cout << std::format("{:#b}\n", 0b1010);    // C++20, {:#b}는 인자를 0b 접두사 포함 2진수로 출력
    std::println("{:b} {:#b}", 0b1010, 0b1010);     // C++23, 두 인자를 포맷팅하여 출력

    return 0;
}

실행 결과:  
  
1010
0b1010
1010 0b1010

최적화 (Optimization) 소개

  • 프로그래밍에서 최적화는 소프트웨어가 더 효율적으로 작동하도록 수정하는 과정을 말합니다.
  • 어떤 종류의 최적화는 일반적으로 수작업으로 이루어집니다.
  • 프로파일러(profiler)라고 불리는 프로그램을 사용하여 프로그램의 다양한 부분이 실행되는 데 걸리는 시간을 확인하고, 어떤 부분이 전체 성능에 영향을 미치는지 파악할 수 있습니다.
  • 그러면 프로그래머는 이러한 성능 문제를 완화할 방법을 찾을 수 있습니다.
  • 수동 최적화는 시간이 오래 걸리기 때문에, 프로그래머들은 보통 큰 영향을 미치는 고수준의 개선(더 성능이 좋은 알고리즘 선택, 데이터 저장 및 접근 방식 최적화, 리소스 사용량 감소, 작업 병렬화 등)에 집중합니다.
  • 다른 종류의 최적화는 자동으로 수행될 수 있습니다.
  • 다른 프로그램을 최적화하는 프로그램을 최적화기(optimizer, 옵티마이저)라고 합니다.
  • 현대 C++ 컴파일러들은 최적화 컴파일러(optimizing compiler)입니다.
  • 즉, 컴파일 과정의 일부로 프로그램을 자동으로 최적화할 수 있는 능력이 있습니다.
  • 전처리기와 마찬가지로, 이러한 최적화는 소스 코드 파일을 직접 수정하는 것이 아니라, 컴파일 과정의 일부로서 투명하게 적용됩니다.

as-if 규칙 (The as-if rule)

  • C++에서 컴파일러는 프로그램을 최적화할 수 있는 많은 재량권을 가집니다.
  • as-if 규칙은 컴파일러가 프로그램의 관찰 가능한 동작(observable behavior)에 영향을 주지 않는 한, 더 최적화된 코드를 생성하기 위해 프로그램을 원하는 대로 수정할 수 있다는 규칙입니다.

컴파일 시간 평가 (Compile-time evaluation)

  • 현대 C++ 컴파일러는 특정 표현식을 런타임이 아닌 컴파일 시간에 완전히 또는 부분적으로 평가할 수 있는 능력이 있습니다.
  • 컴파일러가 표현식을 컴파일 시간에 완전히 또는 부분적으로 평가하는 것을 컴파일 시간 평가라고 합니다.

상수 접기 (Constant folding)

  • 상수 접기는 컴파일러가 리터럴 피연산자를 가진 표현식을 그 결과값으로 대체하는 최적화 기법입니다.
  • 상수 접기는 전체 표현식이 런타임에 실행되어야 하는 경우라도, 부분 표현식(subexpression)에 적용될 수 있습니다.
#include <iostream>

int main()
{
    std::cout << 3 + 4 << '\n';
    return 0;
}
  • 위 예제에서 3 + 4는 전체 표현식 std::cout << 3 + 4 << '\n';부분 표현식입니다.
  • 컴파일러는 이를 std::cout << 7 << '\n';으로 최적화할 수 있습니다.

상수 전파 (Constant propagation)

  • 상수 전파는 컴파일러가 상수 값을 갖는 것으로 알려진 변수를 그 값으로 대체하는 최적화 기법입니다.
  • 상수 전파는 그 결과를 다시 상수 접기로 최적화할 수 있는 형태로 만들기도 합니다.

죽은 코드 제거 (Dead code elimination)

  • 죽은 코드 제거는 실행될 수는 있지만 프로그램의 동작에 아무런 영향을 미치지 않는 코드를 컴파일러가 제거하는 최적화 기법입니다.

Const 변수는 최적화하기 더 쉽습니다

  • 비상수 변수의 경우 상수 전파 최적화 기법을 적용하기 위해 컴파일러는 비상수 변수의 값이 실제로 변경되지 않았다는 것을 파악해야 합니다.
  • 컴파일러가 이를 수행할 수 있는지 여부는 프로그램의 복잡도와 컴파일러 최적화 루틴의 정교함에 달려 있습니다.
  • 가능한 한 상수 변수(constant variable)를 사용함으로써 우리는 컴파일러가 더 효과적으로 최적화하도록 도울 수 있습니다.

용어 정리: 컴파일 시간 상수 vs 런타임 상수

  • C++에서 상수는 때때로 비공식적인 두 가지 범주로 나뉩니다.
  • 컴파일 시간 상수(compile-time constant)
  • 런타임 상수(runtime constant)

컴파일 시간 상수(compile-time constant)

  • 컴파일 시간에 그 값을 알 수 있는 상수입니다.
  • 리터럴 (Literals) 초기화 식(initializer)이 컴파일 시간 상수인 상수 객체

런타임 상수(runtime constant)

  • 런타임 컨텍스트에서 값이 결정되는 상수입니다.
  • 상수 함수 매개변수 초기화 식이 비상수이거나 런타임 상수인 상수 객체
  • 다음은 예시 입니다.
#include <iostream>

int five()
{
    return 5;
}

int pass(const int x) // x는 런타임 상수입니다
{
    return x;
}

int main()
{
    // 다음은 상수가 아닙니다(non-constants):
    [[maybe_unused]] int a{ 5 };

    // 다음은 컴파일 시간 상수입니다:
    [[maybe_unused]] const int b{ 5 };
    [[maybe_unused]] const double c{ 1.2 };
    [[maybe_unused]] const int d{ b };      // b는 컴파일 시간 상수입니다

    // 다음은 런타임 상수입니다:
    [[maybe_unused]] const int e{ a };      // a는 비-상수(non-const)입니다
    [[maybe_unused]] const int f{ e };      // e는 런타임 상수입니다
    [[maybe_unused]] const int g{ five() }; // 반환 값은 런타임이 될 때까지 알 수 없습니다
    [[maybe_unused]] const int h{ pass(5) };// 반환 값은 런타임이 될 때까지 알 수 없습니다

    return 0;
}
  • 실전에서 이러한 용어들을 접하게 되겠지만, C++에서 이 정의들은 그리 유용하지 않습니다.
  • 어떤 런타임 상수(심지어 비상수 변수까지도)는 as-if 규칙에 따라 최적화 목적을 위해 컴파일 시간에 평가될 수 있습니다.
  • 어떤 컴파일 시간 상수는 컴파일 시간 기능에 사용될 수 없습니다.

상수 표현식 (Constant expressions)

  • 이 코드는 프로그램이 실행되기 전 컴파일 단계에, 컴파일러가 미리 계산해서 정답을 확정 지을 수 있는가?
  • 이 질문에 "네!"라고 답할 수 있다면 상수 표현식, "아니오, 프로그램이 켜져서 실행해 봐야 알아요"라고 한다면 비상수(런타임) 표현식입니다.

상수 표현식
컴파일러가 코드를 번역할 때 "아, 이건 굳이 나중에 실행할 때까지 기다릴 필요 없이 내가 지금 바로 계산해서 결과값으로 덮어써도 되겠다"라고 판단할 수 있는 것들입니다.

  • 리터럴 (5, 1.2) 값 자체가 고정되어 있으니 당연히 미리 알 수 있습니다.
  • 리터럴끼리의 연산 (3 + 4) 컴파일러가 미리 계산해서 그냥 7로 바꿔버립니다. 프로그램 실행 속도가 미세하게 빨라지겠죠.
  • constexpr 변수/함수 프로그래머가 아예 컴파일러에게 "이건 무조건 컴파일할 때 미리 계산해 놔!"라고 도장을 찍어둔 것입니다.
  • const int x { 5 }; C++의 오래된 규칙 때문에, 정수형(int 등)에 한해서만 const를 상수 표현식으로 특별 대우해 줍니다.

비상수 / 런타임 표현식
컴파일러가 코드를 보면서 "음... 이건 프로그램이 실제로 켜지고 돌아가 봐야 값을 알 수 있겠군. 지금은 계산 포기!"라고 선언하는 것들입니다.

  • 일반 변수 (int x = 5;) 언제 코드가 개입해서 값을 바꿀지 모르기 때문에 컴파일러가 섣불리 미리 계산할 수 없습니다.
  • std::cout << "hello" 화면에 글자를 띄우는 행위는 프로그램이 '실행'되어 모니터와 운영체제와 소통해야만 일어날 수 있는 일입니다.
  • new, delete 메모리를 동적으로 빌리고 반납하는 것은 프로그램이 실행 중에 운영체제에 요청해야 하는 작업입니다
  • const double d { 1.2 }; (const 비정수형) 앞서 말한 const int는 봐주면서 const double은 안 봐주는 것은 순전히 과거 C++의 역사적인 잔재(관습) 때문입니다. 실수형이나 다른 타입은 복잡성 때문에 과거 컴파일러들이 미리 계산하는 걸 지원하지 않았기 때문입니다.

대략적인 요약

  • const 처음에 값을 한 번 넣었으면, 프로그램이 끝날 때까지 절대 못 바꾼다! 눈으로 보기만 해라!" (값이 언제 정해졌든 상관없이, 일단 정해지면 자물쇠를 채워버립니다.)
  • 상수 표현식(constant expression) 컴파일러가 코드를 쓱 보고 '아, 이건 굳이 실행 안 해봐도 답이 뭔지 딱 알지' 하고 미리 계산해 둘 수 있는 수식.
  • constexpr 컴파일러야, 프로그램 실행될 때까지 미루지 말고, 무조건 지금 당장(컴파일할 때) 계산해서 완벽한 고정값(상수 표현식)으로 박아버려!
구분의미값이 컴파일 타임에 확정?대표 용도
const수정 금지(불변)아닐 수도 있고, 맞을 수도 있음API 안정성, const-correctness
상수 표현식컴파일 타임에 계산 가능한 “식”배열 크기, 템플릿 인자, case, static_assert
constexpr상수 표현식이 될 수 있게 강제/보장예 (변수는 강제)컴파일 타임 계산, 성능/안전성, 템플릿

컴파일 타임 const의 문제점 (The compile-time const challenge)

  • 초기값이 상수 표현식인 정수형 const 변수는 상수 표현식에서 사용할 수 있습니다.
  • 그 외의 모든 const 변수는 상수 표현식에서 사용할 수 없습니다.
  • 하지만 const를 사용하여 상수 표현식용 변수를 만드는 데는 몇 가지 문제가 있습니다.
  • const를 사용하는 것만으로는 해당 변수가 상수 표현식에서 사용 가능한지 즉각적으로 알기 어렵습니다.
const int d { someVar }; // d가 상수 표현식에 사용 가능한지 불분명함
const int e { getValue() }; // e가 상수 표현식에 사용 가능한지 불분명함
  • const는 컴파일러에게 "이 변수는 반드시 상수 표현식에서 사용 가능해야 한다"고 알리는(그리고 그렇지 않을 경우 컴파일을 중단시키는) 방법을 제공하지 않습니다. 대신, 조건이 맞지 않으면 조용히 런타임 표현식에서만 쓸 수 있는 변수를 생성해 버립니다.
  • 컴파일 타임 상수 변수를 만들기 위해 const를 사용하는 것은 정수형이 아닌 변수에는 적용되지 않습니다. 하지만 정수형이 아닌 변수도 컴파일 타임 상수로 만들고 싶을 때가 많습니다.

constexpr 키워드 (The constexpr keyword)

  • constexpr 변수는 항상 컴파일 타임 상수입니다.
  • 따라서 constexpr 변수는 반드시 상수 표현식으로 초기화되어야 하며, 그렇지 않으면 컴파일 오류가 발생합니다.

변수에서 const와 constexpr의 의미 비교

  • const 객체의 값이 초기화 이후에 변경될 수 없음을 의미합니다.
  • 초기값은 컴파일 타임에 알 수도 있고, 런타임에 알 수도 있습니다.
  • constexpr 객체가 상수 표현식에서 사용될 수 있음을 의미합니다.
  • 초기값은 반드시 컴파일 타임에 알려져야 합니다.
  • constexpr 변수는 암시적으로 const입니다.
  • 반면 const 변수는 암시적으로 constexpr이 아닙니다
  • const와 달리 constexpr객체의 타입에 포함되지 않습니다.
  • 따라서 constexpr int로 정의된 변수의 실제 타입은 const int입니다
  • constexpr이 객체에 암시적으로 const를 부여하기 때문입니다.

모범 사례 (Best practice)

  • 초기값이 상수 표현식인 모든 상수 변수는 constexpr로 선언하십시오.
  • 초기값이 상수 표현식이 아닌(즉, 런타임 상수인) 모든 상수 변수는 const로 선언하십시오.

용어 정리 (Nomenclature recap)

용어 (Term)정의 (한국어)
컴파일 타임 상수 (Compile-time constant)컴파일 타임에 값이 반드시 알려져야 하는 값 또는 수정 불가능한 객체 (예: 리터럴, constexpr 변수)
constexpr (Constexpr)객체를 컴파일 타임 상수로 선언하는 키워드(그리고 컴파일 타임에 평가될 수 있는 함수를 선언). 비공식적으로는 “상수 표현식(constant expression)”의 약어처럼 쓰이기도 함
상수 표현식 (Constant expression)컴파일 타임 상수와 컴파일 타임 평가를 지원하는 연산자/함수만 포함하는 표현식(즉, 컴파일 타임에 계산 가능한 식)
런타임 표현식 (Runtime expression)상수 표현식이 아닌 표현식(즉, 실행 중에 계산되는 식)
런타임 상수 (Runtime constant)값이 바뀌지 않는(수정 불가능한) 값/객체이지만, 컴파일 타임 상수는 아닌 것(값이 런타임에 확정되는 상수)

constexpr 함수 간단 소개

  • constexpr 함수는 입력값과 사용되는 위치가 완벽한 상수 조건을 갖추면 컴파일할 때 미리 계산되고, 조건이 하나라도 어긋나서 런타임 변수가 섞여 들어오면 일반 함수처럼 유연하게 런타임에 실행되는 함수입니다.

무조건 컴파일 타임에 실행되는 경우 (사전 계산)

  • 다음 두 가지 조건이 모두 맞으면, 컴파일러는 무조건 실행 전에 계산을 끝내버립니다.
    1. 함수에 넘겨주는 재료(인자)들이 전부 컴파일 타임에 알 수 있는 값(상수 표현식)이어야 합니다.
    2. 함수의 결과값이 반드시 컴파일 타임에 필요한 곳(예: constexpr 변수에 값을 넣을 때, 배열의 크기를 정할 때)에 쓰여야 합니다.

일반 함수처럼 런타임에 실행되는 경우 (실시간 계산)

  • 함수에 넘겨주는 재료(인자)가 런타임에 변하는 일반 변수이거나, 굳이 컴파일 타임에 계산을 강제하지 않는 일반적인 상황에서 쓰이면, 이 함수는 평범한 일반 함수처럼 프로그램 실행 중에(런타임에) 작동합니다.
#include <iostream>

int max(int x, int y) // 이것은 non-constexpr 함수입니다.
{
    if (x > y)
        return x;
    else
        return y;
}

constexpr int cmax(int x, int y) // 이것은 constexpr 함수입니다.
{
    if (x > y)
        return x;
    else
        return y;
}

int main()
{
    int m1{ max(5, 6) };              // 가능
    const int m2{ max(5, 6) };        // 가능
    constexpr int m3{ max(5, 6) };    // 컴파일 에러: max(5, 6)은 상수 표현식이 아님

    int m4{ cmax(5, 6) };             // 가능: 컴파일 타임 또는 런타임에 평가될 수 있음
    const int m5{ cmax(5, 6) };       // 가능: 컴파일 타임 또는 런타임에 평가될 수 있음
    constexpr int m6{ cmax(5, 6) };   // 가능: 반드시 컴파일 타임에 평가되어야 함

    return 0;
}

std::getline()을 사용하여 텍스트 입력받기

  • std::getline()은 두 개의 인자가 필요합니다. std::cin 문자열 변수
int main()

{
    std::cout << "Enter your full name: ";
    std::string name{};
    std::getline(std::cin >> std::ws, name); // 텍스트 한 줄 전체를 name으로 읽음

    std::cout << "Enter your favorite color: ";
    std::string color{};
    std::getline(std::cin >> std::ws, color); // 텍스트 한 줄 전체를 color로 읽음

    std::cout << "Your name is " << name << " and your favorite color is " << color << '\n';
    return 0;
}

std::ws란 도대체 무엇인가요?

  • C++는 입력을 원하는 방식대로 다룰 수 있게 도와주는 입력 조작자 기능을 제공하며, 그중 대표적인 것이 바로 std::ws입니다.
  • 본격적인 값을 읽어들이기 전에, 입력 버퍼 맨 앞에 있는 선행 공백들을 깔끔하게 건너뛰어 줍니다.

std::string의 길이

  • std::string에 문자가 몇 개 있는지 알고 싶다면, std::string 객체에 길이를 물어볼 수 있습니다.
  • 이를 수행하는 문법은 이전에 본 것과는 다르지만 꽤 간단합니다.
int main()
{
    std::string name{ "Alex" };
    std::cout << name << " has " << name.length() << " characters\n";
    return 0;
}
  • 문자열 길이를 length(name)과 같이 묻는 대신 name.length()라고 쓴다는 점을 주목하세요.
  • 이것은 std::string 내부에 중첩된 멤버 함수(member function) 라고 불리는 특별한 종류의 함수입니다.
  • length() 멤버 함수는 std::string 내부에 선언되어 있기 때문에 문서에서는 때때로 std::string::length()로 표기되기도 합니다.
  • std::string::length()는 unsigned 전용 타입(size_t)을 반환하므로, 이를 부호가 있는 일반 int 변수에 그냥 대입하면 타입 불일치로 인해 컴파일러 경고가 발생합니다.
  • 따라서 이 경고를 방지하고 안전하게 값을 담기 위해, static_cast<int>()를 사용하여 "내가 의도한 변환이다"라고 명시적으로 타입을 바꿔주는 것입니다.
int length { static_cast<int>(name.length()) };
  • C++20에서는 std::ssize() 함수를 사용하여 std::string의 길이를 크기가 큰 부호 있는 정수형으로 얻을 수 있습니다.
std::cout << name << " has " << std::ssize(name) << " characters\n";

std::string 초기화는 비용이 듭니다

  • std::string이 초기화될 때마다, 초기화에 사용된 문자열의 복사본이 만들어집니다.
  • 문자열을 복사하는 것은 비용이 많이 들기 때문에, 복사 횟수를 최소화하도록 주의해야 합니다.

std::string을 값으로 전달하지 마십시오

  • std::string이 함수에 값으로 전달(passed by value)되면, std::string 함수 매개변수는 인자로 인스턴스화되고 초기화되어야 합니다. 이는 비용이 많이 드는 복사를 초래합니다.

std::string을 위한 리터럴

  • 우리는 큰따옴표 문자열 리터럴 뒤에 s 접미사를 사용하여 std::string 타입의 문자열 리터럴을 만들 수 있습니다.
    s반드시 소문자여야 합니다.
int main()
{
    using namespace std::string_literals; // s 접미사에 쉽게 접근하기 위해

    std::cout << "foo\n";   // 접미사가 없으면 C 스타일 문자열 리터럴
    std::cout << "goo\n"s;  // s 접미사가 있으면 std::string 리터럴

    return 0;
}

std::string_view (C++17)

  • std::string의 초기화(또는 복사) 비용이 비싸다는 문제를 해결하기 위해, C++17에서는 std::string_view를 도입했습니다.
  • std::string_view<string_view> 헤더에 존재 합니다.
  • std::string_view는 기존 문자열에 대해 복사본을 만들지 않고 읽기 전용(read-only) 접근을 제공합니다.
  • 읽기 전용이란 보고 있는 값을 접근하고 사용할 수는 있지만, 수정할 수는 없다는 뜻입니다.

std::string_view 리터럴 (Literals)

  • ""으로 감싼 문자열 리터럴은 기본적으로 C 스타일 문자열 리터럴입니다.
  • "" 문자열 리터럴 뒤에 sv 접미사를 사용하여 std::string_view 타입의 리터럴을 만들 수 있습니다.
  • sv는 반드시 소문자여야 합니다.
int main()
{
    using namespace std::string_literals;      // s 접미사 접근용
    using namespace std::string_view_literals; // sv 접미사 접근용

    std::cout << "foo\n";    // 접미사가 없으면 C 스타일 문자열 리터럴
    std::cout << "goo\n"s;   // s 접미사는 std::string 리터럴
    std::cout << "moo\n"sv;  // sv 접미사는 std::string_view 리터럴

    return 0;
}
profile
안녕하세요.

0개의 댓글