티스토리 뷰

※ 주의 사항 ※

  • 이 글의 목적은 '지식의 전달'이 아닌 '학습의 기록'입니다.
  • 따라서 제가 이해하는 그대로의 내용이 포함됩니다.
  • 따라서 이 글은 사실과는 다른 내용이 포함될 수 있습니다.

 

컴퓨터는 내부 회로로 실시간 시계(RTC : Real Time Clock)를 가지고 있어 언제 어느 때라도 필요한 시간을 제공할 수 있다. 그리고 C++17에서는 시간을 다루기 위해 다음 두 가지 방법을 제공하고 있다.

  1. <chrono> 라이브러리, C++11 이후 사용 가능
  2. <ctime> C-style 라이브러리

C-style 라이브러리도 분명 나쁜 건 아니겠지만 나는 <chrono> 라이브러리를 선택해 정리해보았다.

 

chrono 라이브러리의 주요 타입

chrono 라이브러리에서는 다음 세 개의 주요 타입을 정의하고 있다. 이 세 개의 타입만 제대로 이해해도 chrono 라이브러리를 다 이해했다고 할 수 있을 정도로 중요하다. 그렇다고 어렵지도 않으니 지레 겁먹을 필요는 없다.

  1. durations
  2. clocks
  3. time points

durations는 시간 간격을 의미한다. 오후 1시와 오후 1시 42분 사이에는 42분이라는 시간 간격이 존재한다. 이 42분이 duration이 된다.

 

clocks는 시계를 의미한다. 우리가 시간을 알려면 반드시 시계를 봐야 하고 시계에 따라 똑같은 순간을 표현하는 시각도 달라진다. 이게 무슨 말이냐? 오늘은 2022년 2월 6일이다. 하지만 이것은 태양력을 기준으로 한다. 음력을 기준으로 하면 오늘은 2022년 1월 6일이고, 율리우스적일을 기준으로 하면 오늘은 제2459617일이 된다. 똑같은 오늘이지만 태양력이나 음력이냐 율리우스적일(오늘이 음력 며칠인지 찾다가 이런 것도 있다는 걸 알았다)이냐에 따라 오늘을 표현하는 게 달라진다. 즉, 태양력, 음력, 율리우스적일 등이 clocks인 것이다.

 

time points는 어느 한순간의 시점을 의미한다. durations를 설명할 때 언급한 오후 1시나 오후 1시 42분은 time points다. 단순히 '때'를 표현한다.

 

어떤가 생각보다 너무 상식적인 선에서 위의 세 타입이 너무나 잘 이해되지 않는가? 이제 이들을 어떻게 사용할 수 있는지 궁금할 것이다. duration부터 하나씩 살펴보자.

 

durations

duration은 다음과 같이 정의된 클래스 템플릿이다.

template<class Rep, class Period = std::ratio<1>>
class duration;

템플릿 인수로 Rep과 Period가 사용된다. 먼저 이 둘이 무엇인지 설명해야겠다.

 

먼저 rep은 눈금의 수를 저장하는 데이터 타입이다. 아니 잠깐 눈금의 수? 데이터 타입?? 당황하지 말라 period까지 먼저 언급하고 자세히 설명하겠다. period는 눈금의 단위를 의미한다. 자, 우리가 무언가를 측정한다고 생각해보자. 예를 들어 사람의 키를 측정한다. 누군가 홍길동의 키에 대해 다음과 같이 말한다.

 

"홍길동이 키는 173이야."

 

그럼 홍길동의 키는 얼마인가? 1번 173mm, 2번 173cm, 3번 173m, 4번 173km. 당연하게도 2번 173cm를 선택할 것이다. 그럼 1번, 3번, 4번은 왜 아닌가? 똑같은 173인데? 왜냐하면 단위가 다르기 때문이다. 우리는 무언가를 측정하려면 단위가 필요하다. 그리고 그 단위가 몇 개가 있는지 헤아린다. 홍길동의 키 173cm는 길이 1cm인 무언가를 173개 쌓아 올린 것과 같다는 말이다. 여기서 1cm가 바로 눈금의 단위이며, 173개가 눈금의 수가 된다. 이해했는가? 시간도 마찬가지다. 30분이라는 시간은 눈금의 단위가 1분이면 눈금 수는 30개가 되는 것이고, 눈금의 단위가 1초가 되면 눈금 수는 1800개가 된다.

 

