Base64 Encoding & Decoding in C++
C++ has no standard library Base64 support. Your options are a custom implementation, Boost.Beast's built-in codec, or OpenSSL. Here is a complete guide to all three approaches.
No stdlib support — your three options
Despite being one of the most common encoding tasks in network and web programming, Base64 has never made it into the C++ standard library. As of C++23 there is no std::base64_encode, no header to include, nothing in <string> or <algorithm> that handles it. The closest the language gets is the raw building blocks: bit shifts, lookup tables, and std::string.
In practice, C++ developers reach for one of three approaches depending on what is already in the project:
- A custom implementation. Base64 is simple enough that a complete, correct encoder and decoder fits in 50–100 lines. No dependencies, full control, easy to make URL-safe. This is the right choice for small projects or when you want zero external linkage.
- Boost.Beast. If you already use Boost (or are doing HTTP/WebSocket work), Beast ships a fast, header-only Base64 codec in its
detailnamespace. No linking required. - OpenSSL. If your application already links against OpenSSL for TLS or hashing, the
EVP_EncodeBlock/EVP_DecodeBlockfunctions are available with no extra dependency.
The sections below cover all three in working detail. If you only need to verify output or do a one-off conversion, the base64.dev tool handles it in the browser without writing any code.
Custom implementation with std::string
The standard Base64 alphabet maps every 6 bits of input to one printable ASCII character. Encoding works by taking three input bytes (24 bits) and splitting them into four 6-bit groups, each indexed into the alphabet. When the input length is not a multiple of three, the output is padded with = characters. Here is a complete, self-contained encoder:
#include <string>
#include <cstdint>
static const char kEncodeTable[] =
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz"
"0123456789+/";
std::string base64_encode(const unsigned char* data, std::size_t len) {
std::string out;
out.reserve(((len + 2) / 3) * 4);
std::size_t i = 0;
while (i + 3 <= len) {
std::uint32_t n = (data[i] << 16) | (data[i + 1] << 8) | data[i + 2];
out.push_back(kEncodeTable[(n >> 18) & 0x3F]);
out.push_back(kEncodeTable[(n >> 12) & 0x3F]);
out.push_back(kEncodeTable[(n >> 6) & 0x3F]);
out.push_back(kEncodeTable[n & 0x3F]);
i += 3;
}
if (std::size_t rem = len - i; rem > 0) {
std::uint32_t n = data[i] << 16;
if (rem == 2) n |= data[i + 1] << 8;
out.push_back(kEncodeTable[(n >> 18) & 0x3F]);
out.push_back(kEncodeTable[(n >> 12) & 0x3F]);
out.push_back(rem == 2 ? kEncodeTable[(n >> 6) & 0x3F] : '=');
out.push_back('=');
}
return out;
}
Decoding reverses the process. Rather than searching the alphabet for each character, build a reverse lookup table once that maps each ASCII byte back to its 6-bit value (and marks invalid bytes). Then read four input characters at a time and reassemble three output bytes:
#include <array>
static std::array<int, 256> build_decode_table() {
std::array<int, 256> t;
t.fill(-1);
for (int i = 0; i < 64; ++i)
t[static_cast<unsigned char>(kEncodeTable[i])] = i;
return t;
}
std::string base64_decode(const std::string& in) {
static const auto table = build_decode_table();
std::string out;
out.reserve((in.size() / 4) * 3);
std::uint32_t buffer = 0;
int bits = 0;
for (unsigned char c : in) {
if (c == '=') break;
int v = table[c];
if (v < 0) continue; // skip whitespace / newlines
buffer = (buffer << 6) | v;
bits += 6;
if (bits >= 8) {
bits -= 8;
out.push_back(static_cast<char>((buffer >> bits) & 0xFF));
}
}
return out;
}
The decoder above tolerates and skips embedded whitespace and newlines, which is important because MIME Base64 (RFC 2045) wraps lines at 76 characters. Treating any non-alphabet byte as a skip rather than an error makes the function robust against real-world input.
Use unsigned char when indexing the decode table. Indexing a 256-entry array with a plain char is undefined behavior on platforms where char is signed and the input has the high bit set (any byte > 127). The static_cast<unsigned char> in the table build and the for (unsigned char c : in) loop both guard against this.
Boost.Beast base64 (header-only)
If your project already pulls in Boost, the Beast library includes a Base64 codec that is both fast and header-only — no separate compilation or linking is needed. It lives in a detail namespace, which signals it is an implementation detail of Beast's HTTP support rather than a public API, but it is widely used directly and is stable across versions.
#include <boost/beast/core/detail/base64.hpp>
#include <string>
namespace base64 = boost::beast::detail::base64;
std::string encode(const std::string& input) {
// base64_encode returns a std::string directly
return boost::beast::detail::base64_encode(
reinterpret_cast<const std::uint8_t*>(input.data()),
input.size());
}
std::string decode(const std::string& encoded) {
std::string out;
out.resize(base64::decoded_size(encoded.size()));
auto const result = base64::decode(
out.data(), encoded.data(), encoded.size());
out.resize(result.first); // result.first = bytes written
return out;
}
The decode function returns a std::pair<std::size_t, std::size_t> where .first is the number of output bytes written and .second is the number of input characters consumed. You must resize the output string down to result.first afterward, because decoded_size only gives an upper bound (it assumes no padding).
Beast also exposes base64::encoded_size(n) which returns ((n + 2) / 3) * 4 — the exact encoded length including padding. Use it to pre-size buffers when you want to call the lower-level base64::encode(dest, src, len) overload instead of the convenience base64_encode wrapper.
OpenSSL EVP_EncodeBlock / EVP_DecodeBlock
When OpenSSL is already a dependency, its EVP layer offers single-call Base64 functions. EVP_EncodeBlock encodes a buffer and null-terminates the output; EVP_DecodeBlock reverses it. Both are declared in <openssl/evp.h> and return the number of characters/bytes produced.
#include <openssl/evp.h>
#include <string>
#include <vector>
std::string openssl_encode(const std::string& input) {
std::string out;
out.resize(4 * ((input.size() + 2) / 3)); // exact encoded size
int n = EVP_EncodeBlock(
reinterpret_cast<unsigned char*>(out.data()),
reinterpret_cast<const unsigned char*>(input.data()),
static_cast<int>(input.size()));
out.resize(n);
return out;
}
std::string openssl_decode(const std::string& encoded) {
std::vector<unsigned char> out(3 * (encoded.size() / 4));
int n = EVP_DecodeBlock(
out.data(),
reinterpret_cast<const unsigned char*>(encoded.data()),
static_cast<int>(encoded.size()));
if (n < 0) return {}; // decode error
return std::string(reinterpret_cast<char*>(out.data()), n);
}
The EVP_EncodeBlock path is clean, but decoding has a well-known wrinkle. EVP_DecodeBlock always returns a length that is a multiple of three and does not account for padding — it treats trailing = characters as zero bytes. So if your original data was 4 or 5 bytes (encoding to 8 characters with one or two padding signs), the decoder will hand back 6 bytes with one or two extra trailing zeros.
To get the true decoded length from EVP_DecodeBlock, subtract the padding count from the returned value: count the trailing = characters in the input (0, 1, or 2) and subtract that many bytes. For strict, padding-aware decoding instead use the streaming API — EVP_DecodeInit, EVP_DecodeUpdate, and EVP_DecodeFinal with an EVP_ENCODE_CTX — which reports the exact output size.
Link against OpenSSL with -lcrypto (the EVP encoding functions live in libcrypto, not libssl). See the dedicated OpenSSL Base64 guide for the full streaming-decode example.
URL-safe Base64 variant
Standard Base64 uses + and /, both of which have special meaning in URLs and file paths. The URL-safe variant (RFC 4648 §5) replaces + with - and / with _, and frequently omits the = padding entirely. With the custom implementation above, supporting it is a two-character change to the alphabet plus optional padding handling:
static const char kUrlSafeTable[] =
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz"
"0123456789-_";
std::string base64url_encode(const unsigned char* data, std::size_t len) {
std::string out = base64_encode(data, len); // standard encode
for (char& c : out) {
if (c == '+') c = '-';
else if (c == '/') c = '_';
}
// strip padding, common for tokens / JWT
while (!out.empty() && out.back() == '=')
out.pop_back();
return out;
}
Decoding URL-safe input is just as straightforward: translate - and _ back to + and / before feeding the string to the standard decoder. Because the streaming decoder shown earlier reconstructs bytes from the bit count rather than relying on padding, it handles unpadded input correctly without modification — you do not need to re-add the = characters.
std::string base64url_decode(std::string in) {
for (char& c : in) {
if (c == '-') c = '+';
else if (c == '_') c = '/';
}
return base64_decode(in); // bit-count decoder ignores missing padding
}
URL-safe Base64 is what you will encounter in JSON Web Tokens (JWT), URL query parameters, and many REST APIs. If you need both variants, pass the alphabet as a parameter to a single shared encode function rather than duplicating the body. The full topic is covered in URL-Safe Base64.
Encoding binary files with std::ifstream
To Base64-encode a file — an image, a PDF, a certificate — read it in binary mode and pass the raw bytes to any of the encoders above. The critical detail is opening the stream with std::ios::binary; without it, on Windows the runtime performs newline translation and corrupts binary data. Read the whole file into a buffer first:
#include <fstream>
#include <iterator>
#include <string>
#include <vector>
std::string encode_file(const std::string& path) {
std::ifstream file(path, std::ios::binary);
if (!file) return {}; // open failed
std::vector<unsigned char> bytes(
(std::istreambuf_iterator<char>(file)),
std::istreambuf_iterator<char>());
return base64_encode(bytes.data(), bytes.size());
}
The std::istreambuf_iterator pair reads the entire stream byte-for-byte without skipping whitespace, which a default std::istream_iterator would do. Note the extra parentheses around the first iterator argument — that disambiguation guards against the "most vexing parse," where the compiler would otherwise read the line as a function declaration.
For large files, reading the entire contents into memory before encoding is wasteful and can fail outright on multi-gigabyte inputs. Encode in fixed-size chunks instead, but always read in multiples of 3 bytes per chunk (e.g. 3072 bytes). Splitting input on a non-multiple-of-3 boundary would insert spurious padding mid-stream and produce invalid output. Only the final chunk should be allowed a remainder.
To decode back to a file, run the decoder and write the result to an std::ofstream opened with std::ios::binary, using file.write(data.data(), data.size()) rather than the << operator so that embedded null bytes are preserved.
FAQ
Is there a Base64 function in the C++ standard library?
No. As of C++23, there is no Base64 encoding in the C++ standard library. The most common approaches are: a custom lookup-table implementation (50-100 lines), Boost.Beast (header-only, no linking required), or OpenSSL (if already a dependency).
How do I use Boost.Beast for Base64 in C++?
Include <boost/beast/core/detail/base64.hpp>. Then: std::string encoded = boost::beast::detail::base64_encode(input_data, input_size); and for decode: std::string decoded; decoded.resize(boost::beast::detail::base64::decoded_size(encoded.size())); auto [n, m] = boost::beast::detail::base64::decode(decoded.data(), encoded.data(), encoded.size());
What is the output buffer size for Base64 encoding in C++?
The encoded size is ((input_size + 2) / 3) * 4 bytes. For decoding, the maximum output is (encoded_size / 4) * 3 bytes (exact size depends on padding). Always allocate at least this much buffer space.
Try Base64 encoding and decoding instantly
Paste any string or file — base64.dev auto-detects and converts it instantly.
Open base64.dev →