Une implémentation header-only (C++17)
encode prend l’alphabet et un indicateur de remplissage : la même fonction produit donc du Base64 standard et URL-safe. decode accepte une entrée avec ou sans remplissage, mais rejette les caractères invalides, les = mal placés, les longueurs impossibles et les bits de remplissage non nuls.
// base64.hpp
#pragma once
#include <array>
#include <cstdint>
#include <optional>
#include <string>
#include <string_view>
#include <vector>
namespace b64 {
inline constexpr std::string_view kStd =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
inline constexpr std::string_view kUrl =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
inline std::string encode(const void* data, std::size_t n,
std::string_view abc = kStd, bool pad = true) {
auto p = static_cast<const std::uint8_t*>(data);
std::string out;
out.reserve((n + 2) / 3 * 4);
std::size_t i = 0;
for (; i + 2 < n; i += 3) {
std::uint32_t v = p[i] << 16 | p[i + 1] << 8 | p[i + 2];
out += abc[v >> 18 & 63];
out += abc[v >> 12 & 63];
out += abc[v >> 6 & 63];
out += abc[v & 63];
}
if (i < n) {
std::uint32_t v = p[i] << 16 | (i + 1 < n ? p[i + 1] << 8 : 0);
out += abc[v >> 18 & 63];
out += abc[v >> 12 & 63];
if (i + 1 < n) out += abc[v >> 6 & 63];
if (pad) out.append(i + 1 < n ? 1 : 2, '=');
}
return out;
}
inline std::string encode(std::string_view s, std::string_view abc = kStd,
bool pad = true) {
return encode(s.data(), s.size(), abc, pad);
}
inline std::optional<std::vector<std::uint8_t>> decode(
std::string_view s, std::string_view abc = kStd) {
std::array<int, 256> val;
val.fill(-1);
for (int i = 0; i < 64; ++i) val[static_cast<unsigned char>(abc[i])] = i;
std::size_t len = s.size();
if (len % 4 == 0 && len > 0 && s[len - 1] == '=')
len -= (s[len - 2] == '=') ? 2 : 1;
if (len % 4 == 1) return std::nullopt;
if (s.size() != len && s.size() % 4 != 0) return std::nullopt;
std::vector<std::uint8_t> out;
out.reserve(len * 3 / 4);
std::uint32_t acc = 0;
int bits = 0;
for (std::size_t i = 0; i < len; ++i) {
int v = val[static_cast<unsigned char>(s[i])];
if (v < 0) return std::nullopt;
acc = (acc << 6) | static_cast<std::uint32_t>(v);
bits += 6;
if (bits >= 8) {
bits -= 8;
out.push_back(static_cast<std::uint8_t>(acc >> bits));
acc &= (1u << bits) - 1;
}
}
if (acc != 0) return std::nullopt; // leftover bits must be zero
return out;
}
} // namespace b64
Le décodeur fonctionne comme un accumulateur de bits : chaque caractère ajoute 6 bits et chaque tranche complète de 8 bits devient un octet. Ce qui reste à la fin, ce sont les bits de remplissage, et ils doivent valoir zéro. La page le fonctionnement de Base64 explique pourquoi.
Encoder et décoder des chaînes
#include "base64.hpp"
#include <iostream>
int main() {
std::string enc = b64::encode("héllo wörld");
std::cout << enc << '\n'; // aMOpbGxvIHfDtnJsZA==
if (auto bytes = b64::decode(enc))
std::cout << std::string(bytes->begin(), bytes->end()) << '\n'; // héllo wörld
}
Une std::string contient des octets. Avec un fichier source en UTF-8 (le défaut de GCC et Clang ; MSVC demande /utf-8), les littéraux sont en UTF-8 et la sortie correspond à celle de tous les autres langages. Pour std::u8string en C++20, passez s.data() et s.size() à la surcharge qui prend un pointeur.
URL-safe et sans remplissage
std::string q = "<<?\?\?>>"; // "<<???>>", escaped (see below)
b64::encode(q, b64::kUrl); // PDw_Pz8-Pg==
b64::encode(q, b64::kUrl, false); // PDw_Pz8-Pg
b64::decode("PDw_Pz8-Pg", b64::kUrl); // bytes of "<<???>>"
Les échappements sont là parce que ??> est un trigraphe en C++ avant C++17 ; GCC le remplace en mode strict -std=c++11 / c++14. Les JWT et la plupart des jetons utilisent du Base64URL sans remplissage ; voir Base64 ou Base64URL.
Résultats de validation
Ce que renvoie decode, tout a été vérifié :
| Entrée | Résultat |
|---|---|
aGk= | 2 octets |
aGk | 2 octets (absence de remplissage acceptée) |
aA== / aA | 1 octet |
aGl= | nullopt (bits de remplissage non nuls) |
aGk== | nullopt (longueur incorrecte) |
a=== | nullopt (trop de remplissage) |
aGk=!! | nullopt |
PDw_Pz8-Pg== | nullopt avec kStd |
Nous avons compilé avec g++ -std=c++17 -Wall -Wextra -Wpedantic -fsanitize=address,undefined, fait l’aller-retour sur 5 000 tampons aléatoires dans les deux alphabets, comparé l’encodeur avec b64encode de Python et vérifié la décision d’acceptation ou de rejet pour 30 000 chaînes aléatoires face à une référence Python. Aucune divergence. Pour vérifier une chaîne à la main, le validateur Base64 explique chaque rejet.
Fichiers
#include <fstream>
#include <iterator>
std::ifstream f("hello.txt", std::ios::binary);
std::string data((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
std::cout << b64::encode(data) << '\n'; // aGVsbG8K
auto raw = b64::decode("aGVsbG8K");
std::ofstream("copy.txt", std::ios::binary)
.write(reinterpret_cast<const char*>(raw->data()),
static_cast<std::streamsize>(raw->size()));
std::ios::binary compte sous Windows, où le mode texte convertit les fins de ligne et corrompt les données binaires. Pour les gros fichiers, lisez des blocs dont la taille est un multiple de 3 et appelez encode sur chacun : les sorties se concatènent proprement. La page Base64 en C montre cette boucle. Pour transformer le résultat en tableau C à intégrer, l’outil Base64 en tableau d’octets le génère.
Les bibliothèques que vous avez peut-être déjà
Si l’une d’elles est déjà une dépendance, son API Base64 vous épargne un fichier :
| Bibliothèque | Encoder | Décoder |
|---|---|---|
| OpenSSL | EVP_EncodeBlock | EVP_DecodeBlock (renvoie une longueur qui inclut les octets de remplissage) |
| Abseil | absl::Base64Escape, absl::WebSafeBase64Escape | absl::Base64Unescape, absl::WebSafeBase64Unescape |
| Qt | QByteArray::toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals) | QByteArray::fromBase64 |
| Poco | Poco::Base64Encoder (un ostream) | Poco::Base64Decoder (un istream) |
| Boost | boost::beast::detail::base64 (interne, pas une API stable) | idem |
Vérifiez comment chaque bibliothèque traite le remplissage et les espaces avant de compter sur elle pour valider : elles diffèrent. Par exemple, EVP_DecodeBlock ne retire pas le remplissage de sa valeur de retour : aGk= annonce 3 octets, pas 2.
Pièges
- Un
charsigné comme index.table[s[i]]a un comportement indéfini pour les octets au-delà de 127 quandcharest signé. Convertissez enunsigned charcomme ci-dessus. - Traiter les octets décodés comme une chaîne C. Les octets décodés peuvent contenir
\0; ne les passez jamais àstrlenni à une API qui prend un simpleconst char*sans longueur. - Dimensionner les tampons à
n * 4 / 3. Il manque jusqu’à 3 caractères. La formule exacte se trouve dans le guide du surcoût de taille.
FAQ
Le C++ a-t-il une fonction Base64 standard ?
Non. Ni la bibliothèque standard ni aucune proposition intégrée jusqu’à C++23 n’en contient. Utilisez une petite implémentation comme celle de cette page, ou l’API d’une bibliothèque dont votre projet dépend déjà.
decode doit-il renvoyer std::string ou std::vector<uint8_t> ?
Du Base64 décodé, c’est du binaire : std::vector<std::uint8_t> le dit honnêtement. Si vous savez que le contenu est du texte, construisez une std::string à partir des itérateurs du vecteur.
Pourquoi std::optional plutôt que des exceptions ?
Du Base64 invalide saisi par un utilisateur est un cas attendu, pas exceptionnel. std::optional oblige l’appelant à le traiter et ne coûte rien quand tout va bien. Levez une exception dans une fonction d’enrobage si votre base de code les préfère.