티스토리 뷰
16-3. 타입을 알려주는 키워드 decltype 와 친구 std::declval
1. decltype
C++11에서 도입된 decltype 키워드는 식의 타입을 알아내는데 사용하는 키워드이다.
decltype(/* 타입을 알고자 하는 식*/)
기본적인 사용 예시:
#include <iostream>
struct A {
double d;
};
int main() {
int a = 3;
decltype(a) b = 2; // int
int& r_a = a;
decltype(r_a) r_b = b; // int&
int&& x = 3;
decltype(x) y = 2; // int&&
A* aa;
decltype(aa->d) dd = 0.1; // double
}
2. 값 카테고리
C++에서 모든 식(expression)에는 두 가지 정보가 따라다닌다:
- 식의 타입
- 값 카테고리
값 카테고리는 크게 세 가지로 나뉜다:
- lvalue
- 정체를 알 수 있고 이동시킬 수 없는 값
- 예: 변수, 함수 이름, 문자열 리터럴
- prvalue
- 정체를 알 수 없지만 이동시킬 수 있는 값
- 예: 리터럴(문자열 제외), 산술 연산 결과
- xvalue
- 정체를 알 수 있고 이동시킬 수 있는 값
- 예: std::move() 결과
3. decltype의 타입 추론 규칙
decltype은 전달받은 식에 따라 다음과 같은 규칙으로 타입을 결정한다:
- 식별자 표현식의 경우: 해당 식의 타입 그대로
- 그 외의 경우:
- xvalue면 T&&
- lvalue면 T&
- prvalue면 T
예시:
int a, b;
decltype(a + b) c; // int (a + b는 prvalue)
decltype((a)) d; // int& ((a)는 lvalue)
4. std::declval 사용하기
std::declval은 생성자 호출 없이 타입의 멤버 함수 타입을 알아낼 때 유용하다
#include <utility>
template <typename T>
decltype(std::declval<T>().f()) call_f_and_return(T& t) {
return t.f();
}
struct A {
int f() { return 0; }
};
struct B {
B(int x) {} // 기본 생성자 없음
int f() { return 0; }
};
int main() {
A a;
B b(1);
call_f_and_return(a); // OK
call_f_and_return(b); // OK - declval 덕분에 가능
}
5. deltype의 유용한 사용 사례
1. 정확한 타입 복사:
const int i = 4;
auto j = i; // int
decltype(i) k = i; // const int
2. 배열 타입 보존:
int arr[10];
auto arr2 = arr; // int*
decltype(arr) arr3; // int[10]
3. 템플릿 함수의 리턴 타입 추론:
template <typename T, typename U>
auto add(T t, U u) -> decltype(t + u) {
return t + u;
}
C++14부터는 함수의 리턴 타입을 auto로 지정하면 컴파일러가 자동으로 추론해주므로 더 간단하게 작성할 수 있다.
17-1. type_traits 라이브러리, SFINAE, enable_if
1. 템플릿 메타 함수와 type_traits
템플릿 메타 함수는 값을 다루는 일반 함수와 달리 타입을 다룬다는 점이 특징이다.
#include <iostream>
#include <type_traits>
template <typename T>
void tell_type() {
if (std::is_void<T>::value) {
std::cout << "T 는 void!\n";
} else {
std::cout << "T 는 void 가 아니다.\n";
}
}
int main() {
tell_type<int>(); // void 아님!
tell_type<void>(); // void!
}
위 코드에서 std::is_void는 주어진 타입이 void인지 판별하는 메타 함수이다.
실제로는 템플릿 특수화로 구현되어 있다. 모든 타입에 대해 value가 false가 되지만, void 타입에 대해 특수화하여 value를 true로 설정한다.
template <typename T>
struct is_void {
static constexpr bool value = false;
};
template <>
struct is_void<void> {
static constexpr bool value = true;
};
2. type_traits의 다양한 메타 함수
C++ 표준 라이브러리 type_traits는 타입을 분석하거나 조작하는 여러 메타 함수를 제공한다. 몇 가지 대표적인 메타 함수는 다음과 같다.
- std::is_integral
타입이 정수형인지 판단한다
#include <iostream>
#include <type_traits>
std::cout << std::is_integral<int>::value; // 1 (true)
std::cout << std::is_integral<float>::value; // 0 (false)
- std::is_class
타입이 클래스인지 확인한다.
namespace detail {
template <typename T>
char test(int T::*);
struct two { char c[2]; };
template <typename T>
two test(...);
}
template <typename T>
struct is_class
: std::integral_constant<bool, sizeof(detail::test<T>(0)) == 1> {};
std::is_class의 구현 방식은 독특한데, 데이터 멤버를 가리키는 포인터 문법을 이용한다. 클래스에서만 사용 가능한 문법을 통해 클래스를 판별하는 방식이다.
- std::is_integral_v, std::is_class_v (C++17 이후)
value를 직접 사용하는 대신 _v 접미사를 붙여 간단하게 사용할 수 있다.
3. SFINAE (치환 오류는 컴파일 오류가 아니다)
C++에서 템플릿 인자를 치환할 때 문법적으로 잘못된 경우 SFINAE 규칙에 따라 컴파일 오류를 발생시키지 않고 해당 함수가 오버로딩 후보에서 제외된다.
#include <iostream>
template <typename T>
void test(typename T::x a) {
std::cout << "T::x 호출됨\n";
}
template <typename T>
void test(typename T::y b) {
std::cout << "T::y 호출됨\n";
}
struct A { using x = int; };
struct B { using y = int; };
int main() {
test<A>(33); // T::x 호출됨
test<B>(22); // T::y 호출됨
}
위 코드에서 T::x와 T::y 중 문법적으로 유효한 것만 오버로딩 후보로 남게 된다. SFINAE는 템플릿 메타프로그래밍의 핵심 규칙 중 하나이다.
4. enable_if 로 조건부 템플릿 정의
enable_if는 특정 조건이 참일 때에만 템플릿을 활성화할 수 있도록 돕는다. 다음은 정수형 타입만 인자로 받는 함수의 예이다.
#include <iostream>
#include <type_traits>
template <typename T,
typename = typename std::enable_if<std::is_integral<T>::value>::type>
void test(const T& t) {
std::cout << "t: " << t << "\n";
}
int main() {
test(1); // OK
test('c'); // OK
// test(3.14); // 컴파일 오류
}
enable_if의 첫 번째 인자는 조건식이고, 두 번째는 조건이 참일 경우 활성화할 타입이다. 조건이 거짓이면 type이 정의되지 않으므로 SFINAE에 의해 제외된다.
5. void_t로 코드 간소화하기
C++17에서 추가된 void_t는 여러 조건식을 한 번에 처리하는 데 유용하다. 예를 들어 특정 컨테이너의 begin과 end가 존재하는지 확인하고 싶을 때 다음과 같이 작성할 수 있다.
#include <iostream>
#include <type_traits>
template <typename Cont,
typename = std::void_t<decltype(std::declval<Cont>().begin()),
decltype(std::declval<Cont>().end())>>
void print(const Cont& container) {
for (const auto& elem : container) {
std::cout << elem << " ";
}
std::cout << "\n";
}
int main() {
std::vector<int> v = {1, 2, 3};
print(v); // [ 1 2 3 ]
}
void_t는 주어진 조건이 모두 유효할 경우 void로 평가되며, 조건 중 하나라도 잘못되면 해당 템플릿 함수는 오버로딩 후보에서 제외된다.
17-2. C++ 정규 표현식(<regex>) 라이브러리 소개
1. 전체 문자열 매칭하기: std::regex_match
정규 표현식을 사용해 문자열 전체가 특정 패턴과 일치하는지 확인할 수 있다. 예를 들어, 서버 로그 파일 이름이 **db-123-log.txt**와 같은 형식인지 확인하려면 다음과 같이 작성할 수 있다.
#include <iostream>
#include <regex>
#include <vector>
int main() {
std::vector<std::string> file_names = {"db-123-log.txt", "db-124-log.txt",
"not-db-log.txt", "db-12-log.txt",
"db-12-log.jpg"};
std::regex re("db-\\d*-log\\.txt");
for (const auto& file_name : file_names) {
std::cout << file_name << ": " << std::boolalpha
<< std::regex_match(file_name, re) << '\n';
}
}
실행 결과:
db-123-log.txt: true
db-124-log.txt: true
not-db-log.txt: false
db-12-log.txt: true
db-12-log.jpg: false
**std::regex_match**는 문자열 전체가 정규 표현식과 일치할 경우에만 true를 반환한다. 여기서 사용된 std::regex 객체는 패턴을 정의하며, 정규 표현식 문법은 기본적으로 ECMAScript 표준을 따른다.
2. 문자열 일부 검색하기: std::regex_search
문자열의 일부가 특정 패턴에 부합하는지 검색하려면 std::regex_search를 사용할 수 있다. 예를 들어, HTML 코드에서 특정 태그를 추출하려면 다음과 같이 작성한다.
#include <iostream>
#include <regex>
int main() {
std::string html = R"(
<div class="sk-circle1 sk-circle">a</div>
<div class="sk-circle2 sk-circle">b</div>
<div class="sk-circle3 sk-circle">asd</div>
)";
std::regex re(R"(<div class="sk[\w -]*">[\w]*</div>)");
std::smatch match;
while (std::regex_search(html, match, re)) {
std::cout << match.str() << '\n';
html = match.suffix(); // 검색된 패턴 뒤의 문자열로 업데이트
}
}
실행 결과:
<div class="sk-circle1 sk-circle">a</div>
<div class="sk-circle2 sk-circle">b</div>
<div class="sk-circle3 sk-circle">asd</div>
std::regex_search는 문자열의 일부분이 정규 표현식과 매칭되면 true를 반환하며, 매칭된 결과를 std::smatch 객체에 저장한다.
3. 문자열 치환하기: std::regex_replace
특정 패턴에 맞는 문자열을 다른 문자열로 치환하려면 std::regex_replace를 사용한다. 예를 들어, sk-circle1 같은 문자열을 1-sk-circle로 변환하려면 다음과 같이 작성한다.
#include <iostream>
#include <regex>
int main() {
std::string html = R"(
<div class="sk-circle1 sk-circle">a</div>
<div class="sk-circle2 sk-circle">b</div>
<div class="sk-circle3 sk-circle">asd</div>
)";
std::regex re(R"(sk-circle(\d))");
std::string modified_html = std::regex_replace(html, re, "$1-sk-circle");
std::cout << modified_html;
}
실행결과:
<div class="1-sk-circle">a</div>
<div class="2-sk-circle">b</div>
<div class="3-sk-circle">asd</div>
여기서 사용된 $1은 정규 표현식의 캡쳐 그룹으로, (\d)에 매칭된 숫자를 나타낸다. 치환 문자열에서 캡쳐 그룹을 활용하면 정교한 변환이 가능하다.
캡쳐 그룹과 중첩된 치환
캡쳐 그룹을 중첩해 더 복잡한 패턴을 처리할 수도 있다. 예를 들어, <div class="sk-circle1 sk-circle">를 <div class="1-sk-circle">로 변경하려면 다음과 같이 작성한다.
#include <iostream>
#include <regex>
int main() {
std::string html = R"(
<div class="sk-circle1 sk-circle">a</div>
<div class="sk-circle2 sk-circle">b</div>
<div class="sk-circle3 sk-circle">asd</div>
)";
std::regex re(R"((sk-circle(\d) sk-circle))");
std::string modified_html = std::regex_replace(html, re, "$2-sk-circle");
std::cout << modified_html;
}
실행 결과:
<div class="1-sk-circle">a</div>
<div class="2-sk-circle">b</div>
<div class="3-sk-circle">asd</div>
중첩된 캡쳐 그룹에서 괄호가 열리는 순서대로 $1, $2 등이 할당된다.
17-3. 난수 생성(<random>)과 시간 관련 라이브러리(<chrono>) 소개
1. 난수 생성: <random>
C 스타일의 rand()는 난수 생성에 있어 여러 단점이 존재한다:
- 시드값의 제한적 변경: srand(time(NULL))은 같은 초에 실행되는 프로그램에서 동일한 난수열을 생성한다.
- 비균등한 난수 생성: rand() % 100 방식은 특정 값에 치우친 결과를 낼 수 있다.
- 낮은 품질의 난수: rand()는 선형 합동 생성기를 사용하여 상관 관계가 높은 난수를 생성한다.
C++에서는 <random> 라이브러리를 통해 더 나은 품질의 난수를 생성할 수 있다.
#include <iostream>
#include <random>
int main() {
std::random_device rd; // 진짜 난수를 제공하는 random_device
std::mt19937 gen(rd()); // Mersenne Twister 엔진 초기화
std::uniform_int_distribution<int> dis(0, 99); // 0~99 균등 분포
for (int i = 0; i < 5; i++) {
std::cout << "난수 : " << dis(gen) << std::endl;
}
}
실행 결과:
난수 : 42
난수 : 73
난수 : 15
난수 : 88
난수 : 7
- std::random_device: 운영체제 기반의 진정한 난수를 제공한다. 느리기 때문에 초기화에만 사용한다.
- std::mt19937: 고성능 Mersenne Twister 엔진으로, rand()보다 품질이 우수하다.
- std::uniform_int_distribution: 특정 범위 내의 값을 균등하게 생성한다.
2. 다양한 분포 생성
<random>은 균등 분포 외에도 다양한 분포를 제공한다.
정규 분포(Normal distribution) 생성:
#include <iostream>
#include <map>
#include <random>
int main() {
std::random_device rd;
std::mt19937 gen(rd());
std::normal_distribution<> dist(0, 1); // 평균 0, 표준편차 1
std::map<int, int> histogram;
for (int i = 0; i < 10000; i++) {
++histogram[std::round(dist(gen))];
}
for (const auto& p : histogram) {
std::cout << p.first << ": " << std::string(p.second / 100, '*') << '\n';
}
}
실행 결과:
-3: *
-2: ****
-1: **************
0: ********************
1: ***************
2: ***
3: *
정규 분포에서 무작위 샘플을 생성하여 히스토그램 형태로 출력한다.
3. 시간 측정: <chrono>
<chrono>는 다음과 같은 주요 요소로 구성된다:
- clock: 현재 시간을 제공한다. 예: system_clock, high_resolution_clock.
- time_point: 특정 시간을 나타낸다.
- duration: 시간 간격을 나타낸다.
chrono를 사용해 코드 실행 시간을 측정하는 예:
#include <iostream>
#include <chrono>
#include <random>
#include <vector>
int main() {
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution<> dist(0, 1000);
for (int n = 1; n <= 1000000; n *= 10) {
std::vector<int> numbers;
numbers.reserve(n);
auto start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < n; ++i) {
numbers.push_back(dist(gen));
}
auto end = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
std::cout << n << "개 난수 생성 시간: " << duration.count() << "us" << std::endl;
}
}
실행 결과:
1개 난수 생성 시간: 1us
10개 난수 생성 시간: 2us
100개 난수 생성 시간: 15us
1000개 난수 생성 시간: 150us
10000개 난수 생성 시간: 1200us
- high_resolution_clock: 매우 정밀한 시계로, 성능 측정에 적합하다.
- duration_cast: 시간 차이를 원하는 단위(마이크로초, 밀리초 등)로 변환한다.
- count(): 시간 차이 값을 반환한다.
4. 현재 시간 가져오기
현재 시간을 날짜와 시간 형식으로 출력하려면:
#include <iostream>
#include <chrono>
#include <ctime>
#include <iomanip>
int main() {
auto now = std::chrono::system_clock::now();
std::time_t t = std::chrono::system_clock::to_time_t(now);
std::cout << "현재 시간: " << std::put_time(std::localtime(&t), "%F %T") << '\n';
}
실행 결과:
현재 시간: 2024-11-18 13:45:12
std::put_time은 날짜와 시간을 형식화하여 출력한다. %F, %T 등은 strftime과 동일한 형식을 따른다.
17-4
17-5
.
'개발 > C++' 카테고리의 다른 글
[ 씹어먹는 C++ ] 유니폼 초기화, constexpr (2) | 2024.10.27 |
---|---|
[ 씹어먹는 C++ ] 쓰레드 풀 만들기 (0) | 2024.10.26 |
[ 씹어먹는 C++ ] C++의 멀티스레딩 (0) | 2024.10.13 |
[ 씹어먹는 C++ ] 함수 객체 (0) | 2024.10.12 |
[ 씹어먹는 C++ ] 스마트 포인터 (unique_ptr/ shared_ptr/ weak_ptr) (0) | 2024.09.22 |
- Total
- Today
- Yesterday
- unorderedset
- SpinLock
- NotFoundException: String resource ID #0x0
- 지크슈
- 광유다
- 포톤
- mutex
- semaphore
- registerForActivityResult
- 게임개발
- 스핀락
- unityAR
- photon
- ARface
- 바이너리세마포
- StartActivityForResult
- 세마포
- 유니티
- list
- 유니티슈팅게임
- 뮤텍스
- dependencyResilutionManagement
- Vector
- 동기화
- Unity
- map
- Java
- 안드로이드스튜디오
- C++
- unorderedmap
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | ||||
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |