IPv4 ↔ IPv6 Converter — C++ source
Convert between IPv4 and IPv6 addresses both ways. Parse and validate addresses, expand and compress IPv6 to its canonical RFC 5952 form, map an IPv4 into IPv4-mapped and IPv4-compatible IPv6 (or any custom /96 prefix), and extract an embedded IPv4 back out.
This is the C++ implementation — the same logic the interactive tool runs, in a shareable, citable form.
// ip-converter — IPv4 ↔ IPv6 conversion (polyglot showcase: C++)
//
// Language: C++ (C++17, standard library only)
// Source: CosmoDev polyglot showcase port of the ip-converter tool,
// ported from src/lib/ip-converter.ts (the canonical TypeScript
// implementation).
// License: display source — part of CosmoDev's polyglot tool pages.
//
// Pure, deterministic IPv4/IPv6 address conversion logic. Every parse
// function returns std::nullopt (or "" for the string renderers) on invalid
// input rather than throwing, so the UI can show a graceful error. IPv6 text
// follows RFC 5952: lowercase hex, no leading zeros, the single longest run
// of zero groups collapsed to "::", and a dotted-decimal tail only for
// IPv4-mapped ("::ffff:") addresses.
//
// The types stay precise the way the Rust port's do: octets are uint8_t,
// IPv6 16-bit groups are uint16_t. stdlib only — the TS regex guards are
// hand-rolled below as length + charset checks.
#include <array>
#include <cstdint>
#include <optional>
#include <string>
#include <string_view>
#include <vector>
namespace ipconv {
/// Embedding family for placing an IPv4 quad inside an IPv6 address.
enum class EmbedMode {
/// `::ffff:a.b.c.d` — the modern, non-deprecated IPv4-mapped form (default).
Mapped,
/// `::a.b.c.d` — the deprecated IPv4-compatible form.
Compatible,
};
/// Options for embedding an IPv4 octet quad into an IPv6 address.
struct Ipv4ToIpv6Options {
/// Embedding family. Ignored when `prefix` is set.
EmbedMode mode = EmbedMode::Mapped;
/// Optional custom high-96-bit prefix (a valid IPv6 string; its first 6
/// groups are used and its low 32 bits are overwritten by the IPv4). e.g.
/// `"64:ff9b::"` yields a NAT64-style `64:ff9b::a.b.c.d`. Overrides `mode`.
std::optional<std::string> prefix;
};
/// A valid IPv6 group token: 1-4 hex digits, no sign, no underscores.
/// (Equivalent to the TS /^[0-9a-fA-F]{1,4}$/ regex.)
inline bool is_hex_group(std::string_view s)
{
if (s.size() < 1 || s.size() > 4)
return false;
for (char c : s) {
bool hex = (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') ||
(c >= 'A' && c <= 'F');
if (!hex)
return false;
}
return true;
}
/// A valid IPv4 octet token: 1-3 decimal digits. Range is enforced separately.
/// (Equivalent to the TS /^\d{1,3}$/ regex.)
inline bool is_dec3(std::string_view s)
{
if (s.size() < 1 || s.size() > 3)
return false;
for (char c : s)
if (c < '0' || c > '9')
return false;
return true;
}
/// Remove surrounding whitespace. Mirrors Python's str.strip() / Rust's
/// str::trim().
inline std::string_view trim(std::string_view s)
{
constexpr const char* ws = " \t\n\r\f\v";
size_t b = s.find_first_not_of(ws);
if (b == std::string_view::npos)
return {};
size_t e = s.find_last_not_of(ws);
return s.substr(b, e - b + 1);
}
/// Split s on sep, keeping empty tokens (so "a:" → {"a", ""}, mirroring the
/// Rust/Python split semantics this parser relies on). An empty input yields
/// no tokens.
inline std::vector<std::string_view> split_keep_empty(std::string_view s, char sep)
{
std::vector<std::string_view> out;
if (s.empty())
return out;
size_t start = 0;
for (size_t i = 0; i <= s.size(); i++) {
if (i == s.size() || s[i] == sep) {
out.push_back(s.substr(start, i - start));
start = i + 1;
}
}
return out;
}
/// Numeric value of a validated hex-group token (1-4 hex digits).
inline std::optional<uint16_t> hex_value(std::string_view tok)
{
if (!is_hex_group(tok))
return std::nullopt;
uint16_t v = 0;
for (char c : tok) {
uint16_t d;
if (c >= '0' && c <= '9')
d = static_cast<uint16_t>(c - '0');
else if (c >= 'a' && c <= 'f')
d = static_cast<uint16_t>(c - 'a' + 10);
else
d = static_cast<uint16_t>(c - 'A' + 10);
v = static_cast<uint16_t>((v << 4) | d);
}
return v;
}
/// Lowercase hex for one 16-bit group, with no leading zeros.
inline std::string hex_group(uint16_t v)
{
static const char digits[] = "0123456789abcdef";
std::string out;
bool started = false;
for (int shift = 12; shift >= 0; shift -= 4) {
uint16_t d = static_cast<uint16_t>((v >> shift) & 0xF);
if (d != 0 || started || shift == 0) {
out += digits[d];
started = true;
}
}
return out;
}
/// Same group rendered with a fixed 4-digit width: 0000..ffff.
inline std::string hex_group_padded(uint16_t v)
{
static const char digits[] = "0123456789abcdef";
std::string out;
for (int shift = 12; shift >= 0; shift -= 4)
out += digits[(v >> shift) & 0xF];
return out;
}
/// Render four octets as `a.b.c.d`. (The octet type is uint8_t, which already
/// guarantees range, but the signature keeps the symmetry with the other
/// ports.)
inline std::string ipv4_to_string(const std::array<uint8_t, 4>& octets)
{
return std::to_string(octets[0]) + "." + std::to_string(octets[1]) + "." +
std::to_string(octets[2]) + "." + std::to_string(octets[3]);
}
/// Count non-overlapping occurrences of "::" — used to enforce the
/// at-most-one-compression rule without pulling in <regex>.
inline size_t count_double_colon(std::string_view s)
{
size_t n = 0;
size_t pos = 0;
while ((pos = s.find("::", pos)) != std::string_view::npos) {
n++;
pos += 2;
}
return n;
}
/// Parse a dotted-decimal IPv4 string into four octets, validating each is
/// 0-255. Returns std::nullopt for anything that is not exactly four numeric
/// octets in range.
inline std::optional<std::array<uint8_t, 4>> parse_ipv4(std::string_view s)
{
std::string_view input = trim(s);
std::vector<std::string_view> parts = split_keep_empty(input, '.');
if (parts.size() != 4)
return std::nullopt;
std::array<uint8_t, 4> octets{};
for (size_t i = 0; i < 4; i++) {
if (!is_dec3(parts[i]))
return std::nullopt;
// is_dec3 guarantees digits only, so the accumulation cannot overflow.
uint32_t n = 0;
for (char c : parts[i])
n = n * 10 + static_cast<uint32_t>(c - '0');
if (n > 255)
return std::nullopt;
octets[i] = static_cast<uint8_t>(n);
}
return octets;
}
/// Parse an IPv6 string (with "::" compression, hex groups, and an optional
/// dotted-decimal IPv4 tail for mapped/compatible forms) into eight 16-bit
/// groups. Returns std::nullopt on any malformed input — never throws.
inline std::optional<std::array<uint16_t, 8>> parse_ipv6(std::string_view s)
{
std::string_view input = trim(s);
if (input.empty())
return std::nullopt;
// At most one "::" run is legal; reject ambiguous double-compression.
if (count_double_colon(input) > 1)
return std::nullopt;
// Branch on the position of "::" (if any). The two arms mirror each
// other: split into tokens, validate each, allow a dotted-quad only in
// the final slot, then assemble exactly eight groups.
size_t dc = input.find("::");
if (dc != std::string_view::npos) {
std::string_view before = input.substr(0, dc);
std::string_view after = input.substr(dc + 2);
std::vector<std::string_view> head_tokens =
before.empty() ? std::vector<std::string_view>{} : split_keep_empty(before, ':');
std::vector<std::string_view> tail_tokens =
after.empty() ? std::vector<std::string_view>{} : split_keep_empty(after, ':');
std::vector<uint16_t> head;
head.reserve(head_tokens.size());
for (std::string_view g : head_tokens) {
std::optional<uint16_t> v = hex_value(g);
if (!v)
return std::nullopt;
head.push_back(*v);
}
std::vector<uint16_t> tail;
tail.reserve(tail_tokens.size());
for (size_t i = 0; i < tail_tokens.size(); i++) {
std::string_view g = tail_tokens[i];
// A dotted-quad IPv4 tail is permitted only in the final slot,
// where it contributes two groups (high octet pair, low octet pair).
if (i == tail_tokens.size() - 1 && g.find('.') != std::string_view::npos) {
std::optional<std::array<uint8_t, 4>> oct = parse_ipv4(g);
if (!oct)
return std::nullopt;
tail.push_back(static_cast<uint16_t>(((*oct)[0] << 8) | (*oct)[1]));
tail.push_back(static_cast<uint16_t>(((*oct)[2] << 8) | (*oct)[3]));
} else {
std::optional<uint16_t> v = hex_value(g);
if (!v)
return std::nullopt;
tail.push_back(*v);
}
}
size_t total = head.size() + tail.size();
if (total >= 8)
return std::nullopt; // "::" must elide at least one group.
std::array<uint16_t, 8> groups{};
for (size_t k = 0; k < head.size(); k++)
groups[k] = head[k];
// The middle [head.size() .. 8 - tail.size()] stays zero — that is
// the elided run "::" stands in for.
for (size_t k = 0; k < tail.size(); k++)
groups[8 - tail.size() + k] = tail[k];
return groups;
}
// No compression: split on ':' and parse, allowing a dotted-quad only in
// the last slot. The result must be exactly eight groups.
std::vector<std::string_view> tokens = split_keep_empty(input, ':');
std::array<uint16_t, 8> groups{};
size_t n = 0;
for (size_t i = 0; i < tokens.size(); i++) {
std::string_view g = tokens[i];
if (i == tokens.size() - 1 && g.find('.') != std::string_view::npos) {
std::optional<std::array<uint8_t, 4>> oct = parse_ipv4(g);
if (!oct)
return std::nullopt;
if (n + 2 > 8)
return std::nullopt;
groups[n] = static_cast<uint16_t>(((*oct)[0] << 8) | (*oct)[1]);
groups[n + 1] = static_cast<uint16_t>(((*oct)[2] << 8) | (*oct)[3]);
n += 2;
} else {
std::optional<uint16_t> v = hex_value(g);
if (!v)
return std::nullopt;
if (n + 1 > 8)
return std::nullopt;
groups[n++] = *v;
}
}
if (n != 8)
return std::nullopt;
return groups;
}
/// True when the eight groups form an IPv4-mapped ("::ffff:") address.
inline bool is_mapped(const std::array<uint16_t, 8>& g)
{
return g[0] == 0 && g[1] == 0 && g[2] == 0 && g[3] == 0 && g[4] == 0 &&
g[5] == 0xFFFF;
}
/// True when the eight groups form an IPv4-compatible ("::") address.
inline bool is_compatible(const std::array<uint16_t, 8>& g)
{
return g[0] == 0 && g[1] == 0 && g[2] == 0 && g[3] == 0 && g[4] == 0 &&
g[5] == 0;
}
/// Collapse the longest run (length >= 2) of zero groups into "::" (first run
/// wins on ties) and strip leading zeros — RFC 5952 canonical text for pure-
/// hex IPv6. Works over any group span (8 for a whole address, 6 for the
/// high part of an embedded-IPv4 render). Does not emit dotted-decimal; call
/// `render_canonical` for that.
inline std::string compress_groups(const uint16_t* groups, size_t count)
{
size_t best_start = 0, best_len = 0, cur_start = 0, cur_len = 0;
// Track the longest run of consecutive zero groups. best_start records
// the first run of the longest length (strict > keeps earliest).
for (size_t i = 0; i < count; i++) {
if (groups[i] == 0) {
if (cur_len == 0)
cur_start = i;
cur_len++;
if (cur_len > best_len) {
best_len = cur_len;
best_start = cur_start;
}
} else {
cur_len = 0;
}
}
auto render = [&](size_t from, size_t to) {
std::string out;
for (size_t i = from; i < to; i++) {
if (i > from)
out += ':';
out += hex_group(groups[i]);
}
return out;
};
if (best_len < 2)
return render(0, count);
return render(0, best_start) + "::" + render(best_start + best_len, count);
}
/// Render a compressed high part followed by a dotted-decimal IPv4 tail. When
/// the high part already ends in "::" (its zero run reaches the boundary) the
/// IPv4 attaches directly; otherwise a single ":" separates them — so
/// "::ffff:" → "::ffff:a.b.c.d" and "::" → "::a.b.c.d".
inline std::string render_with_embedded_tail(const std::array<uint16_t, 6>& high,
const std::array<uint8_t, 4>& octets)
{
std::string high_str = compress_groups(high.data(), high.size());
std::string ipv4 = ipv4_to_string(octets);
bool bare = high_str.size() >= 2 && high_str[high_str.size() - 2] == ':' &&
high_str[high_str.size() - 1] == ':';
return bare ? high_str + ipv4 : high_str + ":" + ipv4;
}
/// Canonical RFC 5952 text for eight groups: a dotted-decimal tail for
/// IPv4-mapped ("::ffff:") addresses, otherwise pure compressed hex. The
/// deprecated IPv4-compatible range ("::/96") is NOT rendered dotted here —
/// that would mis-render the unspecified ("::") and loopback ("::1")
/// addresses as "::0.0.0.0" / "::0.0.0.1". Compatible extraction is still
/// available via `ipv6_to_ipv4`; on-demand compatible generation via
/// `ipv4_to_ipv6` is untouched.
inline std::string render_canonical(const std::array<uint16_t, 8>& groups)
{
if (is_mapped(groups)) {
std::array<uint8_t, 4> octets = {
static_cast<uint8_t>(groups[6] >> 8),
static_cast<uint8_t>(groups[6] & 0xFF),
static_cast<uint8_t>(groups[7] >> 8),
static_cast<uint8_t>(groups[7] & 0xFF),
};
std::array<uint16_t, 6> high = {groups[0], groups[1], groups[2],
groups[3], groups[4], groups[5]};
return render_with_embedded_tail(high, octets);
}
return compress_groups(groups.data(), groups.size());
}
/// Render eight groups as canonical compressed IPv6.
inline std::string ipv6_to_string(const std::array<uint16_t, 8>& groups)
{
return render_canonical(groups);
}
/// Expand an IPv6 string to its full eight-group, four-hex-digit form;
/// std::nullopt if invalid.
inline std::optional<std::string> expand_ipv6(std::string_view s)
{
std::optional<std::array<uint16_t, 8>> g = parse_ipv6(s);
if (!g)
return std::nullopt;
std::string out;
for (size_t i = 0; i < g->size(); i++) {
if (i > 0)
out += ':';
out += hex_group_padded((*g)[i]);
}
return out;
}
/// Compress an IPv6 string to its RFC 5952 canonical form; std::nullopt if
/// invalid.
inline std::optional<std::string> compress_ipv6(std::string_view s)
{
std::optional<std::array<uint16_t, 8>> g = parse_ipv6(s);
if (!g)
return std::nullopt;
return render_canonical(*g);
}
/// Embed an IPv4 octet quad into an IPv6 address. By default produces the
/// IPv4-mapped form "::ffff:a.b.c.d"; `EmbedMode::Compatible` yields
/// "::a.b.c.d"; a set `prefix` overrides both and places the IPv4 after any
/// custom /96 prefix (e.g. "64:ff9b::a.b.c.d"). Returns std::nullopt for an
/// invalid prefix.
inline std::optional<std::string> ipv4_to_ipv6(
const std::array<uint8_t, 4>& octets, const Ipv4ToIpv6Options& opts)
{
if (opts.prefix) {
std::optional<std::array<uint16_t, 8>> p = parse_ipv6(*opts.prefix);
if (!p)
return std::nullopt;
std::array<uint16_t, 6> high = {(*p)[0], (*p)[1], (*p)[2],
(*p)[3], (*p)[4], (*p)[5]};
return render_with_embedded_tail(high, octets);
}
if (opts.mode == EmbedMode::Compatible)
return render_with_embedded_tail({0, 0, 0, 0, 0, 0}, octets);
return render_with_embedded_tail({0, 0, 0, 0, 0, 0xFFFF}, octets);
}
/// Extract the embedded IPv4 from an IPv4-mapped ("::ffff:a.b.c.d") or
/// IPv4-compatible ("::a.b.c.d") address, returning dotted-decimal or
/// std::nullopt when the address carries no embedded IPv4 (or is
/// unparseable).
inline std::optional<std::string> ipv6_to_ipv4(std::string_view s)
{
std::optional<std::array<uint16_t, 8>> g = parse_ipv6(s);
if (!g || !(is_mapped(*g) || is_compatible(*g)))
return std::nullopt;
std::array<uint8_t, 4> octets = {
static_cast<uint8_t>((*g)[6] >> 8),
static_cast<uint8_t>((*g)[6] & 0xFF),
static_cast<uint8_t>((*g)[7] >> 8),
static_cast<uint8_t>((*g)[7] & 0xFF),
};
return ipv4_to_string(octets);
}
} // namespace ipconv
Also available in 13 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →