To convert a GMT time to another time zone in C++, store the GMT moment in a std::chrono::sys_seconds and wrap it in a std::chrono::zoned_time with an IANA zone name such as America/New_York. The library applies the offset and daylight saving rules in force on that date, so you never add or subtract hours yourself.
sys_seconds from your GMT value, write std::chrono::zoned_time local{"America/Chicago", gmt}; and print it with std::format("{:%F %T %Z}", local). This works on GCC 13 or newer (GCC 14 adds std::chrono::parse) and on MSVC since Visual Studio 2019 16.10. Clang’s own libc++ still treats it as experimental, so with libc++ or an older compiler use Howard Hinnant’s date library.Every program below was compiled on Ubuntu 24.04 with g++-14 -std=c++20 -Wall -Wextra (GCC 14.2) without warnings, and each output block is what it printed, using tzdata 2026a. One surprise: the stock GCC 13.3 also ran the zoned_time examples, because libstdc++ gained time zone support in GCC 13.1. It only lacks std::chrono::parse, added in GCC 14.
Which compilers support C++20 time zones in 2026
Support depends on the standard library, not the compiler front end: Clang 18 on Ubuntu uses libstdc++ by default and compiled our first example with identical output. This is the picture as of September 2026.
| Toolchain | zoned_time and tzdb | chrono::parse | Zone data source |
|---|---|---|---|
| GCC 13 (libstdc++) | Yes, since 13.1 | No | System tzdata.zi, with an embedded fallback copy |
| GCC 14 and 15 | Yes | Yes | Same as GCC 13 |
| GCC 16 | Yes, and C++20 is the default dialect | Yes | Same; current_zone() also works on Windows |
| MSVC | Yes, since Visual Studio 2019 16.10 | Yes | Windows ICU (Windows 10 version 1903 or later), updated by Windows Update |
| Clang with libc++ | Experimental, needs -fexperimental-library | Not implemented yet | System tzdata.zi on Linux |
GCC 13 to 15 label C++20 library support experimental; GCC 16.1, released April 30, 2026, drops that label. On MSVC, chrono formatting works under /std:c++20 since Visual Studio 2022 version 17.2.
Convert a GMT time with zoned_time
sys_seconds is a time point on system_clock, which counts seconds since 1970 in UTC and ignores leap seconds. That is what a GMT timestamp from a log or an API means in practice.
#include <chrono>
#include <format>
#include <iostream>
int main() {
using namespace std::chrono;
// A fixed GMT instant: September 23, 2026 at 14:30:00
sys_seconds gmt = sys_days{2026y / September / 23} + 14h + 30min;
std::cout << std::format("GMT {:%F %T}\n", gmt);
for (const char* name : {"America/New_York", "America/Chicago", "America/Phoenix",
"America/Los_Angeles", "Europe/London", "Asia/Kolkata",
"Etc/GMT+5"}) {
zoned_time local{name, gmt};
std::cout << std::format("{:<20} {:%F %T %Z (%z)}\n", name, local);
}
}$ g++-14 -std=c++20 -Wall -Wextra gmt_to_zones.cpp -o gmt_to_zones && ./gmt_to_zones
GMT 2026-09-23 14:30:00
America/New_York 2026-09-23 10:30:00 EDT (-0400)
America/Chicago 2026-09-23 09:30:00 CDT (-0500)
America/Phoenix 2026-09-23 07:30:00 MST (-0700)
America/Los_Angeles 2026-09-23 07:30:00 PDT (-0700)
Europe/London 2026-09-23 15:30:00 BST (+0100)
Asia/Kolkata 2026-09-23 20:00:00 IST (+0530)
Etc/GMT+5 2026-09-23 09:30:00 -05 (-0500)Phoenix and Los Angeles show the same clock time but different abbreviations, because Arizona does not observe daylight saving time. Kolkata uses a half hour offset. %Z prints the abbreviation and %z the numeric offset, which is what a machine should read back. For many values, call locate_zone() once and reuse the pointer. The last line is a trap explained below.
Parsing a GMT string first
std::chrono::parse reads text straight into a sys_seconds and sets the fail bit when the format does not match.
#include <chrono>
#include <format>
#include <iostream>
#include <sstream>
#include <string>
// Parse "YYYY-MM-DD HH:MM:SS" as GMT/UTC. Returns false on bad input.
bool parse_gmt(const std::string& text, std::chrono::sys_seconds& out) {
std::istringstream in{text};
in >> std::chrono::parse("%F %T", out);
return !in.fail();
}
int main() {
using namespace std::chrono;
for (std::string text : {"2026-09-23 14:30:00", "09/23/2026 2:30 PM"}) {
sys_seconds gmt;
if (!parse_gmt(text, gmt)) {
std::cout << "could not parse: " << text << '\n';
continue;
}
zoned_time ny{"America/New_York", gmt};
zoned_time la{"America/Los_Angeles", gmt};
std::cout << std::format("{} GMT\n New York: {:%F %R %Z}\n Los Angeles: {:%F %R %Z}\n",
text, ny, la);
}
}$ g++-14 -std=c++20 -Wall -Wextra parse_gmt.cpp -o parse_gmt && ./parse_gmt
2026-09-23 14:30:00 GMT
New York: 2026-09-23 10:30 EDT
Los Angeles: 2026-09-23 07:30 PDT
could not parse: 09/23/2026 2:30 PMOn GCC 13.3 the same file fails to compile:
$ g++-13 -std=c++20 -Wall -Wextra parse_gmt.cpp -o parse_gmt
parse_gmt.cpp:10:24: error: 'parse' is not a member of 'std::chrono'Z (2026-09-23T14:30:00Z) use the format "%FT%TZ". For text with a numeric offset (2026-09-23T10:30:00-04:00) use "%FT%T%Ez". Both produced 14:30:00 GMT in our tests. For the current time, use floor<seconds>(system_clock::now()).Why a fixed offset gives the wrong answer
The classic bug is a hard coded offset: “Eastern is GMT minus five.” In New York that holds for about four months a year.
#include <chrono>
#include <format>
#include <iostream>
int main() {
using namespace std::chrono;
const time_zone* ny = locate_zone("America/New_York");
for (sys_days day : {sys_days{2026y / January / 15}, sys_days{2026y / July / 15}}) {
sys_seconds gmt = day + 17h;
auto fixed = gmt - 5h; // "Eastern is GMT minus 5", hard coded
zoned_time real{ny, gmt}; // rules from the tz database
std::cout << std::format("GMT {:%F %R} | fixed: {:%R} | tzdb: {:%R %Z}\n",
gmt, fixed, real);
}
}$ ./fixed_offset_bug
GMT 2026-01-15 17:00 | fixed: 12:00 | tzdb: 12:00 EST
GMT 2026-07-15 17:00 | fixed: 12:00 | tzdb: 13:00 EDTIn July the constant is an hour off. Offsets also change by law: IANA tz release 2026d, published September 11, 2026, records Canada’s Northwest Territories moving to a permanent offset six hours behind UTC. A constant never picks that up; the tz database does once the machine’s data is updated.
GMT versus UTC, and the Etc/GMT trap
Strictly, GMT is the old astronomical time of the Greenwich meridian and UTC is the atomic scale kept close to it with leap seconds. For conversions both are the same zero offset. In C++ they differ only in leap seconds: utc_clock counts them (27 so far, per get_leap_second_info in our test) and system_clock does not.
Two traps remain. Etc/GMT+5 uses the POSIX sign convention, so it is five hours behind GMT. And Europe/London switches to BST in summer, so if a spec says “GMT” but means UK time, use Europe/London.
From local time back to GMT: gaps and overlaps
A local clock time maps to one GMT instant, to none (the hour skipped in spring) or to two (the hour repeated in fall). time_zone::to_sys() throws in the last two cases unless you choose.
#include <chrono>
#include <format>
#include <iostream>
int main() {
using namespace std::chrono;
const time_zone* ny = locate_zone("America/New_York");
local_seconds meeting = local_days{2026y / September / 23} + 10h + 30min;
std::cout << std::format("10:30 New York = {:%F %T} GMT\n", ny->to_sys(meeting));
// 2:30 AM on March 8, 2026 never happens: clocks jump from 2:00 to 3:00
local_seconds skipped = local_days{2026y / March / 8} + 2h + 30min;
try {
std::cout << std::format("{:%T}\n", ny->to_sys(skipped));
} catch (const nonexistent_local_time& e) {
std::cout << e.what() << '\n';
}
// 1:30 AM on November 1, 2026 happens twice: clocks fall back from 2:00 to 1:00
local_seconds twice = local_days{2026y / November / 1} + 1h + 30min;
std::cout << std::format("1:30 AM on Nov 1 = {:%T} or {:%T} GMT\n",
ny->to_sys(twice, choose::earliest),
ny->to_sys(twice, choose::latest));
}$ ./local_to_gmt
10:30 New York = 2026-09-23 14:30:00 GMT
2026-03-08 02:30:00 is in a gap between
2026-03-08 02:00:00 EST and
2026-03-08 03:00:00 EDT which are both equivalent to
2026-03-08 07:00:00 UTC
1:30 AM on Nov 1 = 05:30:00 or 06:30:00 GMTIn production, pick a policy with choose::earliest or choose::latest, which never throw; for a skipped time both return the jump itself, 07:00 GMT. To reject bad input, catch nonexistent_local_time and ambiguous_local_time as in our guide on how to catch exceptions in C++. Store GMT and convert only for display; a job that fires at a local wall time can use sleep_until, covered in how to add a delay in C++.
Fallbacks: the date library and plain C
Howard Hinnant’s date library
On older compilers, C++17 or libc++, use Howard Hinnant’s date library. Slightly modified versions of its date.h and tz.h were voted into the C++20 draft in March 2018, so the API is nearly identical. Compile src/tz.cpp too; USE_OS_TZDB=1 reads the zone files in /usr/share/zoneinfo.
// Fallback for compilers without C++20 time zones: github.com/HowardHinnant/date
#include "date/tz.h"
#include <chrono>
#include <iomanip>
#include <iostream>
int main() {
using namespace date; // only date, never also std::chrono (ambiguous in C++20)
namespace ch = std::chrono;
sys_seconds gmt = sys_days{2026_y / September / 23} + ch::hours{14} + ch::minutes{30};
for (const char* name : {"America/New_York", "America/Los_Angeles", "Asia/Kolkata"}) {
auto local = make_zoned(name, gmt);
std::cout << std::left << std::setw(21) << name
<< date::format("%F %T %Z (%z)", local) << '\n';
}
}$ g++-13 -std=c++17 -Wall -Wextra -DUSE_OS_TZDB=1 -Idate/include \
hinnant_date.cpp date/src/tz.cpp -o hinnant_date && ./hinnant_date
America/New_York 2026-09-23 10:30:00 EDT (-0400)
America/Los_Angeles 2026-09-23 07:30:00 PDT (-0700)
Asia/Kolkata 2026-09-23 20:00:00 IST (+0530)It also builds with g++-14 -std=c++20. Our first draft did not: with both using namespace date; and using namespace std::chrono;, C++20 mode fails with reference to 'sys_days' is ambiguous.
C and POSIX: TZ, tzset and localtime_r
Plain C has no zone objects. On POSIX systems you set TZ, call tzset(), and let localtime_r() convert a time_t.
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main(void) {
time_t gmt = 1790173800; /* 2026-09-23 14:30:00 GMT */
const char *zones[] = {"America/New_York", "Asia/Kolkata", "Not/A_Zone"};
for (int i = 0; i < 3; i++) {
struct tm local;
char buf[64];
setenv("TZ", zones[i], 1); /* process wide, not thread safe */
tzset();
localtime_r(&gmt, &local);
strftime(buf, sizeof buf, "%Y-%m-%d %H:%M:%S %Z (%z)", &local);
printf("%-18s %s\n", zones[i], buf);
}
return 0;
}$ gcc-14 -std=c17 -Wall -Wextra posix_tz.c -o posix_tz && ./posix_tz
America/New_York 2026-09-23 10:30:00 EDT (-0400)
Asia/Kolkata 2026-09-23 20:00:00 IST (+0530)
Not/A_Zone 2026-09-23 14:30:00 Not (+0000)Note the last line: glibc treats an unknown zone as UTC and invents the abbreviation “Not”. TZ is also process wide, so this is unsafe while other threads format times (see our guide to threads in C++).
Troubleshooting
current_zone() returns UTC even though TZ is set
libstdc++ reads the /etc/localtime symlink, then /etc/timezone, then falls back to UTC. On Linux it ignores TZ (the GCC 14 and 16 sources check it only on AIX); with TZ=America/Denver our container still reported Etc/UTC. Pass the zone name from configuration.
Conversions silently use old rules
Print get_tzdb().version. With /usr/share/zoneinfo hidden, our binary switched to the copy embedded in libstdc++ and reported tzdb version: 2024a without any error. Install and update tzdata (apt-get install -y tzdata); long running services can call std::chrono::reload_tzdb().
runtime_error: cannot locate zone
Our test printed std::chrono::tzdb: cannot locate zone: Not/A_Zone. Use IANA names. PDT is not a zone, and EST is a legacy fixed offset: in July it printed EST -0500 while America/New_York printed EDT -0400.
parse is not a member of std::chrono
You are on GCC 13 or libc++. Upgrade to GCC 14, or use date::parse from the date library.
Frequently asked questions
Is GMT the same as UTC in C++?
For conversions, yes. The "GMT" and "UTC" zones both have a zero offset, and system_clock time points represent both. The only observable difference is leap seconds: utc_clock counts them and system_clock does not. Use sys_seconds for GMT timestamps unless you need exact elapsed seconds across leap seconds.
Does zoned_time handle daylight saving time automatically?
Yes. zoned_time applies the rules that were in force on the date you convert, including historical changes, so January and July results differ correctly. The direction that needs care is local time back to GMT, because skipped and repeated hours throw unless you pass choose::earliest or choose::latest.
Can I use abbreviations like EST or PST as zone names?
No. Abbreviations are ambiguous (in our tests CST appeared for Chicago, Shanghai and Havana), most are not zone names, and EST is a fixed offset with no daylight saving. Use IANA names such as America/Chicago. Abbreviations are fine in output through %Z if you also record the numeric offset.
Which header and compiler flags do I need?
Include <chrono> for the time zone types and <format> for std::format. With GCC compile with -std=c++20 or newer (GCC 16 defaults to C++20), and with MSVC use /std:c++20. libstdc++ needs no extra link flags, while libc++ still requires -fexperimental-library.
The bottom line
Keep GMT in a sys_seconds and convert with zoned_time and a real zone name only when a person needs to read the result. That habit removes the fixed offset, half hour zone and daylight saving bugs at once.
GCC 13 or newer and current MSVC have what you need (GCC 14 for parse). On older compilers or libc++, the date library offers the same API. Either way, keep your tz data current.

