이미 알려진 일반적인 원리나 법칙을 바탕으로 구체적인 결론을 도출하는 추리 방법을 말한다.
논리학 용어로는 연역적 추론(deductive reasoning)이라고도 하며, 귀납 논증과 함께 논리학의 두 축을 이룬다.
모던 C++
에 추가된 데이터 형식 가운데 가장 많은 변화를 가져온 auto
에 대해 알아보자.
모던 C++
에서는 형식 추론을 말할 때형식 연역(type deduction)
이라는 단어를 사용한다.
auto
는 형식 연역을 할 수 있는 형식 지정자(type specifier)
이다. 더 정확하게는 플레이스홀더 형식 지정자(placeholder type specifier)
이다. auto
는 다른 프로그래밍 언어의 형식 추론과 달리 제약 사항이 많은 편이다. 대표적인 4가지만 보면 다음과 같다.
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
키워드는 적절한 곳에서 제한적으로만 사용해야 한다.
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++
에서 많이 사용하므로 템플릿을 자유자재로 사용하려면 깊게 공부해야 한다.
열거형
은 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
키워드 다음에 class
나 struct
를 사용하면 범위 지정 열거형
, 그렇지 않으면 범위 미지정 열거형
이 된다.범위 미지정 열거형 선언
class
나 struct
를 사용하지 않고 선언한다. 선언 범위(my_enum3::)
를 명시하지 않고 사용해도 정상으로 실행된다.열거형 데이터 형식 지정
콜론(:)
을 적고 열거한 값의 데이터 형식
을 지정한다. 만약 데이터 형식을 생략하면 기본 정수형
이 된다식별자 값 부여
범위 지정 열거형
의 값은 반드시 범위 지정자
와 함께 사용해야 한다.열거형
은 주로 분류나 특성을 나타내는 값을 표현할 때 사용한다. 열거형은 C
언어에서 사용하던 매크로
를 조금 더 구조적으로 사용 범위 등을 제약할 수 있게 만든 것이다.
수학 계산에서 저오학한 상수가 필요하다. 그리고 같은 상수를 모두가 공유할 수 있어야 한다. 만약 상수값이 다르면 모듈마다 계산이 달라 질 수 있다. 특히 과학 분야에서는 작은 수의 차이로도 결과가 완전히 달라질 수 있다. 기존에는 수학 상수를 개인이나 개발 조직에서 정의해서 사용했지만, 모던 C++
에서는 기본으로 제공해 누구나 같은 값으로 계산하여 정확도를 높일 수 있게 하였다.
표준 라이브러리가 제공하는 수학 상수
를 사용하려면 먼저 다음의 코드를 입력해야 한다.
// 수학 상수 라이브러리 사용 선언
#include <numbers>
using namespace std::numbers;
수학 상수 | C++ 정의 상수 | 정의된 값 |
---|---|---|
자연 상수(e) | e | 2.71828182859045 |
원주율(pi) | pi | 3.141592653589793 |
밑이 원주율인 분수(1/pi) | inv_pi | 0.3183098861837907 |
밑이 원주율의 루트인 분수(1/루트(pi) | inv_sqrtpi | 0.5641895835477563 |
루트 2 | sqrt2 | 1.4142135623730951 |
루트 3 | sqrt3 | 1.7320508075688772 |
밑이 루트 3인 분수(1/루트(3)) | snv_sqrt3 | 0.5773502691896257 |
2의 자연로그(log 2) | ln2 | 0.6931471805599453 |
10의 자연로그(log 10) | ln10 | 2.302585092994046 |
오일러 마스케로니 상수 | egamma | 0.5772156649015329 |
황금비 | phi | 1.618033988749895 |
널 포인터(null pointer)
는 C++ 언어에서 중요한 식발자이면서 데이터이다. 포인터에 메모리가 할당되지 않았음을 나타내기도 하고, 빈 값을 나타내기도 한다. 모던 C++ 에서는 널 포인터를 나타내는 키워드로 nullptr
을 제공한다.
모던 C++ 이전에는 컴파일러마다 NULL
이나 null
로 값을 정의해서 사용했다. 기존의 NULL
은 특히 (void*)0
으로 정의된 경우가 많았다. 따라서 정수나 불리언 등 다양한 데이터 형식으로 변환할 수 있었기 때문에 예상치 못한 버그로 연결되는 경우도 많았다.
하지만 nullptr
은 명확하게 널 포인터를 나타내는 리터럴
이므로 다른 데이터 형식으로 변환할 수 없다. nullptr
을 사용하면 컴파일러 종류가 달라도 가독성을 높일 수 있고 잠재적인 오류 발생을 줄일 수 있다.
모던 C++ 이전까지는 10진수와 16진수를 표현하는 리터럴 형식이 있었다. 16진수
는 숫자 앞에 0x
나 0X
를 붙였고, 잘 사용되지 않지만 10진수
숫자 앞에 0
을 붙였다. 모던 C++에서는 2진수
를 표현하는 리터럴을 제공한다. 16진수와 유사하게 숫자 앞에 0b
나 0B
를 붙여서 표현한다.
다음은 2진수 0100을 나타낸 표현이다.
int bit_variable = 0b0100; // int bit_variable = 4;
2진수 리터럴을 사용하면 비트 연산을 위한 변수를 만들고 값을 지정할 때 비트값을 16진수나 10진수로 표현하는 번거로움을 덜 수 있다.