duration은 눈금의 수를 rep 타입으로 저장한다. 여기서 rep 타입이라 함은 rep 자체가 새로운 데이터 타입이라는 말이 아니라 우리가 템플릿 인수로서 rep 자리에 넣어준 int, long long 등의 데이터 타입을 의미한다. rep 자리에 int를 넣으면 rep 타입이라는 말은 자연히 int 타입이라는 말이 되는 것이다. 

 

period는 눈금의 단위를 제공하는 std::ratio타입이다. 그리고 std::ratio는 다음과 같이 정의된 클래스 템플릿이다.

template<std::intmax_t Num, std::intmax_t Denom = 1>
class ratio;

이제부터는 period = std::ratio라고 생각해도 된다. 템플릿 인수 중 Num은 분자, Denom은 분모를 의미한다. 그리고 분자/분모(초)를 눈금의 단위로 하겠다는 뜻을 가지고 있다. 이게 무슨 말이냐? 분자가 30이고 분모가 1이라고 가정해보자. 그럼 30초가 되지 않는가? 바로 이 30초를 눈금의 단위로 하겠다는 뜻이다. 분자가 1이고 분모가 1000이라면? 0.001초, 즉, 1 밀리초를 눈금의 단위로 하겠다는 뜻이다. Denom 자리에는 0이나 음수가 들어올 수 없다. 하지만 Num자리에는 음수를 넣는 것이 가능하다. 즉, 눈금의 단위를 음수로 만드는 것도 가능하다는 의미(하지만 이걸 어떤 식으로 써먹는지는 잘 모르겠다).

 

std::ratio를 만들어서 사용할 때는 조심해야 한다. 만약 눈금의 단위를 1 나노초로 하고 싶다면 다음과 같이 만들어서 사용해야 한다. 0이 총 9개가 들어간다.

std::ratio<1, 100000000>

그런데 혹시 눈치챘는가? 위의 ratio는 0이 8개다! 하나하나 새어본 사람은 금방 눈치챘겠지만 앞에서 0이 9개라고 하니 그런가 보다 0이 많이 사용되네하고 넘어간 사람이 더 많을 것으로 조심히 예상해본다. 실제 개발자가 이런 실수를 했다면 경우에 따라 끔찍한 결과도 초래할 수 있다. 벌써부터 std::ratio를 사용하기가 껄끄럽지 않은가? 하지만 이 역시 걱정할 필요는 없다. 사실 개발자들의 편의를 위해 다음과 같이 using 선언문으로 ratio를 재정의 해놓은 것이 많이 있다.

using std::giga = std::ratio<1000000000i64, 1i64>;
using std::mega = std::ratio<1000000i64, 1i64>;
using std::kilo = std::ratio<1000i64, 1i64>;
using std::centi = std::ratio<1i64, 100i64>;
using std::milli = std::ratio<1i64, 1000i64>;
using std::micro = std::ratio<1i64, 1000000i64>;
using std::nano = std::ratio<1i64, 1000000000i64>;

C++레퍼런스를 찾아보면 위에서 소개한 것들보다 더 많이 있다. 다시 한번 우리가 1 나노초를 눈금의 단위로 사용하고 싶을 경우 이제는 std::ratio <1, 1000000000>이 아니라 다음과 같이 사용할 수 있다.

std::nano

 

period의 분자와 분모를 알고 싶은 경우(그럴 일이 있을까 싶지만)엔 다음과 같이 접근할 수 있다.

#include <iostream>
#include <chrono>

int main() {
	std::cout << "std::milli 타입의 분자 : " << std::milli::num << std::endl;
	std::cout << "std::milli 타입의 분모 : " << std::milli::den << std::endl;

	return 0;
}

/*
실행결과

std::milli 타입의 분자 : 1
std::milli 타입의 분모 : 1000
*/

 

rep과 period가 무엇인지 알았으니 이제 duration을 선언해보자. duration은 다음과 같이 선언할 수 있다.

