Приходится иметь дело с форматом Base64? Тогда этот сайт идеально вам подойдет! Воспользуйтесь нашим невероятно удобным онлайн-инструментом для кодирования или декодирования ваших данных.

Кодирование Base64 в C++ (Cpp): полное руководство

Обратная задача - та, где заголовок крупнее: у вас есть байты - сертификат, изображение, случайный блок байтов, подпись - и их нужно провести через что-то, что понимает только текст: поле JSON, заголовок письма, URL, переменная окружения. Домашняя страница этого сайта разбирает формат вглубь, поэтому здесь только короткая версия: три байта становятся четырьмя символами алфавита, короткий хвост получает один-два знака =, а закодированная форма примерно на 33 процента больше исходной. Кодирование - это направление роста, поэтому каждый буфер в этой статье рассчитан под него, а арифметика - однострочник 4 * ((n + 2) / 3), который не меняется, какой бы кодировщик вы ни выбрали.

Как и на стороне декодирования, сам C++ не закодирует за вас ни одного байта. У стандартной библиотеки было тридцать лет, чтобы вырастить функцию base64, и она потратила их все на другие вещи, поэтому каждая C++-программа приносит свой кодировщик с лавки четырёх очень разных характеров, плюс вариант написать сорок с небольшим строк самому. Один - рабочая лошадка, которая везёт TLS с 1990-х и которая добавляет заполнение, завершает нулём и переносит строки, не спрашивая разрешения. Второй - быстрый кодек только из заголовка, прячущийся в пространстве имён, которое авторы подписали «detail». Третий - итератор 2002 года, который, похоже, никогда не встречал знак заполнения. Четвёртый - функция, которую операционная система поставляет десятилетиями и которая дописывает CRLF в конец вашего токена. А пятый вариант - ваш. Когда вы узнаете, что каждый из них добавляет, в чём отказывает и что тихо дописывает, кодирование перестанет быть источником ошибок в единицу. Приступим к упаковке.

Стандарт никогда не поставлял упаковщик

Каждый стандарт начиная с C++98 - а их вышло восемь, вплоть до C++26, - посмотрел на 64-символьный алфавит и пошёл дальше. Нет ни <base64>, ни std::base64, в <string> и <vector> нет ничего, что упаковало бы ваши байты. Техническая работа над C++26 завершена и принята голосованием (114-12-3) на мартовском совещании ISO C++ 2026 года в Кройдоне, Великобритания, и да, он добавляет заголовок <text_encoding> для работы с текстовыми кодеками; следующие совещания комитета, в июне 2026 (Брно) и в ноябре 2026 (Бюзиос, Бразилия), открывают рабочую версию C++29, а не возвращаются к C++26. Base64 в стандарте нет, и упрекать комитет тут не в чём: текстовое кодирование - про наборы символов, а base64 - про байты, так что новый заголовок никогда не был для него правильным домом. На деле работу сделала экосистема. EVP-рутины base64 от OpenSSL есть в каждом релизе OpenSSL, библиотеки Boost несут два независимых кодировщика, Windows поставляет функцию CryptoAPI с таблицей флагов для этой работы, а сорокастрочный сниппет копируется и вставляется по всему языку с 2008 года. Если ваш проект на CMake, вся настройка зависимостей умещается в три строки:

find_package(OpenSSL REQUIRED)
find_package(Boost REQUIRED)
target_link_libraries(my_app PRIVATE OpenSSL::Crypto)

Стоит запомнить релиз Boost 1.92.0 от августа 2026 года, от проекта, основанного в 1998 году и поставляющего библиотеки с первого релиза в 1999 году. Оба кодировщика Boost ниже состоят только из заголовков - линковать вообще нечего, - а OpenSSL требует -lcrypto, который большинство C++-программ, трогающих TLS, уже держат в бинарнике.

Сначала математика: размер каждого буфера в этой статье

Base64 группирует байты по трое, поэтому у длины выхода есть форма, которая перестаёт удивлять, как только вы её знаете: на каждые 3 входных байта выходят 4 символа, а короткий хвост дополняется до полной группы. Точный подсчёт для n байтов входа:

4 * ((n + 2) / 3)

Это +2 - трюк потолка: целочисленное деление округляет вниз, так что предварительное прибавление 2 заставляет его округляться вверх до следующего кратного трём числа. Отсюда каждый размер буфера в этой статье - подстановка. Разовая функция OpenSSL хочет буфер, который вместит закодированные данные плюс NUL, который она дописывает в конце, - man-страница иллюстрирует договорённость примером: 16 входных байтов превращаются в 24 закодированных байта плюс 1 NUL, всего 25 байтов, а функция возвращает длину без NUL. Её потоковый путь обрабатывает вход блоками по 48 байт, и man-страница задаёт выход 65 байт на блок (64 символа плюс перевод строки, который каждый блок всегда производит), плюс ещё один байт под NUL. Заголовок Boost.Beast выдаёт вам точную формулу в виде constexpr-функции. А свой код резервирует (n + 2) / 3 * 4 и считает дело сделанным. Вот числа, на которые вы реально упрётесь:

Вход Выход (с заполнением) На что обратить внимание
1 байт 4 символа Самая малая дополненная форма: QQ==
2 байта 4 символа Три символа данных и одно заполнение
3 байта 4 символа Одна полная группа, заполнения совсем нет
48 байт 64 символа Ровно один блок потокового кодирования OpenSSL
500 байт 668 символов Перенесите по 64 - получится 11 строк, 679 символов с учётом переводов строк
1 ГБ примерно 1,33 ГБ Заложите налог в размер колонки, файла и канала

Если принимающая сторона - колонка фиксированного размера, буфер или строка в текстовом файле, эта формула и есть весь проект. Единственное направление, в котором она может укусить, - обратное: стороне декодирования нужно 3n/4 минус знаки заполнения, и буфер декодирования, рассчитанный по формуле кодирования, - классическая завышенная аллокация, которая вырастает в тикет про баг памяти. Рассчитывать сжимающее направление - дело сестринского гайда; здесь вы только растёте.

Вот картина, потому что различия все в добавках - в заполнении, переводах строк, NUL, - а не в ядре упаковки, которое каждая строка таблицы реализует идентично:

Кодировщик Откуда берётся Заполнение Дополнительные байты, которые нужно заложить Особенность, которую стоит запомнить
EVP_EncodeBlock <openssl/evp.h>, линкуется -lcrypto Всегда 1 (NUL в буфере) Пример на 16 байт из man-страницы - это договорённость
EVP_EncodeUpdate + Final то же самое Всегда 65 на каждый 48-байтовый блок Жёстко переносит по 64 символа, каждый блок заканчивается переводом строки
Boost.Beast encode boost/beast/core/detail/base64.hpp, только заголовки Всегда 0 Живёт в пространстве имён под названием detail
Итераторы Boost.Serialization boost/archive/iterators/base64_from_binary.hpp, только заголовки Никогда 0 - одно или два знака заполнения добавляете сами Самый старый кодировщик в наборе инструментов, 2002 год
CryptBinaryToStringA wincrypt.h, crypt32.lib Всегда 2 (CRLF), если не NOCRLF Есть URL-безопасный флаг, которого нет у остального набора
Ваши сорок строк Нигде: это ваше Ваш выбор Ваш выбор Каждый крайний случай ваш навсегда

Ядровой алгоритм в каждой строке один и тот же - вот что утешает в формате 1987 года. Разница в том, что каждая реализация добавляет вокруг пелода, и почти каждая ловушка в этой статье - это одна из этих добавок, столкнувшаяся с потребителем, который её не ждал.

OpenSSL: кодировщик, который уже линкует ваш TLS-стек

Если ваша программа уже линкует OpenSSL ради TLS, добавлять нечего. Разовая функция - это один вызов:

int EVP_EncodeBlock(unsigned char *t, const unsigned char *f, int n);

Дайте ей исходные байты и длину, и она запишет дополненную однострочную кодировку. Договорённость стоит запомнить, потому что man-страница формулирует её с примером: на каждые 3 байта входа - 4 байта выхода; хвост, не делящийся на 3, дополняется так, что выход всегда делится на 4; и сверху добавляется завершающий символ NUL. Задокументированный пример: 16 байт на входе, 24 закодированных байта плюс 1 NUL, в буфере всего 25 байтов, а функция возвращает 24 - длину без NUL. Подберите размер буфера соответствующим образом, и обёртка умещается в несколько строк:

#include <cstddef>
#include <cstdio>
#include <string>
#include <openssl/evp.h>

std::string openssl_encode(const std::string &in) {
  std::string out;
  out.resize(4 * ((in.size() + 2) / 3) + 1);
  int n = EVP_EncodeBlock(reinterpret_cast<unsigned char *>(out.data()),
                          reinterpret_cast<const unsigned char *>(in.data()),
                          static_cast<int>(in.size()));
  if (n < 0) return {};
  out.resize(static_cast<size_t>(n));
  return out;
}

int main() {
  std::printf("%s\n", openssl_encode("Mane").c_str());
  std::printf("%s\n", openssl_encode("M").c_str());
  std::printf("%s\n", openssl_encode("").c_str());
}

Обратите внимание, что делает std::string там, где C заставила бы вас это делать: он растёт ровно до возвращённой длины, так что NUL, который дописал OpenSSL, просто за пределами учётной длины и никогда не становится частью пелода. Закодируйте «Mane» - получите TWFuZQ==, классический четырёхсимвольный хвост с одним заполнением; закодируйте один байт - получите пару из двух символов данных в костюме двухсимвольного заполнения; не кодируйте ничего - получите пустую строку, единственный случай, когда base64-кодировщик ведёт себя ровно как тождественная функция. Единственная настоящая логика во всей функции - resize: он превращает «записанные байты плюс NUL» в «ровно пелод».

Для данных, которые прибывают кусками, - файла, сокета, потока, который не хочется буферизовать, - у OpenSSL есть контекст, который вы кормите и завершаете, а блочная арифметика man-страницы необычно ясна. Немедленно обрабатываются только полные блоки по 48 байт; любой остаток держится внутри контекста и выпускается следующим вызовом или финальным. Каждый обработанный блок пишет 64 символа плюс перевод строки - 65 байт, - а финальный вызов разбирается с частичным блоком, отсюда и задокументированный потолок 65 байт плюс NUL. Последствие, о котором стоит знать до вызова: этот API переносит строки по 64 символа. Настроить это нельзя. Именно так выглядит потоковый кодировщик.

#include <algorithm>
#include <cstdio>
#include <string>
#include <vector>
#include <openssl/evp.h>

std::string openssl_encode_wrapped(const std::string &in) {
  EVP_ENCODE_CTX *ctx = EVP_ENCODE_CTX_new();
  EVP_EncodeInit(ctx);
  std::string out;
  out.reserve(4 * ((in.size() + 2) / 3) + in.size() / 48 + 2);
  std::vector<unsigned char> buf(128);
  int outl = 0;
  for (size_t pos = 0; pos < in.size();) {
    size_t take = std::min<size_t>(48, in.size() - pos);
    EVP_EncodeUpdate(ctx, buf.data(), &outl,
                     reinterpret_cast<const unsigned char *>(in.data()) + pos,
                     static_cast<int>(take));
    out.append(reinterpret_cast<const char *>(buf.data()), outl);
    pos += take;
  }
  EVP_EncodeFinal(ctx, buf.data(), &outl);
  out.append(reinterpret_cast<const char *>(buf.data()), outl);
  EVP_ENCODE_CTX_free(ctx);
  return out;
}

int main() {
  std::string s = openssl_encode_wrapped(std::string(500, 'A'));
  std::printf("500 bytes -> %zu chars\n", s.size());
  int lines = 0;
  size_t longest = 0, run = 0;
  for (char c : s) {
    if (c == '\n') { lines++; run = 0; }
    else run++;
    longest = std::max(longest, run);
  }
  std::printf("lines=%d longest=%zu lastchar=%c\n", lines, longest, s.back());
}

Скармливаете ей 500 байт буквы A, и арифметика сходится ровно так, как обещала man-страница: 668 закодированных символов, и поскольку вывод режется на строки по 64 символа, получается 11 строк, 679 символов всего, и самый последний символ - перевод строки. Именно этот завершающий перевод строки ломает потребителей: вставьте результат в JSON-строку, и у вас будет управляющий символ там, где должна быть кавычка; используйте его как сегмент токена, и вы изобрели новый сегмент. Правило большого пальца: блочный API - для однострочных пелодов (токены, заголовки, значения конфигурации), потоковый API - когда потребителю нужен MIME-вид обёрнутого вывода, а если сомневаетесь, срезайте завершающий перевод строки циклом while (out.back() == '\n'), прежде чем пелод пересечёт границу, которая его не ждёт.

Boost.Beast: быстрый упаковщик в пространстве имён detail::

HTTP-библиотека Boost поставляет base64-кодек по непривычному адресу boost/beast/core/detail/base64.hpp. Пространство имён detail:: - это способ Boost сказать «это наши внутренние дела», и мейнтейнеры отказались повышать кодек до публичного API. Тем не менее его используют все: он малый, он быстрый, он состоит только из заголовка (определите BOOST_BEAST_HEADER_ONLY перед include, и линковать нечего), и это тот же самый кодек, которым собственное WebSocket-рукопожатие Boost.Beast пользуется для вычисления Sec-WebSocket-Accept, так что он годами жуёт настоящий трафик.

На стороне кодирования API почти оскорбительно спокойно. constexpr-хелпер даёт точный размер вывода, 4 * ((n + 2) / 3), та же формула, что в разделе про математику, только теперь её проверяет компилятор, а функция encode пишет дополненный результат в ваш буфер и говорит, сколько символов использовала. Канала ошибок нет, потому что кодирование не может провалиться: любой байт - валидный вход, а длина вывода - чистая функция длины входа. Обёртка:

#define BOOST_BEAST_HEADER_ONLY
#include <boost/beast/core/detail/base64.hpp>
#include <cstddef>
#include <cstdio>
#include <string>

namespace b64 = boost::beast::detail::base64;

std::string beast_encode(const std::string &in) {
  std::string out(b64::encoded_size(in.size()), '\0');
  std::size_t n = b64::encode(out.data(), in.data(), in.size());
  out.resize(n);
  return out;
}

int main() {
  std::printf("%s\n", beast_encode("Mane").c_str());
  std::printf("%s\n", beast_encode("M").c_str());
}

Закодируйте «Mane» - получите TWFuZQ==; закодируйте единственный байт M - получите TQ== - те же байты, что выдавала обёртка OpenSSL, только без NUL, о котором нужно думать, и без строк, которые нужно срезать. Две вещи, которые стоит спрятать в карман. Первая, происхождение: на исходниках стоит копирайт 2016-2019 за Vinnie Falco, а в подвале часть кода приписана сниппету Rene Nyffenegger 2004-2008 годов, - та же народная песня, с которой началась C++-история base64, теперь едущая внутри Boost, в вашем бинарнике, и делающая WebSocket-рукопожатия для всего веба. Вторая, практическая: поскольку кодек дополняет, но никогда не переносит строки, это правильный инструмент для всего, что должно остаться в одну строку, - токены, заголовки, API-пелоды, - а формула encoded_size даёт буфер ровно нужного размера, никогда не приближение.

Boost.Serialization: итератор, который забыл, что заполнение существует

Самый старый base64 в экосистеме C++ - это не функция, а набор компонуемых адаптеров-итераторов, написанных Робертом Рэйми в 2002 году для библиотеки сериализации Boost. Направление кодирования - цепочка из двух адаптеров: трансформатор ширины, который пересобирает сырые байты восемь к шести, и итератор, который превращает каждое пересобранное значение в символ алфавита:

#include <boost/archive/iterators/base64_from_binary.hpp>
#include <boost/archive/iterators/transform_width.hpp>
#include <cstddef>
#include <cstdio>
#include <string>

namespace it = boost::archive::iterators;

std::string boost_iter_encode(const std::string &in) {
  using enc =
      it::base64_from_binary<it::transform_width<const char *, 6, 8>>;
  std::string out(enc(in.data()), enc(in.data() + in.size()));
  switch (in.size() % 3) {
    case 1: out += "=="; break;
    case 2: out += '=';  break;
    default: break;
  }
  return out;
}

int main() {
  std::printf("%s\n", boost_iter_encode("Mane").c_str());
  std::printf("%s\n", boost_iter_encode("M").c_str());
}

Итератор делает ядро упаковки и ничего больше: без заполнения, без NUL, без переводов строк и без канала ошибок, потому что ядро упаковки не может провалиться. Закодируйте «Mane», и итератор с самым серьёзным видом выдаст шесть символов, TWFuZQ: настоящая кодировка четырёх байт - это восемь символов, и итератору 2002 года никогда не приходило в голову этим интересоваться. Именно поэтому оператор switch несущий, а не декоративный: не хватает одного байта до группы - два знака заполнения, не хватает двух байтов - один. Та же цепочка без switch - это то, что вы получите, если забудете этот шаг, и результат - строка, которая декодируется без проблем снисходительным декодером, отклоняется строгим, и превращает сообщение об ошибке вашего API-потребителя в загадку. (Декодирующая сторона этой же семьи итераторов - та, что бросает исключение на единственном лишнем пробеле, подробности - в сестринском гайде.)

Сорок строк, ноль зависимостей

Base64 достаточно мал, так что держать собственный корректный кодировщик - вполне почётное занятие, а на C++ выгода ещё больше, чем на любом другом языке: std::string делает управление буфером приятным, формула даёт точный размер заранее, а кустарный кодировщик - тот, у которого нет никаких притязаний: ни NUL, ни переводов строк, ни платформенных привычек, - а это ровно то, что нужно под файлом конфигурации или на границе API. Эта версия упаковает группами по 3 байта по таблице из 64 символов:

#include <cstddef>
#include <cstdio>
#include <string>

std::string base64_encode(const std::string &in) {
  static const char *table =
      "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
  std::string out;
  out.reserve((in.size() + 2) / 3 * 4);
  const unsigned char *p =
      reinterpret_cast<const unsigned char *>(in.data());
  size_t n = in.size();
  for (size_t i = 0; i < n; i += 3) {
    unsigned v = p[i] << 16;
    if (i + 1 < n) v |= p[i + 1] << 8;
    if (i + 2 < n) v |= p[i + 2];
    out.push_back(table[(v >> 18) & 63]);
    out.push_back(table[(v >> 12) & 63]);
    out.push_back(i + 1 < n ? table[(v >> 6) & 63] : '=');
    out.push_back(i + 2 < n ? table[v & 63] : '=');
  }
  return out;
}

int main() {
  std::printf("%s\n", base64_encode("Mane").c_str());
  std::printf("%s\n", base64_encode("M").c_str());
  std::printf("%s\n", base64_encode("M\312\277").c_str());
}

Разберём по частям. Строка reserve - это раздел про математику: ровно (n + 2) / 3 * 4 символа, так что перераспределения посреди цикла не будет. reinterpret_cast в const unsigned char * - не ритуал: на платформах, где char со знаком, байт выше 127 без этого стал бы отрицательным числом, и в момент, когда он коснулся бы индекса таблицы, получилось бы неопределённое поведение в халате учёного. Каждая итерация тянет до трёх байтов в 24-битовое значение, проталкивает четыре шестибитовых среза в таблицу, а на хвосте выдаёт = вместо тех байтов, которых не было, - стражи i + 1 < n и i + 2 < n и есть вся логика заполнения. Скармливаете «Mane» - получаете TWFuZQ==. Скармливаете одиночный M - получаете TQ==. Скармливаете байты выше 127, пару 0xCA 0xBF из третьей строки примера, - и вывод остаётся чистым ASCII (Tcq/), потому что байт выше 127 - это просто байт, и таблице всё равно, что он значит. Сорок строк, без зависимостей, и каждый крайний случай - это строка, которую написали вы, а в этом и весь смысл.

Windows CryptoAPI: упаковщик, встроенный в ОС

На Windows base64-кодировщик есть прямо в операционной системе, и он старше большинства фреймворков в этой статье: CryptBinaryToStringA из wincrypt.h, в crypt32.lib, часть CryptoAPI, который поставляется с Windows десятилетиями. Он превращает массив байтов в форматированную строку, и его таблица флагов читается как меню, перечисляющее всю историю формата:

Флаг Значение Что вы получите
CRYPT_STRING_BASE64HEADER 0x0 Base64, обёрнутый в строки-заголовки сертификата BEGIN/END
CRYPT_STRING_BASE64 0x1 Чистый base64, без заголовков
CRYPT_STRING_BASE64URI 0xD URL-безопасный алфавит: + становится -, / становится _, по разделу 5 RFC 4648
CRYPT_STRING_NOCRLF 0x40000000 Без перевода строки в конце
CRYPT_STRING_NOCR 0x80000000 Голый LF вместо CRLF по умолчанию

Первое, что нужно знать, - значение по умолчанию: если вы не передадите CRYPT_STRING_NOCRLF, функция допишет пару «возврат каретки / перевод строки» в конец вашей строки - задокументированное поведение состоит в том, что каждый небинарный формат получает последовательность перевода строки, - так что base64-токен, который должен поместиться в одну строку, хочет BASE64 | NOCRLF, и эта комбинация - идиоматичный вызов. Второе - порядок вызова, классическая Windows-двойка: вызвать с NULL-буфером, чтобы спросить, сколько места нужно (ответ включает завершающий NUL), выложить память, вызвать снова и считать длину без NUL:

#include <windows.h>
#include <wincrypt.h>
#include <cstddef>
#include <string>

std::string win32_encode(const std::string &in,
                         DWORD flags = CRYPT_STRING_BASE64) {
  DWORD need = 0;
  if (!CryptBinaryToStringA(reinterpret_cast<const BYTE *>(in.data()),
                            static_cast<DWORD>(in.size()),
                            flags | CRYPT_STRING_NOCRLF,
                            nullptr, &need))
    return {};
  std::string out(need, '\0');
  DWORD got = 0;
  if (!CryptBinaryToStringA(reinterpret_cast<const BYTE *>(in.data()),
                            static_cast<DWORD>(in.size()),
                            flags | CRYPT_STRING_NOCRLF,
                            out.data(), &got))
    return {};
  out.resize(got);
  return out;
}

Ещё две заметки. URI-флаг - единственный нативный base64url во всей этой статье: на Windows можно кодировать алфавит токенов напрямую, а подход с транскодированием из раздела ниже - строго для других платформ. И строка CRYPT_STRING_BASE64HEADER со значением 0 - это также тот флаг, который вы получите, если передадите ноль, так что вызов, который «имел в виду» вообще никаких флагов, тихо обёрнёт пелод в строки-заголовки сертификата, - привычка PEM-эпохи к кадрированию, полезная для генерации .pem-файлов и неожиданная для всего остального. Пролинкуйте crypt32.lib, и функция ваша на всё оставшееся время жизни программы.

Base64url: алфавит для токенов и URL

В стандартном алфавите есть два символа, которые не переживают URL: в строке запроса + означает пробел, а в пути / означает каталог. Раздел 5 RFC 4648 чинит это двумя обменами символов - + становится -, а / становится _ - и откровенно говорит о результате: эту кодировку «не следует считать той же самой, что base64-кодировка». Это алфавит JWT, код-вызовов OAuth PKCE, идентификаторов видео YouTube и большинства API-токенов, и он регулярно сбрасывает и заполнение =, потому что в токене длина известна неявно, а дополняющие знаки были бы всего лишь процент-экранированием, которое обязательно случится.

Среди кодировщиков этой статьи нативно выдаёт этот алфавит только флаг Windows: у OpenSSL нет URL-безопасного режима, и ни у одного из вариантов Boost тоже, - так что на большинстве платформ рецепт таков: закодировать в стандарт, заменить два символа, сбросить заполнение. Это десяток с небольшим строк:

#include <cstddef>
#include <cstdio>
#include <string>

/* base64_encode из раздела "Сорок строк, ноль зависимостей" */

std::string base64url_encode(const std::string &in, bool pad = false) {
  std::string out = base64_encode(in);
  for (char &c : out) {
    if (c == '+') c = '-';
    else if (c == '/') c = '_';
  }
  if (!pad)
    while (!out.empty() && out.back() == '=')
      out.pop_back();
  return out;
}

int main() {
  std::printf("%s\n", base64url_encode("M\312\277").c_str());
  std::printf("%s\n", base64url_encode("M").c_str());
  std::printf("%s\n", base64url_encode("M", true).c_str());
}

Первая строка вывода - Tcq_, где стандартный алфавит написал бы /; две следующие показывают переключатель заполнения в действии: TQ без заполнения по умолчанию и TQ==, когда потребителю нужно вернуть его обратно. Аргумент pad - тот, о котором стоит подумать, потому что потребители не согласны: сегменты JWT хотят без заполнения, PKCE-вызовы хотят без заполнения, но base64url-значение, которое попадает в поле, где декодер строг к длине, может захотеть вернуть его, и переключатель - это bool, а не переписывание. И не забудьте про режим отказа в обратную сторону: - внутри пелода стандартного алфавита просто недопустимо, так что два алфавита не взаимозаменяемы на уровне байтов: токен, закодированный не тем алфавитом, не декодируется, он падает, - а это именно тот сбой, который нужен на границе безопасности.

Перенос строк: 64, 76 или никогда

Обёрнутый base64 в живой природе имеет три длины строки, и у каждой своя история. Потоковый кодировщик OpenSSL жёстко за 64 символа - PEM-привычка, где стандарт Privacy-Enhanced Mail 1987 года обёртывал по 64. MIME, стандартизовав кодировку для почты в 1993 году, перешёл на 76 символов, и это число по умолчанию у команды coreutils base64 (флаг -w задаёт ширину, а -w 0 отключает перенос полностью) и у большинства инструментов экосистемы. Сам RFC 4648 не занимает чью-либо сторону: он ссылается на 76 как на лимит MIME и говорит реализациям не переносить вообще, если только ссылающаяся спецификация не велит иначе. Что именно вы выдаёте, зависит от того, кто потребляет, и потребитель, а не формат, - и есть проектное ограничение.

