References
- C++ Standard Library 2nd
Contents
- Chrono Library
- Durations
- Clocks and Timepoints
- Data and Time Functions by C and POSIX
[C++] chrono 라이브러리 (Date, Time 유틸리티)
예전에 chrono 라이브러리에 대해서 정리한 적이 있는데, 위 내용은 Professional C++ (C++17, C++20을 커버)에서 언급하고 있는 내용을 정리했습니다. 이번에는 C++ Standard Library 2nd(C++11 커버)에서 언급하고 있는 내용들을 정리하면서 리마인드해보고자 합니다.
Overview of the Chrono Library
chrono 라이브러리는 각 시스템마다 타이머와 클럭이 서로 다를 수 있다는 점을 고려하여 처리할 수 있으며 시간 정밀도를 개선할 수 있도록 설계되었습니다. (실제로 POSIX의 time 라이브러리에서 그랬던 것처럼) 10년마다 새로운 시간 데이터 타입을 도입하는 것을 피하기 위해 duration과 point of time('timepoint')라는 개념을 분리하여 precision-neutral 개념을 제공합니다. 따라서, chrono 라이브러리의 핵심은 현재 가리키는 시간(point)과 시간 사이의 기간(duration)을 나타내고 처리하기 위한 추상 메커니즘을 제공하는 데이터 타입 또는 개념이라고 할 수 있습니다.
- duration은 시간 단위에 대한 틱(ticks) 수로 정의됩니다. 예를 들어, '3 분'('분' 기준으로 3틱)이 바로 duration 입니다. 또 다른 예시로, '42 밀리초' 또는 하루를 '86,000 초'로 나타낼 수 있습니다. 이 개념을 사용하면 1/3초의 1.5배와 같은 것을 표현할 수 있습니다. 여기서 1.5배는 시간 단위의 갯수(틱의 갯수)이며, 1/3초는 시간 단위(1틱)를 말합니다.
- timepoint는 시간의 시작(beginning of time; epoch로 불림)과 duration을 결합한 개념입니다. 일반적인 예시로 2000년 새해 첫날 자정으로 표현되는 시간은 '1970년 1월 1일(UNIX와 POSIX 시스템의 시스템 클럭의 시작 시간(epoch))로부터 1,262,300,400초가 지났다'라고 표현할 수 있습니다.
- timepoint라는 개념은 clock으로 파라미터화될 수 있습니다. 이는 timepoint의 epoch를 정의합니다. 따라서, 서로 다른 clock은 다른 epoch를 갖습니다. 일반적으로 다른 두 time-point 간의 duration이나 difference를 처리하는 것과 같은 다중 timepoint를 다루는 연산은 동일한 epoch/clock을 필요로 합니다. 또한, clock은 now라는 timepoint를 반환하는 편리한 함수를 제공합니다.
정리하면, timepoint는 clock으로 정의된 epoch의 이전 또는 이후의 duration으로 정의됩니다.
참고로 chrono 라이브러리의 모든 식별자들은 std::chrono 네임스페이스에 정의되어 있습니다.
Durations
Duration이란 시간 단위의 갯수를 나타내는 값(value)과 초로 표현된 단위를 나타내는 분수(fraction)의 조합입니다. 분수는 ratio 클래스로 표현됩니다.
std::chrono::duration<int> twentySeconds(20);
std::chrono::duration<double, std::ratio<60>> halfAMinute(0.5);
std::chrono::duration<long, std::ratio<1,1000>> oneMillisecond(1);
여기서 첫 번째 템플릿 인자는 틱의 데이터 타입을 정의하고, optional인 두 번째 템플릿 인자는 단위 타입(unit type)을 초로 정의합니다. 따라서 첫 번째 행의 코드는 시간 단위가 초(seconds)이고, 두 번째 코드는 분(60/1 seconds), 세 번째 코드는 밀리초(1/1000 seconds) 입니다.
조금 더 편리하도록, C++ 표준 라이브러리는 아래의 type definitions를 제공합니다.
위에 정의된 타입들을 사용하면 일반적인 시간을 쉽게 지정할 수 있습니다.
std::chrono::seconds twentySeconds(20);
std::chrono::hours aDay(24);
std::chrono::milliseconds oneMillisecond(1);
Arithmetic Duration Operations
duration에 대한 계산은 다음과 같은 방식으로 수행됩니다.
- sum, difference, product, or quotient of two durations
- add or subtract ticks or other durations
- compare two durations
여기서 중요한 점은 연산에 사용되는 두 duration의 단위 타입이 서로 달라도 된다는 것입니다. duration의 common_type<>의 오버로딩이 제공되기 때문에, 결과로 얻은 duration은 두 피연산자의 단위 사이의 최대공약수가 됩니다.
예를 들면, 아래의 두 durations에 대해
chrono::seconds d1(42); // 42 seconds
chrono::milliseconds d2(10); // 10 milliseconds
아래의 표현식에 대한 결과는
d1 - d2
밀리초(1/1000 seconds) 단위의 41,990 ticks의 duration이 됩니다.
또는 조금 더 일반적으로 예를 들면,
chrono::duration<int,ratio<1,3>> d1(1); // 1 tick of 1/3 second
chrono::duration<int,ratio<1,5>> d2(1); // 1 tick of 1/5 second
에 대해 아래의 표현식은
d1 + d2
1/15 second의 8 ticks가 되며, 아래의 표현식은
d1 < d2
false를 나타냅니다.
위의 두 표현식에서 d1은 1/15초의 5틱, d2는 1/15초의 3틱으로 확장됩니다.
암묵적인 형 변환이 가능한 경우, duration을 다른 단위로 변환하는 것도 가능합니다. 따라서, 시간을 초로 변환할 수는 있지만 그 반대는 불가능합니다. 예를 들면, 다음과 같습니다.
std::chrono::seconds twentySeconds(20); // 20 seconds
std::chrono::hours aDay(24); // 24 hours
std::chrono::milliseconds ms(0); // 0 milliseconds
ms += twentySeconds + aDay; // 86,420,000 milliseconds
--ms; // 86,419,999 milliseconds
ms *= 2; // 172,839,998 milliseconds
std::cout << ms.count() << " ms\n";
std::cout << std::chrono::nanoseconds(ms).count() << " ns" << std::endl;
Other Duration Operations
바로 위의 예제 코드에서 count() 멤버 함수를 사용하여 현재 틱의 수를 출력하며, 이는 duration을 위해 제공되는 연산들 중 하나 입니다.
위 표에서 제공되는 멤버 함수나 생성자 등을 보여주고 있습니다.
기본 생성자는 값을 default-initialized 하므로 초기 값은 undefined value 입니다.
위에서 보여주는 멤버들을 사용하면 duration을 출력하기 위한 출력 연산자 <<를 위한 함수를 정의할 수 있습니다.
template<typename V, typename R>
ostream& operator<<(ostream& s, const chrono::duration<V,R>& d)
{
s << "[" << d.count() << " of " << R::num << "/" << R::den << "]";
return s;
}
여기서는 ticks 수를 count()를 사용하여 출력하고, 사용된 시간 단위를 분자(numerator)와 분모(denominator)를 사용하여 출력합니다. 이때, 분자와 분모는 컴파일 시간에 계산되는 ratio 클래스의 멤버입니다.
앞서 살펴봤듯이, 더 정밀한 단위로의 변환은 암묵적으로 가능합니다. 하지만, 더 큰 단위로 변환하는 것은 정보를 잃을 수 있기 때문에 암묵적으로 변환할 수 없습니다. 예를 들어, 42,010 밀리초를 초 단위로 바꾸면 42라는 정수값이 되면서 10밀리초에 대한 정밀도를 잃습니다. 하지만, duration_cast를 사용하여 명시적으로 변환할 수 있습니다.
std::chrono::seconds sec(55);
std::chrono::minutes m1 = sec; // ERROR
std::chrono::minutes m2 = std::chrono::duration_cast<std::chrono::minutes>(sec); // OK
또한, 데이터 타입이 부동소수점인 duration을 정수 타입의 duration으로 변환할 때도 명시적으로 변환해야 합니다.
std::chrono::duration<double,std::ratio<60>> halfMin(0.5);
std::chrono::seconds s1 = halfMin; // ERROR
std::chrono::seconds s2 = std::chrono::duration_cast<std::chrono::seconds>(halfMin); // OK
또 다른 예시로 한 duration을 다른 단위로 분할하는 것을 생각해봅시다. 예를 들어, 아래 코드는 밀리초 단위의 duration을 시간, 분, 초, 밀리초로 분할합니다.
#include <iostream>
#include <iomanip>
#include <chrono>
using namespace std;
using namespace std::chrono;
template<typename V, typename R>
ostream& operator<<(ostream& s, const duration<V,R>& d)
{
s << "[" << d.count() << " of " << R::num << "/" << R::den << "]";
return s;
}
int main()
{
milliseconds ms(7255042);
// split into hours, minutes, seconds, and milliseconds
hours hh = duration_cast<hours>(ms);
minutes mm = duration_cast<minutes>(ms % hours(1));
seconds ss = duration_cast<seconds>(ms % minutes(1));
milliseconds msec = duration_cast<milliseconds>(ms % seconds(1));
// print durations and values:
cout << "raw: " << hh << "::" << mm << "::"
<< ss << "::" << msec << endl;
cout << " " << setfill('0') << setw(2) << hh.count() << "::"
<< setw(2) << mm.count() << "::"
<< setw(2) << ss.count() << "::"
<< setw(3) << msec.count() << endl;
}
위 코드에서 타입을 명시적으로 변환할 때, 단위에 맞지 않는 나머지 값은 버리게 됩니다. 또한, 모듈러 연산을 사용하여 분, 초, 밀리초를 얻고 있습니다.
마지막으로 chrono 클래스는 세 가지 정적 함수를 제공하는데, zero()는 0초의 duration을 반환하고, min()과 max()는 duration이 가질 수 있는 최소, 최대를 나타냅니다.
Clocks and Timepoints
timepoints와 clocks의 관계는 조금 까다롭습니다.
- clock은 epoch와 tick period를 정의합니다. 예를 들어, clock은 UNIX epoch(1970년 1월 1일)에서부터 지금까지 흐른 밀리초 단위의 tick일 수도 있고, 프로그램의 시작에서부터 나노초 단위의 tick일 수도 있습니다. 또한, clock은 이 clock에 따라 명시된 모든 timepoint에 대한 데이터 타입을 제공할 수 있습니다.
clock은 시간에서 현재 포인트에 대한 객체를 반환하는 now() 함수를 제공합니다. - timepoint는 주어진 clock에서의 positive or negative duration이 지난 특정 포인트를 나타냅니다. 따라서, 만약 '10일'이라는 duration과 1970년 1월 1일이라는 epoch가 만나면, 이 timepoint는 1970년 1월 10일을 나타냅니다.
timepoint의 인터페이스는 이 clock에 대한 epoch, minimum/maximum timepoints를 반환하는 기능과 timepoint 연산을 제공합니다.
Clocks
아래의 표는 타입 정의와 clock의 static member를 보여줍니다.
C++ 표준 라이브러리는 위와 같은 인터페이스를 제공하는 세 가지 종류의 clock을 제공합니다.
- system_clock은 현재 시스템의 real-time clock과 관련된 timepoints를 나타냅니다. 이 클럭은 to_time_t()와 from_time_to()라는 함수를 제공하여 timepoint와 C 시스템 시간 타입인 time_t 간의 변환을 도와줍니다. 즉, timepoint를 calendar times로, 그 반대의 경우도 가능하다는 것을 의미합니다.
- steady_clock은 그 값을 절대로 변하지 않는다는 것을 보장합니다. 따라서, 물리적인 시간이 진행됨에 따라서 timepoint가 절대 감소하지 않으며, 실제 시간에 상대적인 steady rate로 진행됩니다.
- high_resolution_clock은 현재 시스템에서 가능한 가장 짧은 tick period를 갖는 클럭을 나타냅니다.
사실 표준에서는 이러한 precision, epoch, range(minimum/maximum timepoint)에 대해 아무것도 정의하지 않습니다. 따라서, 시스템의 클럭이 UNIX epoch(1970년 1월 1일)일 수도 있지만 항상 그렇지는 않습니다. 만약 특정 epoch나 클럭이 커버하지 못하는 timepoint를 다루어야 한다면 함수를 사용하여 알아내야 합니다.
예를 들어, 아래 코드는 클럭의 속성을 출력합니다.
#include <iostream>
#include <iomanip>
#include <chrono>
using namespace std;
template<typename C>
void printClockData()
{
cout << "- precision: ";
// if time unit is less than or equal to one millisecond
typedef typename C::period P; // type of time unit
if (ratio_less_equal<P,milli>::value) {
// convert to and print as milliseconds
typedef typename ratio_multiply<P,kilo>::type TT;
cout << fixed << double(TT::num)/TT::den << " milliseconds\n";
}
else {
// print as seconds
cout << fixed << double(P::num)/P::den << " seconds\n";
}
cout << "- is_steady: " << boolalpha << C::is_steady << endl;
}
int main()
{
cout << "system_clock: \n";
printClockData<chrono::system_clock>();
cout << "\nhigh_resolution_clock: \n";
printClockData<chrono::high_resolution_clock>();
cout << "\nsteady_clock: \n";
printClockData<chrono::steady_clock>();
}
제가 사용하는 시스템에서는 모든 클락이 1나노초라는 동일한 정밀도를 사용합니다.
steady_clock은 프로그램에서 두 시간의 차이를 계산하거나 비교할 때 중요합니다. 예를 들어, 아래의 코드를 실행한 뒤
auto system_start = chrono::system_clock::now();
아래와 같이 프로그램이 1분 이상 실행되었는지 검사하는 조건이 있을 수 있습니다.
if (chrono::system_clock::now() > system_start + minutes(1))
하지만 이 코드는 정상적으로 동작하지 않을 수 있는데, 왜냐하면 만약 클럭이 중간에 바뀐다면 비교문은 1분 넘게 실행되었더라도 false로 평가할 수 있습니다.
유사하게, 실행된 시간을 처리하는 아래 코드를 살펴봅시다.
auto diff = chrono::system_clock::now() - system_start;
auto sec = chrono::duration_cast<chrono::seconds>(diff);
cout << "this program runs: " << s.count() << " seconds\n";
만약 클럭이 중간에 바뀐다면 위 코드는 음수를 출력할 수도 있습니다. 이와 같이 steady_clock은 시스템 클럭이 변경될 때 duration을 변경할 수 있으므로 다른 타이머를 사용하는 것이 좋습니다. 타이머 관련해서는 아래에서 자세히 다루도록 하겠습니다.
Timepoints
이러한 클럭(또는 사용자 정의 클럭)을 사용하여 timepoint를 처리할 수 있습니다. time_point 클래스는 아래와 같이 클럭으로 파라미터화되는 인터페이스를 제공합니다.
namespace std {
namespace chrono {
template<typename Clock,
typename Duration = typename Clock::duration>
class time_point;
}
}
여기서 4가지의 timepoint가 특별한 역할을 담당합니다.
- epoch : time_point 클래스의 기본 생성자가 각 클럭에 따라 반환하는 값
- current time : 클럭의 static member function now()
- minimum timepoint : 클럭에 대한 time_point 클래스의 static member function min()
- maximum timepoint : 클럭에 대한 time_point 클래스의 static member function max()
예를 들어, 아래 코드는 위의 timepoints을 tp에 할당하고, 이를 calendar notation으로 출력합니다.
#include <iostream>
#include <iomanip>
#include <string>
#include <chrono>
using namespace std;
string asString(const chrono::system_clock::time_point& tp)
{
// convert to system time
time_t t = chrono::system_clock::to_time_t(tp);
string ts = ctime(&t); // convert to calendar time
ts.resize(ts.size()-1); // skip training newline
return ts;
}
int main()
{
// print the epoch of this system clock
chrono::system_clock::time_point tp;
cout << "epoch: " << asString(tp) << endl;
// print current time
tp = chrono::system_clock::now();
cout << "now: " << asString(tp) << endl;
// print minimum time of this system clock
tp = chrono::system_clock::time_point::min();
cout << "min: " << asString(tp) << endl;
// print maximum time of this system clock
tp = chrono::system_clock::time_point::max();
cout << "max: " << asString(tp) << endl;
}
여기서 to_time_t()를 사용하여 timepoint를 C와 POSIX에서 사용하던 시간 타입인 time_t로 변환합니다. 이 타입은 일반적으로 UNIX epoch인 1970년 1월 1일에서부터 몇 초가 흘렀는지를 표현합니다. 그리고 ctime()을 사용하여 이 값을 달력 표기로 변환합니다.
이때 출력되는 값을 볼 때 시간대를 염두해야 합니다. 여기서 사용된 UNIX epoch는 영국 그리니치 시간으로 0시인데, 한국의 시간대에서는 +9 이므로 여기서는 1970년 1월 1일 9시로 출력됩니다. 대신 UTC(universal time)을 사용하고 싶다면 ctime 대신 아래와 같이 작성하면 됩니다.
string ts = asctime(gmtime(&t));
이를 사용하면 아래와 같이 출력합니다.
일반적으로 time_point 객체는 duration을 단 하나의 멤버로 가지며, 이는 연관된 clock의 epoch로부터 흐른 시간을 나타냅니다. timepoint 값은 time_since_epoch()로부터 얻을 수 있습니다. 또한, timepoint 연산에서는 timepoint와 또 다른 timepoint 또는 duration의 조합할 수 있습니다.
인터페이스는 ratio 클래스를 사용하지만, duration 값에 대해 오버플로우가 발생할 수 있습니다. 만약 duration 단위에서 오버플로우가 발생하면 컴파일시간 에러를 발생시킵니다. 아래 예제 코드를 살펴보겠습니다.
#include <iostream>
#include <ctime>
#include <string>
#include <chrono>
using namespace std;
string asString(const chrono::high_resolution_clock::time_point& tp)
{
// convert to system time
time_t t = chrono::high_resolution_clock::to_time_t(tp);
string ts = ctime(&t); // convert to calendar time
ts.resize(ts.size()-1); // skip training newline
return ts;
}
int main()
{
// define type for durations that represent day(s)
typedef chrono::duration<int,ratio<3600*24>> Days;
// process the epoch of this system clock
chrono::time_point<chrono::system_clock> tp;
cout << "epoch: " << asString(tp) << endl;
// add one day, 23 hours, and 55 minutes
tp += Days(1) + chrono::hours(23) + chrono::minutes(55);
cout << "later: " << asString(tp) << endl;
// process difference from epoch in minutes and days
auto diff = tp - chrono::system_clock::time_point();
cout << "diff: " << chrono::duration_cast<chrono::minutes>(diff).count()
<< " minute(s)\n";
Days days = chrono::duration_cast<Days>(diff);
cout << "diff: " << days.count() << " day(s)\n";
// subtract one year (hoping it is valid and not a leap year)
tp -= chrono::hours(24 * 365);
cout << "-1 year: " << asString(tp) << endl;
// subtract 50 years (hoping it is valid and ignoring leap years)
tp -= chrono::duration<int,ratio<3600*24*365>>(50);
cout << "-50 years: " << asString(tp) << endl;
// subtract 50 years (hoping it is valid and ignoring leap years)
tp -= chrono::duration<int,ratio<3600*24*365>>(50);
cout << "-50 years: " << asString(tp) << endl;
}
출력은 위와 같습니다.
먼저, 아래와 같은 표현식이나
tp += Days(1) + chrono::hours(23) + chrono::minutes(55);
아래의 표현식을 사용하면
tp -= chrono::hours(24 * 365);
timepoint 산술 연산을 사용하여 timepoint를 수정할 수 있습니다.
항상 분이나 시간보다는 system clock의 정밀도가 더 좋기 때문에 두 timepoint 사이의 차이를 날짜로 변경하려면 명시적으로 변환해주어야 합니다.
auto diff = tp - chrono::system_clock::time_point();
Days days = chrono::duration_cast<Days>(diff);
하지만 위와 같은 연산들은 오버플로우가 발생하는지에 대해 검사를 하지 않습니다.
위의 코드에 대한 출력으로부터 확인할 수 있는 부분은 다음과 같습니다.
- 결과 단위를 duration_cast<>를 사용하여 변환했습니다. 일반 정수 타입의 단위인 경우, 소수점 이하의 값들은 반올림되지 않고 버려집니다. 따라서, 47시간 55분(2875분)은 1일로 변환됩니다.
- 1년을 365일 기준으로 하는 50년을 빼면 윤년을 계산하지 않습니다. 따라서, 결과값은 1월 3일이 아니라 1월 16일이 되었습니다.
- 한 번더 50년을 뺄 경우, 더 옛날로 갑니다. 참고 서적에서의 최소값과 제 시스템에서의 최소값이 달라서 저의 경우에는 정상적인 값이 나왔으나, 참고 서적의 경우 연도가 2005년으로 출력되고 있습니다. 이처럼 C++ 표준 라이브러리는 어떠한 오류 처리도 하지 않습니다. 300년을 빼보면 출력은 다음과 같습니다.
chrono는 duration과 timepoint를 위한 것이며, 날짜 및 시간을 위한 라이브러리가 아니라는 것을 염두해두어야 합니다. 따라서, duration과 timepoint를 계산할 수 있지만, epoch와 최소/최대 timepoint, 윤년이나 윤초를 염두해야 합니다.
Data and Time Functions by C and POSIX
C++ 표준 라이브러리는 날짜와 시간을 처리하는 표준 C와 POSIX 인터페이스를 제공합니다. <ctime>에서는 <time.h>의 매크로, 타입, 함수를 std 네임스페이스에서 사용할 수 있습니다. 데이터 타입과 함수들은 아래와 같습니다.
CLOCKS_PER_SEC는 clock()의 단위 시간을 정의합니다. clock() 함수는 elapsed CPU time을 1/CLOCKS_PER_SEC 초 단위로 리턴합니다. 일반적으로 time_t는 UNIX의 epoch인 1970년 1월 1일에서부터 흐른 시간을 초단위로 나타낸 값을 갖습니다. 하지만 C/C++ 표준에 따르면 이 값이 항상 보장되는 것은 아닙니다.
Conversion between Timepoints and Calendar Time
timepoint를 calendar time 문자열로 변환하는 함수는 위에서 간단하게 살펴봤습니다. 여기서 system_clock의 timepoint를 calendar time 문자열로, 그리고 그 반대의 변환에 대해서 간단히 살펴보겠습니다.
#include <iostream>
#include <ctime>
#include <string>
#include <chrono>
using namespace std;
string asString(const chrono::high_resolution_clock::time_point& tp)
{
// convert to system time
time_t t = chrono::high_resolution_clock::to_time_t(tp);
string ts = ctime(&t); // convert to calendar time
ts.resize(ts.size()-1); // skip training newline
return ts;
}
chrono::system_clock::time_point makeTimePoint(
int year, int mon, int day,
int hour, int min, int sec = 0)
{
struct std::tm t;
t.tm_sec = sec; // second of minute (0..59 and 60 for leap seconds)
t.tm_min = min; // minute of hour (0..59)
t.tm_hour = hour; // hour of day (0..23)
t.tm_mday = day; // day of month (1..31)
t.tm_mon = mon - 1; // month of year (0..11)
t.tm_year = year - 1900; // year since 1900
t.tm_isdst = -1; // determin whether daylight saving time
std::time_t tt = std::mktime(&t);
if (tt == -1) {
throw "no valid system time";
}
return std::chrono::system_clock::from_time_t(tt);
}
int main()
{
auto tp1 = makeTimePoint(2022,12,8,01,00,00);
cout << asString(tp1) << endl;
auto tp2 = makeTimePoint(2023,05,04,23,13,44);
cout << asString(tp2) << endl;
}
위에서 구현한 makeTimePoint()와 asString()은 local time zone에 영향을 받는다는 것을 기억합시다. 그렇기 때문에 makeTimePoint()로 전달된 날짜와 asString()에서 출력하는 날짜가 동일한 것입니다.
'프로그래밍 > C & C++' 카테고리의 다른 글
[C++] 함수 객체 활용 (0) | 2022.12.12 |
---|---|
[C++] Iterator Traits와 User-Defined Iterators (0) | 2022.12.11 |
[C++] Type Traits와 Type Utilities (0) | 2022.12.07 |
[C++] Numeric Limits (0) | 2022.12.06 |
[C++] Pairs and Tuples (0) | 2022.12.06 |
댓글