std::chrono::duration<int, std::micro> du1;    //duration 변수 du1 선언

위의 duration은 눈금의 수를 int 타입으로 저장하고, 눈금의 단위는 1 마이크로초이다. 이제 좀 감이 오는가? 정말 감이 좋은 사람이라면 위의 선언이 뭔가 이상하다는 것을 느꼈을지도 모른다. 물론 선언 자체에는 문제가 없다. 그럼 뭐가 이상하단 말인가? 한 번 생각해보자 시간을 1 마이크로초 단위로 측정하게 되면 30분을 표현할 경우 눈금의 수는 몇 개가 되는가? (계산기 두드리는 중)... 무려 1,800,000,000이다. 아니 고작 30분을 저장하는데 10억 8천만?! 참고로 int 타입이 최대로 표현할 수 있는 양수가 얼마인지 아는가? 2,147,483,647이다. 이론대로라면 위에서 정의한 duration이 표현할 수 있는 값은 35분이 최대이다.

 

무엇이 문제인지 단박에 이해가 갔는가? duration은 선언할 때는 period에 맞춰서 rep 타입을 지정해주어야 한다. 보통 시나 분을 단위로 할 경우 int 타입을 사용하지만 초 이하의 단위에서는 long long 타입을 사용한다. 하지만 개발자가 이걸 하나하나 신경 쓰면서 하려면 duration을 사용하기가 얼마나 껄끄럽겠는가? 하지만 걱정하지 말라 duration도 개발자 편의에 맞춰 다음과 같이 using 선언문으로 재정의 한 것이 있다.

using std::chrono::hours = std::chrono::duration<int, std::ratio<3600i64>>
using std::chrono::minutes = std::chrono::duration<int, std::ratio<60i64>>
using std::chrono::seconds = std::chrono::duration<long long>
using std::chrono::milliseconds = std::chrono::duration<long long, std::milli>
using std::chrono::microseconds = std::chrono::duration<long long, std::micro>
using std::chrono::nanoseconds = std::chrono::duration<long long, std::nano>

C++ 레퍼런스를 보면 위에서 소개한 것보다도 많이 있다.

 

아래는 duration을 사용하는 예들을 모아 놓은 것이다. duration에 대한 개념 이해는 이걸로 끝이 났으니 이제 직접 코드를 작성해보면서 사용법을 익히는 것만 남았다.

#include <iostream>
#include <chrono>

int main() {
	//생성자를 이용해서 객체 생성 및 초기화
	std::chrono::minutes m(1);    //60분
	std::chrono::seconds s(60);    //3600초


	//대입 연산자를 사용하면 자동으로 형변환
	std::chrono::microseconds us = m;    //3600000000 마이크로초


	//duration타입을 출력하기 위해 count() 함수 사용
	//count() 함수는 duration이 저장하고 있는 Rep 타입 데이터를 반환
	std::cout << "1분 = " << s.count() << "초" << std::endl;
	std::cout << "1분 = " << us.count() << "마이크로초" << std::endl;


	//데이터의 소실이 발생할 수 있는 형변환은 불가, 보다 작은 단위로의 형변환은 가능하나
	//보다 큰 단위로의 형변환을 하면 데이터가 소실됨
	//std::chrono::hours h = m;
	//std::chrono::milliseconds ms = mcs;


	//duration끼리 산술연산 가능
	std::cout << std::endl;
	std::cout << "s의 값 : " << s.count() << "초" << std::endl;
	s += std::chrono::minutes(1);
	std::cout << "s에 1분을 더한 값 : " << s.count() << "초" << std::endl;


	//보다 큰 단위로 형변환을 하더라도 Rep 타입이 double이므로 데이터의 소실을 발생시키지 않고 형변환 가능
	std::chrono::duration<double, std::ratio<3600>> h2 = s;
	std::cout << std::endl;
	std::cout << "h의 값 : " << h2.count() << "시" << std::endl;


	//duration_cast() 함수를 이용해 duration 변환가능
	s = std::chrono::duration_cast<std::chrono::seconds>(h2);
	std::cout << "s의 값 : " << s.count() << "초" << std::endl;


	//duration 간 논리연산 가능
	std::cout << std::endl;
	std::cout << std::boolalpha << (m > s) << std::endl;


	//몇몇 특별한 값 출력
	std::cout << std::endl;
	std::cout << std::chrono::hours::zero().count() << std::endl;
	std::cout << std::chrono::hours::min().count() << std::endl;
	std::cout << std::chrono::hours::max().count() << std::endl;


	//std::chrono_litersals
	using namespace std::chrono_literals;
	auto day = 24h;
	auto halfHour = 0.5h;
	std::cout << std::endl;
	std::cout << "하루는 " << day.count() << "시간" << std::endl;
	std::cout << "한 시간의 절반은 " << halfHour.count() << "시간" << std::endl;

	return 0;
}