Перенос строк - это постобработка закодированной строки, никогда не входной этап: группы из четырёх символов - единица смысла, поэтому разрез строки в любом кратном ширине месте - безопасный разрез, каждая граница строки приходится между группами. C++-версия - цикл:

#include <cstddef>
#include <cstdio>
#include <string>

/* base64_encode из раздела "Сорок строк, ноль зависимостей" */

std::string wrap_lines(std::string s, size_t width = 76) {
  std::string out;
  for (size_t i = 0; i < s.size(); i += width)
    out += s.substr(i, width) + "\r\n";
  return out;
}

int main() {
  std::string mime = wrap_lines(base64_encode(std::string(200, 'x')));
  int lines = 0;
  for (char c : mime)
    if (c == '\n') lines++;
  std::printf("mime: %d lines, %zu chars\n", lines, mime.size());
}

Арифметика: 200 байтов кодируются в 268 символов, и при переносе по 76 с терминациями CRLF это 4 строки - три полные и хвост из 40 символов, - 276 символов на проводе. Выбор CRLF в сниппете - выбор почты; для всего остального LF - современный дефолт, и единственное непреложное правило - согласованность: строгий декодер, ожидающий CRLF, прочитает одиночный LF как символ данных. (Правило MIME состоит в том, что декодеры обязаны игнорировать переводы строк, поэтому почта никогда не страдала от этой разницы.) Третья привычка, о которой стоит знать: команда openssl base64 - та самая программа enc в плаще, проверяющая собственное имя в argv[0], - без -A переносит по 64, с -A выдаёт одну строку, и это единственный инструмент командной строки, чьё поведение вы проверяете при каждом запуске, а не доверяете памяти.

Бинарные данные в JSON и конфигурациях

У JSON-строки короткий список символов, которые она не может содержать сырьём: кавычка, обратный слэш и управляющие символы ниже 0x20. Сертификат, случайный ключ, подпись - всё это полно байтов, которые превратились бы в каскад экранирований, попробуй они поехать внутри сырого строкового поля, а управляющие символы заставили бы некоторые парсеры подавиться. Base64 - и есть решение, и это ответ по умолчанию, который даёт вам любой формат конфигурации, вынужденный носить бинарные данные: значение хранится одной строкой чистых символов алфавита, и правилам кавычек JSON-библиотеки больше нечего делать.

C++-паттерн и есть вся реализация: прочитать байты (в бинарном режиме, разумеется), закодировать, сохранить строку. Потребитель декодирует по другую сторону. Одна JSON-специфичная яма - обёрнутая строка: сертификат, обёрнутый по 76 и вставленный прямо в JSON-файл, - это строка, полная литеральных управляющих символов, и это либо ошибка разбора, либо тихая порча, в зависимости от настроения парсера. Если значение нужно обёрнуть для человеческих глаз, его либо экранируют, либо делают в одну строку, - а для машинной конфигурации ответ одна строка. Другая яма - значение без метки: колонка конфигурации, которая в man-странице 2014 года подписана как base64, обычно - дополненный стандартный алфавит, но токены API-эпохи - неполный URL-безопасный, и четырёхсимвольный тест из гайда по декодированию - содержит ли + или /, - или _, есть ли в конце =?, - и есть вся диагностика.

Data URI: файлы, которые вставляют в страницы

Data URI - это URL, чей пелод лежит прямо в адресе: data:, опциональный медиатип, опциональный маркер ;base64, запятая и сами данные - весь механизм RFC 2397. Браузеры используют их, чтобы встраивать изображения, шрифты и мелкие скрипты прямо в HTML и CSS без лишних запросов, и если страница продолжает работать при отключённой сети, data URI - главный подозреваемый. На стороне C++ задача кодирования - собрать строку, а это строковое сложение с одной константой:

#include <cstdio>
#include <string>

/* base64_encode из раздела "Сорок строк, ноль зависимостей" */

std::string make_data_uri(const std::string &mime_type,
                          const std::string &binary) {
  return "data:" + mime_type + ";base64," + base64_encode(binary);
}

int main() {
  std::printf("%s\n", make_data_uri("text/plain", "hi").c_str());
}

Все ямы - в деталях. Маркер ;base64 - это ровно семь символов, и именно эту длину ошибки в единицу обожают брать на прицел: парсер, который проверяет шесть, - это парсер, который примет data:text/plain;base4,... и с самым серьёзным видом декодирует мусор. И пелод base64 data URI - одна строка: переводы строк не входят в грамматику URI, так что если ваш кодировщик обёрнул изображение по 76 (MIME-подобные кодировщики делают это по умолчанию), URI сломан ещё до того, как дойдёт до браузера. Правило для этого потребителя: кодируйте, не переносите и держите медиатип точным - неверный image/png поверх JPEG - это то враньё, которое проявляется только как битное превью в два часа ночи.

Токены: JWT, PKCE и ключи API

Самый высокорисковый base64 в интернете живёт в токене. JSON Web Token - это три base64url-сегмента, склеенных точками: JSON заголовка, JSON утверждений и подпись, вычисленная над строкой header.claims. В C++ нет встроенного JWT-типа, но собрать его - это base64url-кодировщик из раздела выше плюс один вызов HMAC, потому что токен целиком в base64url, пока не перестаёт быть им: пока не становится подписью:

#include <cstddef>
#include <cstdio>
#include <string>
#include <openssl/evp.h>
#include <openssl/hmac.h>

/* base64_encode и base64url_encode из предыдущих разделов */

std::string jwt_hmac256(const std::string &signing_input,
                        const std::string &secret) {
  unsigned char digest[EVP_MAX_MD_SIZE];
  unsigned int len = 0;
  HMAC(EVP_sha256(), secret.data(), static_cast<int>(secret.size()),
       reinterpret_cast<const unsigned char *>(signing_input.data()),
       signing_input.size(), digest, &len);
  return std::string(reinterpret_cast<const char *>(digest), len);
}

int main() {
  const std::string header_json = "{\"alg\":\"HS256\",\"typ\":\"JWT\"}";
  const std::string claims_json =
      "{\"sub\":\"1234567890\",\"name\":\"John Doe\",\"iat\":1516239022}";
  std::string head = base64url_encode(header_json);
  std::string claims = base64url_encode(claims_json);
  std::string signing_input = head + "." + claims;
  std::string sig = base64url_encode(jwt_hmac256(signing_input, "secret"));
  std::printf("token: %s\n", (signing_input + "." + sig).c_str());
}

