C++ 형식 연역, 열거형, 수학 상수, 널포인터, 2진수 표현

Seongcheol Jeon·2024년 11월 30일
0

CPP

목록 보기
28/47
post-thumbnail

연역이란?

이미 알려진 일반적인 원리나 법칙을 바탕으로 구체적인 결론을 도출하는 추리 방법을 말한다.

논리학 용어로는 연역적 추론(deductive reasoning)이라고도 하며, 귀납 논증과 함께 논리학의 두 축을 이룬다.



형식을 추론하는 auto

모던 C++에 추가된 데이터 형식 가운데 가장 많은 변화를 가져온 auto에 대해 알아보자.

모던 C++에서는 형식 추론을 말할 때 형식 연역(type deduction)이라는 단어를 사용한다.

auto는 형식 연역을 할 수 있는 형식 지정자(type specifier)이다. 더 정확하게는 플레이스홀더 형식 지정자(placeholder type specifier)이다. auto는 다른 프로그래밍 언어의 형식 추론과 달리 제약 사항이 많은 편이다. 대표적인 4가지만 보면 다음과 같다.

  • auto 변수는 반드시 선언과 함께 초기화해야 한다.
  • 함수에서 매개변수의 형식 지정자로 사용할 수 없다.
  • 구조체나 클래스의 멤버 변수로 사용할 수 없다.
  • auto 변수를 반환하는 함수는 dctltype을 사용해야 한다.

4가지 제약 사항 모두 형식 연역이 안 되는 상황이다. auto는 형식 연역 키워드이므로 컴파일 시점에 형식을 연력할 수 없거나 모호하면 오류가 발생한다.

이러한 auto는 언제 사용할 수 있을까? 다음의 예를 보자.

#include <iostream>
#include <map>
#include <array>

using std::cout;
using std::endl;

// 맵 선언
std::map<char, std::array<int, 4>> auto_type_example;
std::map<int, std::array<double, 3>> auto_type_example2;

void build_map() {
    std::array<int, 4> array_a = { 1, 2, 3, 4 };
    auto_type_example.insert({ 'a', array_a });

    std::array<double, 3> array_b = { 1.1, 2.2, 3.3 };
    auto_type_example2.insert({ 1, array_b });
}


int main()
{
    build_map();

    std::map<char, std::array<int, 4>>::iterator iter;
    for (iter = auto_type_example.begin(); iter != auto_type_example.end(); iter++) {
        // 반복할 작업
    }

    std::map<int, std::array<double, 3>>::iterator iter2;
    for (iter2 = auto_type_example2.begin(); iter2 != auto_type_example2.end(); iter2++) {
        // 반복할 작업
    }

    // 형식 불일치로 오류
    std::map<int, std::array<int, 4>>::iterator iter3;
    for (iter3 = auto_type_example.begin(); iter3 != auto_type_example.end(); iter3++) {
        // 반복할 작업
    }

    return 0;
}

int와 char를 키로 사용하는 맵을 2개 선언했다. 그리고 맵을 순회하는 for문을 3개 작성했다. 그런데 세 번째 for문에서 오류가 발생한다. auto_type_example의 선언부를 보면 map<char, array<int, 4>>와 같은 형식이지만, 세 번째 for문에서 반복자를 map<int, array<int, 4>>으로 선언했기 때문에 형식 불일치로 오류가 발생한다.

그리고 첫 번째, 두 번째 for문도 오류는 없지만 긴 코드를 작성해야 하고 맵의 선언부에서 정확한 형식을 확인해야 하는 불편함이 있다. 범용성을 위해 어쩔 수 없는 부분이지만 흐름이 복잡해 보이고 가독성도 떨어진다.

auto를 이용하면 이러한 문제를 개선할 수 있다. 오류가 발생한 세 번째 for 문을 auto를 사용해 다음처럼 수정할 수 있다.

auto iter3 = auto_type_example.begin();
for(; iter3 != auto_type_example.end(); iter++)

이렇게 하면 코드를 작성할 때 맵의 선언부에서 데이터 형식을 확인하지 않아도 된다. 또한 범위 기반 for문을 사용하면 다음처럼 한 줄로 간단하게 표현할 수도 있다.