/*
실행결과

1분 = 60초
1분 = 60000000마이크로초

s의 값 : 60초
s에 1분을 더한 값 : 120초

h의 값 : 0.0333333시
s의 값 : 120초

false

0
-2147483648
2147483647

하루는 24시간
한 시간의 절반은 0.5시간
*/

 

clocks

이제 clock에 관해 설명하겠다. clock은 시계가 돌아가기 시작한 시작점이 있고, 그 시작점으로부터 현재까지 쭉 이어온 duration이 있다. 유닉스 시간에 대해 아는가? 유닉스 시간은 1970년 1월 1일 00:00:00을 시작점으로 하여 1초 단위로 계산되어온 시간이다. 이 글을 작성하고 있는 현재는 유닉스 시간으로 1,644,157,338초이다.

 

clocks에는 세 종류의 시계가 존재한다. 세 개 모두 클래스로서 존재한다.

  1. system_clock
  2. steady_clock
  3. high_resolution_clock

각각의 시계가 무엇이 다른지 한 번 알아보자. 먼저 system_clock이다. system_clock은 시스템에 내장되어 있는 실시간 시계다. 1970년 1월 1일 00:00:00을 시작점으로 하고 있다. C++의 시계 중 유일하게 C-style time에 time point를 매핑할 수 있는 시계인데 굳이 이걸 기억하려고 노력할 필요는 없다. 그냥 흘려들어도 좋다. 

 

system_clock은 디폴트로 가지고 있는 rep, period, duration, time point 등의 멤버가 있는데 다음과 같다.

std::chrono::system_clock::rep;           //long long
std::chrono::system_clock::period;        //std::retio<1i64, 10000000i64> = 100 nanoseconds
std::chrono::system_clock::duration;      //std::chrono::duration<std::chrono::system_clock::rep, std::chrono::system_clock::period>
std::chrono::system_clock::time_point;    //std::chrono::time_point<std::chrono::system_clock>
std::chrono::system_clock::is_steady;     //false

is_steady는 상수인데 그냥 이 clock이 steady_clock이냐 아니냐를 반환한다고 생각하면 된다. 아직 system_clock에 대해 알아보고 있으므로 is_steady는 false가 된다. system_clock의 rep, period, duration이 각각 어떤 값을 디폴트로 가지고 있는지 자세히 알아보고 넘어가길 권한다. time_point는 나중에 설명할 것이기 때문에 지금은 이해하지 못한다고 하더라도 그게 당연하다 일단 넘어가고 time_point에 대해 배우고 나서 다시 한번 보면 금방 이해가 될 것이다.

 

clock에 대해서는 설명이 많이 필요하지 않다. 그저 time point를 추출할 때 어떤 시계를 볼 것이냐만 지정해주는 것이기 때문이다. 그리고 이런 특성 때문에 time point와 연관이 좀 많은 것 같다. 사실 clock과 time point 둘 중 어느 것을 먼저 소개해야 할까 고민했는데 그냥 의아할 때마다 왔다 갔다 반복해서 보는 게 답인 것 같다. system_clock의 멤버 함수들에 대해 소개하고 다음 clock으로 넘어가겠다.

#include <iostream>
#include <chrono>