Запустите пример, и токен, который выйдет, - HS256-токен из учебника: заголовок декодируется в {"alg":"HS256","typ":"JWT"}, утверждения - в субъекта, имя и метку времени выпуска, а подпись - это base64url от HMAC-SHA256 над двумя закодированными сегментами. Три детали несут на себе весь замысел. Вход для подписи - это закодированные сегменты, а не сырой JSON: подпишете JSON - подпишете не те байты. Сегменты - base64url без заполнения: знаки заполнения оказались бы посреди URL, а весь смысл алфавита в том, чтобы держать токен одной чистой строкой. И HS256 означает общий секрет, то есть алгоритм сервер-к-серверу: секрет, живущий в коде клиента, - не секрет, и токен, который он подписывает, - не учётные данные. (Поток PKCE в OAuth использует тот же алфавит с одним отдалением: случайный верификатор, прошитый через SHA-256, без заполнения в base64url превращается в код-вызов - кодировщик из раздела про base64url и есть вся клиентская реализация.)

Basic auth HTTP

Самый старый base64 в HTTP - заголовок учётных данных: после Authorization: Basic идёт base64 от user:password, схема настолько старая, что она старше JSON. Собрать её - значит выполнить сложение строк:

#include <cstdio>
#include <string>

/* base64_encode из раздела "Сорок строк, ноль зависимостей" */

std::string basic_auth_header(const std::string &user,
                              const std::string &pass) {
  return "Basic " + base64_encode(user + ":" + pass);
}

int main() {
  std::printf("%s\n", basic_auth_header("user", "password").c_str());
}

На выходе - строка, которую вы наверняка видели в перехваченном запросе: Basic dXNlcjpwYXNzd29yZA==. Две C++-заметки. Сложение user + ":" + pass - то место, где пароль с двоеточием запутал бы наивный парсер на другой стороне: правило разбора - «режь на первом двоеточии», так что сторона сборки свободна класть в оба поля что угодно. И если учётные данные не ASCII, безопасное прочтение схемы - обращаться с идентификатором пользователя и паролем как с UTF-8 до base64, а на C++ это значит, что ваш std::string уже делает эту работу, - при условии, что вы наполнили его UTF-8-байтами, а не тем, что решила ваша локаль. Заметка о безопасности должна стоять при каждом упоминании этой схемы: Basic-аутентификация - это обфускация, а не защита. Заголовок едет в открытом виде для всех, кто умеет читать сеть, поэтому он допустим только за TLS, и даже тогда это выбор для вызовов машина-к-машине, а не для людей. (Кодек Boost.Beast - тот самый, из пространства имён detail::, - делает ту же заголовковую работу внутри WebSocket-реализации Boost: base64-ит SHA-1-дайджест, из которого получается ключ Sec-WebSocket-Accept, - и это тихое доказательство, что паттерн работает так с 2017 года.)

Электронная почта: правила семи бит и ответ Base64

Почта - это то место, где base64 приобрёл свои привычки, и привычки до сих пор несущие. SMTP в своём исходном виде был построен для переноса 7-битного ASCII, так что всё бинарное должно было переписываться в печатаемый текст, прежде чем отправиться в путь. Privacy-Enhanced Mail сделал это в 1987 году строками по 64 символа и с проверкой целостности сообщения RSA-MD2/MD5, приклеенной в конце, а MIME, стандартизовав кодировку для почты в 1993 году, ослабил лимит до 76 символов и добавил правило, что соответствующий декодер должен просто игнорировать переводы строк. Вложение в письме до сих пор base64, обёрнутое по 76, и точная арифметика выходит в 4/3 умножить на 78/76 - примерно 137 процентов от исходного размера, плюс около 814 байтов заголовков.

C++-сторона - это кодировщик плюс функция переноса из раздела выше: закодировать в одну строку, перенести по 76 с CRLF, готово. Два почтовых нюанса: последняя строка может нести завершающий перевод строки, а может и нет (декодеры обязаны его игнорировать, так что оба варианта легальны и оба распространены), и обёрнутое значение - не JSON-значение, не переменная окружения и не токен: это блок байтов, которому место в MIME-теле, и перенос его куда-либо ещё - это то место, где перенос перестаёт быть привычкой и становится багом. Обратное направление - вложение, прибывшее обёрнутым по 76, - территория сестринского гайда, где четыре C++-декодера расходятся во мнениях о переводах строк четырьмя разными способами.

Файлы, потоки и потолок в два гигабайта

Кодирование файла - зеркальное отражение файловой работы из гайда по декодированию: открыть в бинарном режиме (на Windows чтение в текстовом режиме превратило бы пары CRLF в одиночные переводы строк и изменило бы ваши данные ещё до того, как кодировщик их увидел), прочитать байты, закодировать, записать в бинарном. Версия для небольших файлов - работа одной функции:

#include <cstdio>
#include <fstream>
#include <iterator>
#include <string>
#include <vector>

/* base64_encode из раздела "Сорок строк, ноль зависимостей" */

std::string encode_file(const std::string &path) {
  std::ifstream in(path, std::ios::binary);
  if (!in) return {};
  std::vector<unsigned char> bytes{std::istreambuf_iterator<char>(in),
                                   std::istreambuf_iterator<char>()};
  return base64_encode(
      std::string(reinterpret_cast<const char *>(bytes.data()), bytes.size()));
}

int main() {
  std::string b64 = encode_file("/etc/hostname");
  std::printf("file -> %zu chars\n", b64.size());
}

Потолок - это C++-специфичный факт в заголовке раздела: каждый параметр длины в EVP API - это int. Значит, один вызов EVP_EncodeBlock способен закодировать не больше примерно 2 ГБ входа, а выходной буфер для этого вызова, в 1,33 раза больший, вообще не влезает в int. Ниже потолка блочный API годится для файлов, которые помещаются в память. Выше него, либо для файла, который вы не хотите держать в памяти, - чанки, и правило чанков - единственное base64-специфичное ограничение цикла: чанки должны быть кратны 3 байтам, потому что группировка по трое, и граница чанка посреди группы меняет вывод. 3072, три чанка по 1024 байта, - удобный размер чанка, и цикл становится:

#include <algorithm>
#include <cstddef>
#include <string>

/* base64_encode из раздела "Сорок строк, ноль зависимостей" */

std::string encode_streamed(const std::string &data) {
  std::string out;
  for (size_t pos = 0; pos < data.size();) {
    size_t take = std::min<size_t>(3072, data.size() - pos);
    out += base64_encode(data.substr(pos, take));
    pos += take;
  }
  return out;
}

Каждый чанк кодируется независимо, и склейка идентична результату разового вызова, - вот свойство, благодаря которому чанкование вообще безопасно, и оно вытекает прямо из группировки по трое. (Потоковый контекст OpenSSL из раздела про кодировщик делает ту же работу, добавляя перенос по 64 символа бесплатно, - это правильный инструмент, когда потребителю нужен MIME-вид.) И у выходной стороны тот же бюджет, что и у входной: файл на 10 ГБ становится строкой на 13,3 ГБ, поэтому буфер, или тот файл, который вы пишете, рассчитывается по формуле из раздела про математику, и потолок int говорит, что выше 2 ГБ почанковый путь - не удобство, а единственный путь.

Переменные окружения и командная строка

Переменные окружения имеют ту же проблему, что и JSON-строки, и ещё худший ответ: NUL-байты они не несут вообще, и управляющие символы тоже не их друзья. Стандартный трюк - прогнать пелод через base64, чтобы он пережил оболочку, и на C++ направление кодирования - однострочник:

#include <cstdio>
#include <cstdlib>
#include <string>

/* base64_encode из раздела "Сорок строк, ноль зависимостей" */

int main() {
  setenv("MY_PAYLOAD", base64_encode("hello, env").c_str(), 1);
  std::printf("env: %s\n", getenv("MY_PAYLOAD"));
}

Значение, которое оказывается в окружении, - aGVsbG8sIGVudg==: чистый алфавит, безопасен для оболочки, безопасен для .env-файла, безопасен для CI-дашборда, и его можно декодировать на любой машине, где есть base64-декодер. Сама командная строка имеет ту же двухинструментную историю, что и сторона декодирования, только с флагами направления кодирования: base64 из coreutils (или переосмысленная реализация uutils, которую поставляют новые дистрибутивы, проверьте base64 --version) по умолчанию переносит по 76, а -w 0 даёт одну строку; openssl base64 - та самая программа enc, проверяющая собственное имя в argv[0] и переключающаяся в base64-режим, - переносит по 64 и принимает -A для одной строки:

# одна строка, для токенов и конфигов
base64 -w 0 < payload.bin > payload.b64
openssl base64 -A < payload.bin > payload.b64

# с переносами, для почты и текстовых файлов
base64 < payload.bin > payload-76.b64
openssl base64 < payload.bin > payload-64.b64

Ни один из них не понимает base64url нативно, так что токен, отчеканенный в оболочке, получает транскодирование, прежде чем попасть в URL. И командная строка - то место, где привычка кодерного направления молча терпеть неудачу опаснее всего: кодировщик, который перенёс строки, когда ваш потребитель ждал одну строку, не даст ошибки, он просто выдаст строку с переводами строк, - а это ровно тот сбой, который вы теперь ищете в продакшене. Для всего, что важно, кодируйте в своей программе, где буфер рассчитан по формуле, а форма строк - переменная под вашим контролем.

Ловушки: версия для C++

  • NUL, которого вы не заказывали. EVP_EncodeBlock дописывает завершающий NUL после пелода. Пример из man-страницы: 16 байт на входе, 24 закодированных плюс NUL, 25 в буфере, 24 возвращено. Заложите байт на прирост и сделайте resize до возвращённого значения, иначе ваш токен закончится нулевым байтом.
  • Жёсткое 64. Потоковый API OpenSSL переносит по 64 символа, каждый блок заканчивается переводом строки, и флага, чтобы это изменить, нет. Обёрнутый вывод кодировщика у однострочного потребителя - это баг с управляющим символом.
  • 48-байтовый блок. EVP_EncodeUpdate выдаёт вывод только для полных 48-байтовых входных блоков; остаток сидит в контексте до EVP_EncodeFinal. Заложите 65 выходных байт на блок плюс NUL, и не читайте *outl как «байты моего пелода» - это байты, которые записал именно этот вызов, а для маленького первого вызова это ноль.
  • Пропавшие знаки заполнения итератора. Цепочка Boost.Serialization никогда не выдаёт =. Итератор 2002 года, кодируя «Mane», даёт шесть символов. Дописывайте заполнение сами, иначе строгий потребитель отклонит строку.
  • CRLF, который вы не просили. CryptBinaryToStringA дописывает пару CR/LF, если вы не передадите CRYPT_STRING_NOCRLF. Base64-токен, собранный с флагами по умолчанию, на два символа длиннее, чем положено, и предпоследний символ - возврат каретки.
  • NULL-вызов считает NUL. Windows-пробник размера возвращает нужную длину вместе с завершающим нулём; настоящий вызов возвращает длину без него. Путать их - классическая ошибка в единицу, и она пишет байт за пределы буфера или теряет последний символ.
  • Переносите после, а не во время. Перенос строк - постобработка закодированной строки. Режьте в местах, кратных ширине, - всегда безопасно, потому что каждая четырёхсимвольная группа самодостаточна, - и никогда не переносите сырые байты: переводы строк не к месту именно там.
  • Чанки по трое. Если вы кодируете большой пелод кусками, границы кусков должны приземляться на 3-байтовые группы, иначе поменяется группировка - и вывод. 3072 - дружелюбный чанк; 3071 - баг.
  • int, а не size_t. Все параметры длин EVP - это int. Потолок для одного вызова - около 2 ГБ входа, а вывод для такого входа вообще не влезает в int. Выше потолка почанковый или потоковый путь - не вопрос предпочтения.
  • Signed char. Если вы упаковываете из char * без беззнакового каста, на платформах, где char со знаком, байт выше 127 - отрицательное число, и индексация таблицы им - неопределённое поведение. const unsigned char * - не ритуал.
  • Обёрнутая JSON-строка. Значение, обёрнутое по 76 и вставленное в JSON-файл, - это строка из литеральных управляющих символов. Либо это одна строка, либо она экранирована, либо её нет в JSON.
  • Заполнение - это договорённость. Некоторые потребители хотят заполнение (MIME, большинство декодеров), некоторые нет (JWT, PKCE, токены в URL), а некоторые строгие прямо отклоняют отсутствующее или неканоническое заполнение. Знак заполнения - не украшение; это часть соглашения о формате.
  • Два алфавита. - или _ внутри пелода стандартного алфавита недопустимо, и + или / внутри URL-безопасного тоже недопустимо. Алфавиты не взаимозаменяемы на уровне байтов: кодируйте тем, что соответствует месту назначения, и транскодируйте осознанно.
  • std::string и strlen. std::string охотно несёт нулевые байты, но в момент, когда вы отдаёте C-строку унаследованному API, strlen останавливается на первом NUL. Передавайте указатель и длину, никогда не голый указатель.
  • Бюджет. Вывод - это 4/3 от входа: если вход 1,5 ГБ, вывод - 2 ГБ, а это ещё и потолок int. Рассчитывайте принимающий буфер, колонку и канал по формуле, а не наугад.

Откуда у C++ взялся Base64

