C++ chrono::duration: 단위 변환, duration_cast·round, 시간 리터럴과 정밀도 손실
이 글의 핵심
std::chrono::duration은 값과 단위를 함께 가진 시간 간격 타입이라, 초와 밀리초를 섞어도 컴파일러가 변환을 맞춰 줍니다. 이 글은 암시적·명시적 변환 규칙과 duration_cast·floor·ceil·round, 10s·500ms 같은 C++14 리터럴과 네임스페이스, 1.5s가 부동소수 duration이 되는 이유, C++20 months가 달력 한 달이 아니라는 함정까지 GCC로 확인한 예제로 정리합니다.
들어가며
std::chrono::duration은 시간 간격을 표현하는 타입입니다. 타입 안전하며, 단위 변환이 명확하며, 시간 연산을 지원합니다. C 스타일 코드에서는 흔히 시간을 int 하나로 표현하고 그것이 초인지 밀리초인지는 변수 이름이나 주석에만 의존했지만, duration은 단위 정보를 타입 자체에 담아 컴파일 타임에 단위 실수를 잡아낼 수 있게 해줍니다. 예를 들어 sleep(1000)이 1000초인지 1000밀리초인지 헷갈려서 생기는 버그는, std::chrono::milliseconds(1000)처럼 타입으로 명시하면 애초에 발생할 수 없습니다.
도입하면서 가장 먼저 부딪힌 정밀도 손실
duration을 실무에 도입할 때 가장 자주 마주치는 함정은 하위 단위에서 상위 단위로 변환할 때 발생하는 정밀도 손실입니다. duration_cast는 기본적으로 소수점을 버리는(절삭) 방식으로 동작하기 때문에, 1.5초를 duration_cast<seconds>로 변환하면 조용히 1초가 되어버립니다. 이 사실을 모르고 타이머나 재시도 로직에 duration_cast를 사용하면, 의도한 것보다 짧게 대기하거나 통계가 미묘하게 어긋나는 버그로 이어질 수 있습니다. 이 글에서는 이런 변환 규칙을 명확히 하고, C++14 리터럴과 C++17 반올림 함수를 활용해 더 안전하게 시간을 다루는 방법을 정리합니다.
틱 개수와 단위로 이루어진 duration
seconds·milliseconds 만들기
std::chrono::duration은 사실 “틱(tick)의 개수”와 “1틱이 몇 초인지”라는 두 정보를 조합한 템플릿 타입입니다. seconds, milliseconds, minutes, hours는 이 템플릿을 자주 쓰는 단위로 미리 특수화해둔 타입 별칭일 뿐이므로, 서로 다른 이름을 가졌어도 근본적으로는 같은 duration 틀 안에서 동작합니다. count() 메서드는 그 duration이 담고 있는 순수한 숫자 값(틱 개수)을 꺼내는 유일한 방법으로, 이 값 자체는 단위 정보를 포함하지 않는다는 점에 유의해야 합니다.
#include <chrono>
#include <iostream>
int main() {
using namespace std::chrono;
// 시간 간격 생성: duration 타입
// std::chrono::seconds: 초 단위 duration
seconds sec(10); // 10초
// std::chrono::milliseconds: 밀리초 단위 duration
milliseconds ms(10000); // 10000밀리초 = 10초
// std::chrono::minutes: 분 단위 duration
minutes min(1); // 1분 = 60초
// std::chrono::hours: 시간 단위 duration
hours hr(1); // 1시간 = 60분 = 3600초
// 값 얻기: count() 메서드로 숫자 값 추출
std::cout << "초: " << sec.count() << std::endl; // 10
std::cout << "밀리초: " << ms.count() << std::endl; // 10000
return 0;
}
표준 duration 타입 여섯 가지
C++11 표준은 나노초부터 시간(hour) 단위까지 여섯 가지 기본 duration 타입을 <chrono> 헤더에 미리 정의해두었습니다. 이들은 모두 내부적으로 정수 카운터와 std::ratio(분수 형태의 단위 비율)의 조합으로 구현되어 있어, 서로 다른 단위끼리도 정확한 비율로 변환할 수 있습니다. 아래 코드는 여섯 가지 타입이 모두 같은 시간(1시간)을 표현할 수 있다는 것을 보여줍니다.
#include <chrono>
// C++11 표준 단위
std::chrono::hours h(1);
std::chrono::minutes m(60);
std::chrono::seconds s(3600);
std::chrono::milliseconds ms(3600000);
std::chrono::microseconds us(3600000000);
std::chrono::nanoseconds ns(3600000000000);
// 모두 1시간을 표현
단위 변환과 duration_cast
큰 단위에서 작은 단위로: 암시적 변환
큰 단위를 작은 단위로 바꾸는 변환은 정보 손실이 전혀 없기 때문에(10초는 정확히 10000밀리초와 같으므로) 컴파일러가 자동으로 처리해줍니다. seconds에서 milliseconds로의 대입이 별도의 캐스팅 함수 없이 그냥 대입문만으로 이루어지는 것도 이 때문입니다. 다만 정수 오버플로우 가능성은 여전히 존재하므로, 매우 긴 시간을 매우 작은 단위(나노초 등)로 변환할 때는 정수 타입의 표현 범위를 염두에 두어야 합니다.
#include <chrono>
#include <iostream>
int main() {
using namespace std::chrono;
seconds s(10); // 10초
// ✅ 상위 → 하위 (암시적 변환 가능)
// 큰 단위 → 작은 단위: 정보 손실 없음
// seconds → milliseconds: 10초 = 10000밀리초
milliseconds ms = s; // 10000ms (자동 변환)
// seconds → microseconds: 10초 = 10000000마이크로초
microseconds us = s; // 10000000us (자동 변환)
std::cout << "밀리초: " << ms.count() << std::endl; // 10000
std::cout << "마이크로초: " << us.count() << std::endl; // 10000000
return 0;
}
작은 단위에서 큰 단위로: duration_cast
반대로 작은 단위를 큰 단위로 바꾸는 변환은 정보를 잃을 수 있습니다(1500밀리초는 1.5초인데, seconds는 정수만 표현할 수 있어 소수부를 담을 수 없습니다). 표준 라이브러리는 이런 손실 가능성이 있는 변환을 암시적으로 허용하지 않고, 반드시 duration_cast를 명시적으로 호출하도록 강제해 개발자가 “이 변환에서 정밀도를 잃을 수 있다”는 사실을 의식하도록 만듭니다. duration_cast의 기본 동작은 절삭(truncation)이므로, 1500ms는 1s로 변환되며 나머지 500ms는 그냥 버려집니다.
#include <chrono>
#include <iostream>
int main() {
using namespace std::chrono;
milliseconds ms(1500); // 1500밀리초 = 1.5초
// ❌ 하위 → 상위 (암시적 불가)
// 작은 단위 → 큰 단위: 정보 손실 가능 (소수점 버림)
// seconds s = ms; // 컴파일 에러: 명시적 변환 필요
// ✅ duration_cast (명시적 변환)
// duration_cast<seconds>: 밀리초를 초로 변환
// 1500ms → 1s (500ms 버림, 절삭)
seconds s = duration_cast<seconds>(ms); // 1s (절삭)
std::cout << "초: " << s.count() << std::endl; // 1
return 0;
}
C++17 round·ceil·floor
duration_cast의 절삭 동작이 항상 원하는 결과는 아닙니다. C++17은 이를 보완하기 위해 round(반올림), ceil(올림), floor(내림) 세 가지 함수를 추가로 제공합니다. 예를 들어 사용자에게 “약 몇 초 남았는지” 보여주는 UI라면 round가 더 자연스러운 결과를 주고, 타임아웃 계산처럼 “최소한 이만큼은 기다려야 한다”는 보장이 필요하다면 값을 줄여서는 안 되므로 ceil이 적합합니다. floor는 duration_cast와 동일한 절삭 동작을 하지만 이름으로 의도를 더 명확히 드러낼 수 있습니다.
#include <chrono>
#include <iostream>
int main() {
using namespace std::chrono;
milliseconds ms(1500); // 1500밀리초 = 1.5초
// duration_cast: 절삭 (소수점 버림)
// 1.5초 → 1초 (0.5초 버림)
auto s1 = duration_cast<seconds>(ms);
std::cout << "절삭: " << s1.count() << "s" << std::endl; // 1s
// round: 반올림 (C++17)
// 1.5초 → 2초 (0.5 이상이므로 올림)
auto s2 = round<seconds>(ms);
std::cout << "반올림: " << s2.count() << "s" << std::endl; // 2s
// ceil: 올림 (C++17)
// 1.5초 → 2초 (무조건 올림)
auto s3 = ceil<seconds>(ms);
std::cout << "올림: " << s3.count() << "s" << std::endl; // 2s
// floor: 내림 (C++17)
// 1.5초 → 1초 (무조건 내림, duration_cast와 동일)
auto s4 = floor<seconds>(ms);
std::cout << "내림: " << s4.count() << "s" << std::endl; // 1s
return 0;
}
사칙연산과 단위가 다른 값의 비교
산술 연산
duration은 마치 숫자 타입처럼 사칙연산과 나머지 연산을 그대로 지원합니다. 이는 내부적으로 각 연산자가 오버로드되어 있어, 두 duration을 더하거나 정수를 곱하는 것이 일반 정수 연산만큼이나 자연스럽게 표현되도록 설계되었기 때문입니다. 서로 다른 단위의 duration끼리 연산할 때는(예: seconds + milliseconds) 표준이 정한 규칙에 따라 자동으로 더 정밀한 단위로 맞춰져 계산되므로, 개발자가 수동으로 단위를 맞출 필요가 없습니다.
#include <chrono>
#include <iostream>
int main() {
using namespace std::chrono;
seconds s1(10);
seconds s2(5);
// 덧셈
auto sum = s1 + s2;
std::cout << "합: " << sum.count() << "s" << std::endl; // 15s
// 뺄셈
auto diff = s1 - s2;
std::cout << "차: " << diff.count() << "s" << std::endl; // 5s
// 곱셈
auto mul = s1 * 2;
std::cout << "곱: " << mul.count() << "s" << std::endl; // 20s
// 나눗셈
auto div = s1 / 2;
std::cout << "나눔: " << div.count() << "s" << std::endl; // 5s
// 나머지
auto mod = s1 % s2;
std::cout << "나머지: " << mod.count() << "s" << std::endl; // 0s
return 0;
}
비교 연산
duration 사이의 비교 연산자(<, >, == 등)도 단위가 다른 값끼리 자연스럽게 비교할 수 있도록 설계되어 있습니다. 아래 예제의 s1 == ms처럼 seconds와 milliseconds를 직접 비교해도, 내부적으로 두 값을 공통의(더 정밀한) 단위로 변환한 뒤 실제 시간 길이를 기준으로 비교하기 때문에 10초와 10000밀리초는 올바르게 같다고 판정됩니다. 이 덕분에 서로 다른 곳에서 각기 다른 단위로 만들어진 duration 값들도 안전하게 서로 비교할 수 있습니다.
#include <chrono>
#include <iostream>
int main() {
using namespace std::chrono;
seconds s1(10);
seconds s2(5);
milliseconds ms(10000);
std::cout << std::boolalpha;
std::cout << "s1 > s2: " << (s1 > s2) << std::endl; // true
std::cout << "s1 == ms: " << (s1 == ms) << std::endl; // true (단위 다름)
return 0;
}
C++14 duration 리터럴
10s·500ms 리터럴 쓰기
std::chrono::seconds(10)처럼 매번 타입 이름을 완전히 써서 duration을 만드는 것은 코드가 길어지고 가독성이 떨어집니다. C++14는 std::chrono_literals 네임스페이스에 10s, 500ms처럼 숫자 뒤에 단위를 붙이는 사용자 정의 리터럴을 도입해 이 문제를 해결했습니다. 이 리터럴들은 일반 산술 연산자와도 자연스럽게 조합되므로, 아래 예제처럼 1h + 30min + 45s + 500ms같이 여러 단위를 섞어 하나의 총 시간을 표현하는 코드를 매우 직관적으로 작성할 수 있습니다.
#include <chrono>
#include <iostream>
int main() {
using namespace std::chrono_literals;
// 리터럴로 duration 생성
auto d1 = 10s; // seconds
auto d2 = 500ms; // milliseconds
auto d3 = 100us; // microseconds
auto d4 = 100ns; // nanoseconds
auto d5 = 1min; // minutes
auto d6 = 2h; // hours
// 조합
auto total = 1h + 30min + 45s + 500ms;
auto total_ms = std::chrono::duration_cast<std::chrono::milliseconds>(total);
std::cout << "총 시간: " << total_ms.count() << "ms" << std::endl; // 5445500ms
return 0;
}
리터럴을 쓰기 위한 네임스페이스
10s를 쓰려면 리터럴 연산자가 보이게 해야 하고, 그렇지 않으면 unable to find numeric literal operator 'operator""s' 에러가 납니다. 방법은 세 가지이며 범위가 다릅니다.
using namespace std::chrono_literals; // chrono 리터럴만 (가장 좁음, 헤더에서도 함수 안이라면 무난)
using namespace std::literals; // chrono + string("abc"s) + complex 리터럴
using namespace std::chrono; // chrono의 모든 이름 + chrono 리터럴도 함께 보임
std::chrono 안에서 표준이 using namespace literals::chrono_literals;를 선언해 두었기 때문에, using namespace std::chrono;만 해도 5s가 동작합니다(GCC 10에서 확인). 반대로 s는 std::string 리터럴("abc"s)과 접미사가 같지만, 하나는 정수·실수에, 하나는 문자열에 붙으므로 둘 다 열어 두어도 충돌하지 않습니다.
1.5s는 부동소수 duration
auto a = 5s; // std::chrono::seconds (정수 rep)
auto b = 1.5s; // std::chrono::duration<long double> (부동소수 rep)
std::chrono::seconds s1 = 1000ms; // ❌ 컴파일 에러: 밀리초 → 초는 값이 정확히 떨어져도 암시적 변환 불가
std::chrono::seconds s2 = b; // ❌ 컴파일 에러: 부동소수 → 정수 duration
auto s3 = std::chrono::duration_cast<std::chrono::seconds>(1500ms); // 1s (버림)
소수점이 붙은 리터럴은 long double을 담는 duration이 되므로, seconds 변수에 넣으려면 duration_cast가 필요합니다. 또 1000ms처럼 값이 정확히 1초인 경우에도 더 굵은 단위로의 변환은 타입만 보고 막힙니다. 컴파일러는 값이 아니라 타입(밀리초 → 초는 정밀도를 잃을 수 있음)으로 판단하기 때문입니다. 두 에러 모두 GCC 10에서 conversion from 'duration<...>' to non-scalar type 'duration<...>' requested로 나오는데, 템플릿 인자가 [...]로 줄여져 있어 처음 보면 무엇이 문제인지 알기 어렵습니다. 이런 메시지를 보면 “더 굵은 단위나 정수로 가려고 했구나”라고 읽으면 됩니다.
365 * 24h처럼 큰 값을 곱하는 것은 걱정하지 않아도 됩니다. hours의 rep은 최소 23비트 이상의 부호 있는 정수(구현상 보통 long long/int64)라 8760시간은 전혀 문제없습니다. 오버플로우가 현실적인 것은 nanoseconds로 수백 년을 표현하거나, int를 rep으로 직접 지정한 사용자 정의 duration을 쓸 때입니다.
C++20: 날짜 리터럴과 months의 함정
C++20은 달력 타입과 함께 2026y(연), 29d(일) 리터럴을 추가했습니다. 29d는 시간 간격 days가 아니라 달의 날짜 std::chrono::day라는 점에 주의합니다. 이 기능은 표준 라이브러리 지원이 늦게 들어와 GCC 10에서는 unable to find numeric literal operator 'operator""y' 에러가 납니다(libstdc++는 GCC 11부터, 형식화된 출력은 더 이후 버전부터 지원).
using namespace std::chrono;
auto date = 2026y / March / 29d; // year_month_day
auto a = sys_days{date} + months{1}; // ⚠️ 달력 한 달이 아니라 평균 한 달(30.436875일)을 더함
auto b = date + months{1}; // ✅ 달력 기준: 2026-04-29
std::chrono::months는 달력 연산용 이름이지만 타입은 그냥 duration이고, 길이는 그레고리력 평균인 2,629,746초(30.436875일)입니다. 그래서 sys_days처럼 시점에 더하면 “다음 달 같은 날”이 아니라 30일 10시간 29분 6초 뒤가 되고, 결과의 정밀도도 일 단위가 아니게 됩니다. 한 달 뒤, 1년 뒤처럼 달력 의미의 계산은 year_month_day에 직접 months/years를 더해야 하고, 이때 1월 31일 + 1개월처럼 존재하지 않는 날짜가 나올 수 있으니 .ok()로 확인한 뒤 year_month_day_last 등으로 보정합니다.
절삭·타입 불일치·음수·0 값 다루기
duration_cast 절삭으로 인한 정밀도 손실
duration_cast가 항상 절삭한다는 사실을 잊으면, 통계 집계나 사용자에게 보여줄 경과 시간 계산에서 미묘하게 값이 낮게 나오는 버그가 생깁니다. 예를 들어 1.5초가 계속 1초로 절삭되어 누적되면 실제 경과 시간과 표시되는 값 사이에 눈에 띄는 오차가 쌓일 수 있습니다. 반올림이 더 자연스러운 상황이라면 C++17의 round를 사용해 이런 체계적인 편향을 피해야 합니다.
#include <chrono>
#include <iostream>
int main() {
using namespace std::chrono;
milliseconds ms(1500);
// ❌ duration_cast: 절삭 (500ms 손실)
seconds s1 = duration_cast<seconds>(ms);
std::cout << "절삭: " << s1.count() << "s" << std::endl; // 1s
// ✅ round: 반올림
seconds s2 = round<seconds>(ms);
std::cout << "반올림: " << s2.count() << "s" << std::endl; // 2s
return 0;
}
정수를 duration 매개변수에 넘길 수 없음
duration을 매개변수로 받는 함수에 단순 정수를 넘기려고 하면 컴파일 에러가 발생합니다. 이는 버그가 아니라 오히려 duration이 제공하는 타입 안전성의 핵심입니다. 정수 10이 초를 의미하는지 밀리초를 의미하는지 함수 시그니처만으로는 알 수 없기 때문에, 표준 라이브러리는 이런 암묵적 변환을 아예 허용하지 않고 std::chrono::seconds(10)이나 C++14 리터럴(10s)처럼 단위를 명시하도록 강제합니다.
#include <chrono>
#include <iostream>
void waitFor(std::chrono::seconds s) {
std::cout << s.count() << "초 대기" << std::endl;
}
int main() {
// ❌ 타입 불일치
// waitFor(10); // 컴파일 에러
// ✅ 명시적 생성
waitFor(std::chrono::seconds(10));
// ✅ 리터럴 (C++14)
using namespace std::chrono_literals;
waitFor(10s);
return 0;
}
뺄셈 결과가 음수 duration이 됨
duration의 내부 표현이 부호 있는 정수(대부분의 표준 타입은 std::int64_t 기반)이기 때문에, 뺄셈 결과가 음수가 되는 것을 막을 방법이 없습니다. 두 시점 사이의 차이를 계산할 때 어느 쪽이 더 나중인지 확신할 수 없다면 결과가 음수일 가능성을 항상 고려해야 하며, “경과 시간”처럼 항상 양수여야 하는 값을 다룬다면 아래처럼 부호를 명시적으로 처리하는 코드가 필요합니다.
#include <chrono>
#include <iostream>
int main() {
using namespace std::chrono;
seconds s1(5);
seconds s2(10);
// 음수 duration
auto diff = s1 - s2; // -5s
std::cout << "차이: " << diff.count() << "s" << std::endl; // -5
// 절대값
auto abs_diff = (diff < seconds::zero()) ? -diff : diff;
std::cout << "절대값: " << abs_diff.count() << "s" << std::endl; // 5
return 0;
}
zero()로 0 duration 표현
seconds(0)처럼 직접 값을 넣어 0을 표현할 수도 있지만, 표준은 zero()라는 정적 멤버 함수를 제공해 “이 duration 타입의 0 값”을 더 명확하게 표현할 수 있게 해줍니다. zero(), min(), max()는 duration 타입의 특수값을 다룰 때 매직 넘버(0, 하드코딩된 최댓값 등)를 직접 쓰는 대신 사용하는 관용구로, 초기화되지 않은 타이머 상태를 표시하거나 값의 유효 범위를 검사할 때 유용합니다.
#include <chrono>
#include <iostream>
int main() {
using namespace std::chrono;
// 0 duration
seconds s = seconds::zero();
milliseconds ms = milliseconds::zero();
std::cout << std::boolalpha;
std::cout << "s == 0: " << (s == seconds::zero()) << std::endl; // true
return 0;
}
steady_clock 기반 타이머 유틸리티 만들기
Timer 클래스는 지금까지 다룬 duration 개념을 실제 경과 시간 측정에 적용한 예입니다. steady_clock을 사용하는 이유는(대신 system_clock을 쓰지 않는 이유는) 시스템 시계가 NTP 동기화나 사용자 조작으로 갑자기 앞뒤로 튈 수 있는 반면, steady_clock은 오직 단조 증가만 보장하기 때문에 경과 시간 측정처럼 “항상 앞으로만 흐르는 시간”이 필요한 용도에 적합합니다. elapsedMs는 현재 시각에서 시작 시각을 뺀 결과(두 time_point의 차이는 duration이 됩니다)를 duration_cast로 밀리초 단위로 변환해 반환합니다.
#include <chrono>
#include <thread>
#include <iostream>
class Timer {
using Clock = std::chrono::steady_clock;
using TimePoint = std::chrono::time_point<Clock>;
TimePoint start;
public:
Timer() : start(Clock::now()) {}
// 경과 시간 (밀리초)
long long elapsedMs() const {
auto now = Clock::now();
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(now - start);
return duration.count();
}
// 경과 시간 (마이크로초)
long long elapsedUs() const {
auto now = Clock::now();
auto duration = std::chrono::duration_cast<std::chrono::microseconds>(now - start);
return duration.count();
}
// 재시작
void reset() {
start = Clock::now();
}
};
int main() {
Timer timer;
// 작업 1
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::cout << "작업 1 완료: " << timer.elapsedMs() << "ms" << std::endl;
// 작업 2
std::this_thread::sleep_for(std::chrono::milliseconds(200));
std::cout << "작업 2 완료: " << timer.elapsedMs() << "ms" << std::endl;
// 재시작
timer.reset();
// 작업 3
std::this_thread::sleep_for(std::chrono::milliseconds(150));
std::cout << "작업 3 완료: " << timer.elapsedMs() << "ms" << std::endl;
return 0;
}
출력:
작업 1 완료: 100ms
작업 2 완료: 300ms
작업 3 완료: 150ms
duration 정리
핵심 요약
- duration: 시간 간격 표현
- count(): 값 얻기
- 상위→하위: 암시적 변환
- 하위→상위:
duration_cast필요 - 연산: +, -, *, /, % 지원
- 리터럴:
10s,500ms(C++14)
duration 변환 규칙
| 변환 | 방법 | 예시 |
|---|---|---|
| 상위 → 하위 | 암시적 | milliseconds ms = seconds(10); |
| 하위 → 상위 | duration_cast | seconds s = duration_cast<seconds>(ms); |
| 반올림 | round (C++17) | seconds s = round<seconds>(ms); |
| 올림 | ceil (C++17) | seconds s = ceil<seconds>(ms); |
| 내림 | floor (C++17) | seconds s = floor<seconds>(ms); |
이어서 볼 글
자주 묻는 질문 (FAQ)
Q. 타임아웃 값을 함수 인자로 받을 때 int(밀리초) 대신 duration을 받아야 하나요?
A. 가능하면 std::chrono::milliseconds나 std::chrono::nanoseconds처럼 duration 타입으로 받는 것이 좋습니다. 호출하는 쪽이 2s, 500ms 중 무엇을 넘겨도 컴파일러가 단위를 맞춰 주고(더 가는 단위로는 암시적 변환), int timeoutMs에 초 값을 넘기는 실수는 타입 에러가 됩니다. 표준 라이브러리의 std::this_thread::sleep_for, std::condition_variable::wait_for도 같은 방식으로 duration을 받습니다.