int main() {
	//now() 함수는 호출하는 시점의 std::chrono::system_clock::time_point 반환
	std::chrono::system_clock::time_point current = std::chrono::system_clock::now();    


	//to_time_t() 함수는 system clock time point를 std::time_t 타입으로 변환
	//to_time_t() 함수를 사용하면 자동으로 1초 단위의 눈금 수를 반환
	std::time_t times = std::chrono::system_clock::to_time_t(current);
	std::cout << "times : " << times << std::endl;


	//from_time_t() 함수는 time_t 타입의 데이터를 system clock time point로 변환
	//to_time_t() 함수와는 반대
	std::chrono::system_clock::time_point tp = std::chrono::system_clock::from_time_t(times);

	return 0;
}

/*
실행결과

times : 1644139912
*/

 

이제 steady_clock에 대해 알아보겠다. steady_clock은 물리적인 시간의 진행 방향을 거스르지 않는다. 이게 무슨 말이냐 steady_clock은 시작점 이전의 시간을 표현하지 못한다는 말 같다. 사실 나도 이 개념이 맞는지 잘 모르겠다. 파파고의 한계인가. 아무튼 steady_clock의 주요 특징은 이게 아니다. steady_clock은 눈금의 단위가 고정되어 있어 변하지 않는다. system_clock은 중간에 눈금의 단위를 변경할 수 있다. 그리고 rep을 실수형 타입으로 선언하면 눈금 수를 소수로 저장하여 눈금과 눈금의 사이를 표현하는 것도 가능했다(예를 들면 12초는 1분 단위가 0.2개 이런 식). 하지만 steady_clock은 눈금과 눈금 사이를 표현하는 것이 안 된다(그래도 불편할 건 없다. 필요하면 눈금 단위를 나노초 이하까지도 낮출 수 있으니까).

 

그리고 또 중요한 개념, steady_clock은 실시간 시계와는 아무런 연관이 없다. 즉, 이 시계의 시작점은 명확히 정해져 있는 게 아니라는 얘기다. system_clock은 1970년 1월 1일 00:00:00이라는 명확한 시작점이 있었지만, steady_clock의 시작점은 컴퓨터를 마지막으로 재부팅한 순간이 될 수도 있다. 따라서 steady_clock으로 현재 시각을 알아내는 것은 불가능하다. 그래서 보통 steady_clock은 서로 다른 두 time point를 추출하여 그 사이의 duration을 구할 때 사용한다.

 

steady_clock도 자신만의 디폴트 rep, period, duration, time point 등을 멤버로 가지고 있는데 다음과 같다.

std::chrono::steady_clock::rep;           //long long
std::chrono::steady_clock::period;        //std::nano
std::chrono::steady_clock::duration;      //std::chrono::nanoseconds
std::chrono::steady_clock::time_point;    //std::chrono::time_point<std::chrono::steady_clock>
std::chrono::steady_clock::is_steady;     //true

system_clock의 것과는 조금 다른 것이 있으니 잘 구별하기 바란다. 다음은 steady_clock의 멤버 함수를 소개하고 steady_clock에 대한 설명을 마치겠다. system_clock은 now() 함수 외에도 to_time_t() 함수나 from_time_t() 함수가 더 있었지만 system_clock을 제외한 다른 clock들은 now() 함수만 가지고 있다.

#include <iostream>
#include <chrono>

int main() {
	auto steadyClock = std::chrono::time_point_cast<std::chrono::seconds>(std::chrono::steady_clock::now());
	auto systemClock = std::chrono::time_point_cast<std::chrono::seconds>(std::chrono::system_clock::now());

	std::cout << "steadyClock : " << steadyClock.time_since_epoch().count() << std::endl;
	std::cout << "systemClock : " << systemClock.time_since_epoch().count() << std::endl;

	return 0;
}

/*
실행결과

steadyClock : 1928287
systemClock : 1644141663    //steady clock과 system clock의 시작점이 달라서
                            //서로 다른 값이 출력됨
*/

 