История формата старше, чем современная эра языка, и история C++ - это история языка, который снова и снова не поставлял его. Первое стандартизированное применение кодировки, которую теперь называют MIME base64, - протокол Privacy-Enhanced Mail, предложенный в 1987 году со строками по 64 символа и проверкой целостности сообщения RSA-MD2/MD5, приклеенной в конце; само имя «base64» пришло лишь в 1993 году, когда стандарты MIME так его назвали. C++ появился как C++98 в 1998 году - через пять лет после MIME, - и первым base64-кодом, за который потянулись разработчики языка, стала C-пара Рене Ниффенегера 2004-2008 годов, которую вопрос на Stack Overflow от 4 декабря 2008 года разнёс по всему вебу. Самая приятная часть той истории: один ответ ссылаялся на собственную страницу Ниффенегера и перенёс реализацию с неё, вместе с лицензионным заголовком, а ответ с наибольшим числом голосов прогнал его решение в бенчмарке против остального поля. У народной песни есть лицензионный заголовок - композитор сам так и не появился в комментариях.

А потом экосистема сделала то, что делают экосистемы. В 2002 году Boost.Serialization Роберта Рэйми выкатила адаптеры-итераторы - самый старый base64 в C++-наборе инструментов, строгий в направлении декодирования и легендарно без заполнения в направлении кодирования, за год до того, как RFC 3548 кодифицировало правила алфавита, которые он уже выполнял. В 2017 году Boost 1.66 принёс Beast, а с ним кодек только из заголовка, который поставляется по сей день с атрибуцией Ниффенегера в подвале. EVP_EncodeBlock и его друзья от OpenSSL есть в каждом релизе OpenSSL, так что рабочая лошадка в наборе инструментов с тех пор, как язык спорит, быть ли ей в стандарте. На Windows история проще: операционная система просто поставила это - одна функция, одна таблица флагов, без какого-либо стандарта. Тем временем сам стандарт шёл C++11, C++14, C++17, C++20, C++23 (опубликован в 2024) и теперь C++26, и каждый единственный из них посмотрел на 64-символьный алфавит и пошёл дальше. Техническое содержание C++26 завершено и принято голосованием (114-12-3) на мартовском совещании ISO C++ 2026 года в Кройдоне, Великобритания, и да, добавлен новый заголовок <text_encoding> для работы с текстовыми кодеками; следующие совещания комитета, в июне 2026 (Брно) и ноябре 2026 (Бюзиос, Бразилия), открывают рабочую версию C++29, а не возвращаются к C++26. Base64 в стандарте нет. Восемь стандартов, три десятилетия, один заголовок для кодирования текста, - и у комитета теперь были все возможные предлоги добавить base64, и он от всех отказался. Практическая история base64 в C++ есть и остаётся историей его библиотек: пара EVP, два варианта Boost, один флаг Windows и сорокастрочный сниппет, которым владеете вы.

Диковины, о которых стоит знать

  • 48-байтовый блок потокового кодировщика OpenSSL - это число, которое не встречается ни в одном RFC. Это 16 base64-групп, выбранных так, чтобы строка вывода была ровно 64 символа, - PEM-привычка, - и это одно из последних мест, где 1987 год в 2026 году всё ещё несёт нагрузку.
  • encoded_size из Boost.Beast - это раздел про математику в виде constexpr-функции: 4 * ((n + 2) / 3), вычисляется во время компиляции, если вы даёте константу. Стандартной библиотеке этот однострочник так и не достался; Boost поставила его вместо этого в пространстве имён detail::.
  • Самый маленький дополненный base64 - это четыре символа, QQ==: один байт в двухсимвольном костюме. Самый маленький без заполнения - два символа, QQ. Число знаков заполнения - тоже сообщение: два знака означают, что в последней группе был один байт, один знак - два байта, а знаков нет - три, и принимающая сторона способна восстановить длину входа по одному хвосту.
  • Математика накладных расходов MIME точная: 4/3 умножить на 78/76, поэтому вложение в письме прибывает примерно в 137 процентах от исходного размера, плюс около 814 байтов заголовков. Каждый кодировщик в этой статье платит один и тот же налог; ширина переноса лишь меняет способ списания.
  • На типичном libstdc++ или MSVC std::string держит мелкие пелоды в стековом буфере через оптимизацию малых строк, вместо того чтобы аллоцировать. Ввод в 9 байт кодируется в 12 символов и никогда не трогает кучу. Base64-форма вашего токена может буквально жить в кадре стека, и это тот бесплатный обед, который стандартная библиотека не рекламирует.
  • Команда openssl base64, за которую вы можете схватиться в оболочке, - вообще не команда. Это программа enc, проверяющая собственное имя в argv[0] и переключающая персонажа. Алиас через сравнение строк, C++-способ делать вещи - на C.
  • Идентификаторы видео YouTube - это base64url: одиннадцать символов, без заполнения, без + или / где-то рядом с URL. Самая просматриваемая кодировка на планете работает на варианте «безопасном для URL и имён файлов», который RFC 4648 добавил в разделе, помещающемся на одной странице.
  • Четыре A, AAAA, кодируют три нулевых байта, потому что A - это ноль алфавита. Если вы когда-нибудь видели base64-блоб, состоящий целиком из одного символа, теперь вы знаете, что он говорил: ничего.
  • Та же самая пара функций встречается в ответах на вопрос Stack Overflow 2008 года, в исходниках Boost.Beast с атрибутивным подвалом и в заголовочных файлах бесчисленных частных кодовых баз. Спросите C++-разработчика, откуда у него base64, и самый честный ответ будет «не знаю, и интернет тоже не знает».

Другое направление

Всё, что вы только что упаковали, будет распаковано тем же набором инструментов по другую сторону, и у распаковывающей стороны есть свой набор привычек: разовая функция OpenSSL, которая заполняет хвост нулями, багфикс 2025 года, изменивший, что потоковый декодер возвращает для дополненного входа, decode из Boost.Beast, который останавливается на чужом символе и ни словом не обмолвится об этом, итератор, который бросает исключение на единственном пробеле, и сорокастрочный строгий декодер, который указывает на точный байт, навредивший вам. Полная история распаковки - характеры четырёх декодеров, транскодирование base64url, файлы, 76-символьная привычка MIME и два инструмента командной строки, которые терпят неудачу молча, - живёт в C++-гайде по декодированию на сестринском сайте. Сходите прочтите, а потом возвращайтесь и упакуйте что-нибудь большое. В этом и вся игра: никакой стандартной библиотеки, четыре поставщика с четырьмя разными мнениями о переводах строк и NUL, формула, которая рассчитывает каждый буфер в статье, и один налог в 33 процента, который каждый получатель вправе возместить. Удачной упаковки.

Последнее обновление: 2026-09-08

Связанная статья: Декодирование Base64 в C++ (Cpp): полное руководство