for(auto &element : auto_type_example)

모든 변수를 auto로 선언해도 괜찮은걸까? (데이터 형식을 알아서 판별해주니 편리해서)

auto 키워드가 추가되면서 프로그래밍하기가 훨씬 편리해졌지만, 성능 저하라는 비용을 고려해야 한다. 자료형 추론은 컴파일러 내부에서 많은 연산이 필요하기 때문에 성능 저하가 발생한다.

따라서 auto 키워드는 적절한 곳에서 제한적으로만 사용해야 한다.


형식을 추출하는 decltype

auto 키워드와 마찬가지로 decltype 키워드도 형식 연역을 할 수 있다. auto는 변수 선언과 초기화에 주로 사용되지만, decltype변수 선언뿐만 아니라 함수 템플릿이나 클래스 템플릿에서 반환 값의 형식을 연역하는 데도 사용할 수 있다.

함수에서 반환값의 형식을 연역할 때는 선언문 앞쪽이 아니라 뒤쪽에 표기한다. 이를 후행 반환 형식이라고 한다. 반환값의 형식을 나타내는 자리에 auto 키워드만 사용하면 형식을 연력할 수 없어 오류가 발생한다. 따라서 반드시 후행 반환 형식으로 decltype을 지정해야 한다.

// decltype 반환값 형식 연역
template <typename T, typename TT>
auto mix_template(T t, TT tt) -> decltype(t + u) {
	return t * tt;
}

C++11 부터 추가된 decltype은 가로 안에 작성한 표현식으로 형식을 연역한다. auto와는 다르게 복잡한 표현식에서 형식을 연역할 수 있으며, 레퍼런스(&)상수(const)도 정확하게 추론한다.

decltype 키워드는 템플릿을 활용하는 모던 C++에서 많이 사용하므로 템플릿을 자유자재로 사용하려면 깊게 공부해야 한다.

범위 지정 열거형 enum

열거형C++98에서도 사용되었지만 C++11부터 범위 지정 열거형(scoped enum)이 추가되었다. 기존 열거형도 C++11에서 변경된 부분이 있다.

가장 큰 변화는 열거한 데이터의 형식을 지정할 수 있다는 점이다. 이번 코드와 호환성을 보장하고자 데이터 형식을 지정하지 않으면 정수형으로 자동 지정된다.

범위 지정 열거형을 사용할 때는 선언된 범위를 반드시 지정해 주어야 한다. 범위 미지정 열거형은 선언 범위를 명시하지 않아도 사용할 수 있기 때문에 선언된 곳을 알 수 없을 뿐더러 같은 열거형 식별자를 사용했을 때에 혼란을 줄 수 있다.

// 범위 지정 열거형 선언 ('값 없음', 'b', 'c'로 초기화)
enum class my_enum1 : char {a_type, b_type='b', c_type='c'};

// struct를 활용한 범위 지정 열거형 선언(1, 2, 3으로 초기화)
enum struct my_enum2 {a_type, b_type, c_type);

// 범위 미지정 열거형 선언(1, 2, 10으로 초기화)
enum my_enum3 {a_type, b_type, c_type=10};


// 열거형 변수에 열거형 값 대입
my_enum1 type_definition = my_enum1::a_type;
my_enum2 type_num = my_enum2::b_type;
my_enum3 type_val = c_type;
  • 범위 지정 열거형 선언
    • 열거형을 정의할 때 enum 키워드 다음에 classstruct를 사용하면 범위 지정 열거형, 그렇지 않으면 범위 미지정 열거형이 된다.
  • 범위 미지정 열거형 선언
    • 범위 지정 열거형 선언에서 classstruct를 사용하지 않고 선언한다. 선언 범위(my_enum3::)를 명시하지 않고 사용해도 정상으로 실행된다.
  • 열거형 데이터 형식 지정
    • 열거형 선언 다음에 콜론(:)을 적고 열거한 값의 데이터 형식을 지정한다. 만약 데이터 형식을 생략하면 기본 정수형이 된다
  • 식별자 값 부여
    • 소스 코드에서는 열거한 식별자 자체를 상수로 사용하므로 값을 반드시 지정할 필요는 없다. 값을 지정하면 식별자에 해당 값이 지정되며, 값이 증가할 수 있는 정수형의 경우 1씩 점진적으로 증가한다. 이렇게 선언한 열거현ㅇ은 열거한 값만 가지는 새로운 데이터 형식이 된다. 범위 지정 열거형의 값은 반드시 범위 지정자와 함께 사용해야 한다.

열거형은 주로 분류나 특성을 나타내는 값을 표현할 때 사용한다. 열거형은 C언어에서 사용하던 매크로를 조금 더 구조적으로 사용 범위 등을 제약할 수 있게 만든 것이다.


수학 상수

수학 계산에서 저오학한 상수가 필요하다. 그리고 같은 상수를 모두가 공유할 수 있어야 한다. 만약 상수값이 다르면 모듈마다 계산이 달라 질 수 있다. 특히 과학 분야에서는 작은 수의 차이로도 결과가 완전히 달라질 수 있다. 기존에는 수학 상수를 개인이나 개발 조직에서 정의해서 사용했지만, 모던 C++에서는 기본으로 제공해 누구나 같은 값으로 계산하여 정확도를 높일 수 있게 하였다.

표준 라이브러리가 제공하는 수학 상수를 사용하려면 먼저 다음의 코드를 입력해야 한다.

// 수학 상수 라이브러리 사용 선언
#include <numbers>
using namespace std::numbers;
수학 상수
C++ 정의 상수
정의된 값
자연 상수(e)e2.71828182859045
원주율(pi)pi3.141592653589793
밑이 원주율인 분수(1/pi)inv_pi0.3183098861837907
밑이 원주율의 루트인 분수(1/루트(pi)inv_sqrtpi0.5641895835477563
루트 2sqrt21.4142135623730951
루트 3sqrt31.7320508075688772
밑이 루트 3인 분수(1/루트(3))snv_sqrt30.5773502691896257
2의 자연로그(log 2)ln20.6931471805599453
10의 자연로그(log 10)ln102.302585092994046
오일러 마스케로니 상수egamma0.5772156649015329
황금비phi1.618033988749895

널 포인터 리터럴 nullptr

널 포인터(null pointer)는 C++ 언어에서 중요한 식발자이면서 데이터이다. 포인터에 메모리가 할당되지 않았음을 나타내기도 하고, 빈 값을 나타내기도 한다. 모던 C++ 에서는 널 포인터를 나타내는 키워드로 nullptr을 제공한다.

모던 C++ 이전에는 컴파일러마다 NULL이나 null로 값을 정의해서 사용했다. 기존의 NULL은 특히 (void*)0으로 정의된 경우가 많았다. 따라서 정수나 불리언 등 다양한 데이터 형식으로 변환할 수 있었기 때문에 예상치 못한 버그로 연결되는 경우도 많았다.

하지만 nullptr은 명확하게 널 포인터를 나타내는 리터럴이므로 다른 데이터 형식으로 변환할 수 없다. nullptr을 사용하면 컴파일러 종류가 달라도 가독성을 높일 수 있고 잠재적인 오류 발생을 줄일 수 있다.


2진수 리터럴

모던 C++ 이전까지는 10진수와 16진수를 표현하는 리터럴 형식이 있었다. 16진수는 숫자 앞에 0x0X를 붙였고, 잘 사용되지 않지만 10진수 숫자 앞에 0을 붙였다. 모던 C++에서는 2진수를 표현하는 리터럴을 제공한다. 16진수와 유사하게 숫자 앞에 0b0B를 붙여서 표현한다.

다음은 2진수 0100을 나타낸 표현이다.

int bit_variable = 0b0100;    // int bit_variable = 4;

2진수 리터럴을 사용하면 비트 연산을 위한 변수를 만들고 값을 지정할 때 비트값을 16진수나 10진수로 표현하는 번거로움을 덜 수 있다.

0개의 댓글