high_resolution_clock은 사실 모르고 넘어가도 좋다. 왜냐하면 이 시계는 서로 다른 표준 라이브러리마다 구현이 일관되지 않아 사용하지 않는 것을 권고하고 있기 때문이다. 그래도 개념 정도는 알고 가도 좋지 않을까? 하는 사람들이 있다면 조금 설명해보겠다. 이 시계는 매우 매우 작은 눈금 단위를 가질 수 있다. 이게 끝이다. 엥? 사실 나는 이게 무슨 의미가 있나 싶다. 왜냐하면 system_clock이나 steady_clock도 attoseconds(1E-18초, nanoseconds * nanoseconds)를 눈금 단위로 가질 수 있는데?? 근데 우리가 그렇게까지 세밀하게(세밀한 수준을 넘어섰지 이건;) 시간을 갖고 놀 일이 있을까??

 

time points

time point는 어느 한 시점을 의미한다고 했지만 사실은 어느 clock의 시작점으로부터의 duration이다. 그렇다고 time point를 선언해놓고 duration처럼 사용할 수 있다는 말은 아니지만 개념 자체는 time point도 duration이다. system_clock의 경우 시작점은 1970년 1월 1월 00:00:00이고 다음과 같이 system_clock의 now() 함수로 현재 time_point를 추출하면 다음과 같이 된다.

#include <iostream>
#include <chrono>

int main() {
	std::cout << std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()) << std::endl;

	return 0;
}

/*
1644161020
*/

즉, 현재 시점이 1644161020초라는 것인데, 이는 곧 1970년 1월 1일 00:00:00으로부터 현재까지의 duration도 된다. time point도 결국 duration이라는 말이 이해가 되었으리라 생각한다.

 

time point는 다음과 같이 정의된 클래스 템플릿이다.

template<class Clock, class Duration = typename Clock::duration>
class time_point;

현재 시간을 알기 위해서는 시계를 봐야 하듯이 time_point를 선언할 때는 템플릿 인수로 clock을 제공해주어야 한다. time_point는 clock, rep, period, duration를 멤버로 가지고 있는데 clock은 템플릿 인수로 제공한 clock과 같고, rep, period, duration은 clock의 것과 같다.

using Clock = std::chrono::system_clock;

std::chrono::time_point<Clock>::clock;       //std::chrono::system_clock
std::chrono::time_point<Clock>::duration;    //std::chrono::system_clock::duration
std::chrono::time_point<Clock>::rep;         //std::chrono::system_clock::rep
std::chrono::time_point<Clock>::period;      //std::chrono::system_clock::period

 

time point도 그다지 설명이 많이 필요한 것은 아니므로 time point와 관련한 함수들의 예를 소개하고 설명을 마치겠다.

#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <chrono>
#include <ctime>

int main() {
	auto p0 = std::chrono::time_point<std::chrono::system_clock>();
	auto p1 = std::chrono::system_clock::now();
	auto p2 = p1 - std::chrono::hours(24);


	//std::ctime() 함수는 유닉스 타임을 서식에 맞게 출력하고 줄바꿈
	std::time_t epoch = std::chrono::system_clock::to_time_t(p0);
	std::cout << "epoch : " << std::ctime(&epoch);   //std::ctime() 함수는 유닉스 타임을 서식에 맞게 출력하고 줄바꿈
	std::time_t today = std::chrono::system_clock::to_time_t(p1);
	std::cout << "today : " << std::ctime(&today) << std::endl;


	//time_since_epoch()는 clock's epoch로부터의 duration을 반환하는 멤버 함수
	auto d1 = std::chrono::duration_cast<std::chrono::hours>(p1.time_since_epoch());
	auto d2 = std::chrono::duration_cast<std::chrono::hours>(p2.time_since_epoch());
	std::cout << "hours since epoch : " << d1.count() << std::endl;
	std::cout << "yesterday hours since epoch : " << d2.count() << std::endl;


	//std::chrono::time_point_cast()는 time point의 duration을 변환하는 비 멤버 함수
	//clock은 동일하게 유지
	auto p3 = std::chrono::time_point_cast<std::chrono::minutes>(p1);
	std::cout << "minutes since epoch : " << p3.time_since_epoch().count() << std::endl;

	return 0;
}

/*
epoch : Thu Jan  1 09:00:00 1970
today : Sun Feb  6 17:55:21 2022

hours since epoch : 456704
yesterday hours since epoch : 456680
minutes since epoch : 27402295
*/
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2024/12   »
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
글 보관함