Boost in Modern C++: What the Standard Replaced, What Is Still Worth It, and How to Link It

Boost used to be the answer to “the standard library doesn’t have it.” Smart pointers, function, bind, threads, regex, optional, variant, any, filesystem — all of them shipped in Boost years before they reached std. That history is why so many older codebases start with find_package(Boost REQUIRED COMPONENTS system filesystem thread), and also why a new project in 2026 should think twice before copying that line.

This article is about the decision and the plumbing: what the standard has absorbed, which Boost libraries still have no substitute, and the build errors you hit when you do bring Boost in.

What the standard has already absorbed

Boost libraryStandard replacementNotes on the migration
shared_ptr, weak_ptr, scoped_ptrstd::shared_ptr, std::weak_ptr, std::unique_ptr (C++11)Drop-in; make_shared behaves the same
function, bind, refstd::function, lambdas, std::bind (C++11)Prefer lambdas over bind in new code
thread, mutex, condition_variable<thread>, <mutex> (C++11), std::jthread + stop_token (C++20)Boost.Thread interruption points have no direct equivalent; stop_token is cooperative
chrono, much of date_time<chrono> (C++11), calendar and time zones (C++20)C++20 calendar/time-zone support arrived late in some standard libraries
regex<regex> (C++11)API matches; performance does not (see below)
optional, variant, anystd::optional, std::variant, std::any (C++17)boost::optional<T&> is allowed; std::optional<T&> only arrives in C++26
filesystemstd::filesystem (C++17)Different error-code types and some path semantics; mostly mechanical
container::flat_mapstd::flat_map (C++23)Needs a recent standard library
container::static_vectorstd::inplace_vector (C++26)Not widely available yet

If your minimum standard is C++17, the rows up to filesystem are dependencies you can usually delete. The migration is mostly s/boost::/std::/ plus attention to two things:

  • Error reporting. std::filesystem functions throw std::filesystem::filesystem_error or take a std::error_code&; the Boost versions use boost::system::error_code. Code that inspects error categories needs rewriting.
  • Old toolchains. GCC 8 shipped std::filesystem in a separate library, so builds there need -lstdc++fs (GCC 9 and later don’t). If a CI image still has GCC 8, the migration fails with undefined references to symbols like std::filesystem::__cxx11::path::_M_split_cmpts(), which looks like a Boost problem and isn’t.

std::regex deserves its own warning. Its API mirrors Boost.Regex, but libstdc++‘s implementation is known to be slow, and its recursive matcher can overflow the stack on long inputs with patterns such as (a|b)*. I’ve seen a “simple” log-parsing migration from boost::regex to std::regex turn into a segfault on the first multi-megabyte line, with a backtrace hundreds of thousands of frames deep inside _M_dfs. If you do heavy regex work, Boost.Regex is still a reasonable choice — or step outside both and use RE2 or a hand-written parser.

Where Boost is still worth the dependency

These libraries have no standard equivalent and are good enough that writing your own would be a mistake:

  • Asio — async I/O, timers, sockets, serial ports, signals. The Networking TS based on it was never merged into the standard, so Asio (Boost or standalone) remains the de facto networking layer. Header-only.
  • Beast — HTTP/1.1 and WebSocket on top of Asio. Low-level by design: you get parsers and streams, not a web framework.
  • Container — small_vector (inline storage that falls back to the heap), static_vector, stable_vector, flat_map/flat_set for pre-C++23 code, and containers that work with custom allocators and in shared memory.
  • Unordered — boost::unordered_flat_map is an open-addressing hash map. std::unordered_map is node-based because the standard guarantees reference stability, and that design costs an allocation per element and poor cache locality.
  • Multiprecision — arbitrary-precision integers, rationals and floats with a normal arithmetic API.
  • Multi-index — one container indexed several ways (by id and by name, say) without keeping two maps in sync.
  • Interprocess — shared memory, memory-mapped files and process-shared mutexes.
  • Program_options, Spirit, Json, Graph, Geometry — narrower, but mature.

A short Asio example to show the style — an async timer that re-arms itself and stops on Ctrl+C:

#include <boost/asio.hpp>
#include <chrono>
#include <csignal>
#include <iostream>

namespace asio = boost::asio;

void tick(asio::steady_timer& t, int n) {
    t.expires_after(std::chrono::seconds(1));
    t.async_wait([&t, n](const boost::system::error_code& ec) {
        if (ec == asio::error::operation_aborted) return;  // cancelled on shutdown
        std::cout << "tick " << n << "\n";
        tick(t, n + 1);
    });
}

int main() {
    asio::io_context io;
    asio::steady_timer timer(io);
    asio::signal_set signals(io, SIGINT, SIGTERM);
    signals.async_wait([&](const boost::system::error_code&, int) { timer.cancel(); });

    tick(timer, 0);
    io.run();   // returns when no work is pending
}

Two details matter. io.run() returns as soon as there is no pending work, so the handler must re-arm the timer before it finishes. And the handler must check for operation_aborted: cancellation still invokes the callback, with that error code, and ignoring it is the classic source of “my server rescheduled itself after shutdown.”

small_vector is the Container type people most often wish they had known about:

#include <boost/container/small_vector.hpp>

// Up to 8 elements live inside the object; the 9th triggers a heap allocation.
boost::container::small_vector<int, 8> ids;

For a vector that almost always holds a handful of elements (tokens in a short line, children of a tree node), this removes a heap allocation per object without changing calling code.

Header-only vs compiled libraries

Most of Boost is header-only: you need the include directory and nothing else. The libraries with compiled parts are those that talk to the OS or have large non-template implementations.

Needs a compiled libraryHeader-only
Filesystem, Program_options, Thread, Serialization, Log, Locale, Iostreams, Timer, Test (a header-only variant exists), Json (unless you include boost/json/src.hpp in one translation unit)Asio, Beast, Optional, Variant, Any, Container (most of it), Multi-index, Spirit, Multiprecision, Geometry, Unordered, System (since 1.69)

One change trips up older build files: Boost.System has been header-only since Boost 1.69. The boost_system library that still appears in many CMakeLists is a compatibility stub with nothing in it. Asio historically required it, which is where the habit came from. Drop system from COMPONENTS — it buys nothing, and you depend on the stub continuing to be shipped.

find_package(Boost) in modern CMake

Boost installs its own CMake package (BoostConfig.cmake) since 1.70. For years CMake also shipped a separate FindBoost.cmake module that guessed library names from file paths. CMake 3.30 removed that module under policy CMP0167. With CMake 4.3 and cmake_minimum_required(VERSION 3.20), a plain find_package(Boost 1.80 REQUIRED COMPONENTS filesystem) on a machine without Boost printed this for me:

CMake Warning (dev) at CMakeLists.txt:3 (find_package):
  Policy CMP0167 is not set: The FindBoost module is removed.  Run "cmake
  --help-policy CMP0167" for policy details.  Use the cmake_policy command to
  set the policy and suppress this warning.
...
  Could NOT find Boost (missing: Boost_INCLUDE_DIR filesystem) (Required is
  at least version "1.80")

With the policy unset, CMake still falls back to the legacy module, hence the old-style Could NOT find Boost message. After raising the minimum to 3.30 (policy NEW), the same configure step fails differently — it now looks only for Boost’s own config file:

  Could not find a package configuration file provided by "Boost" (requested
  version 1.80) with any of the following names:

    Boost.cps
    boost.cps
    BoostConfig.cmake
    boost-config.cmake

  Add the installation prefix of "Boost" to CMAKE_PREFIX_PATH or set
  "Boost_DIR" to a directory containing one of the above files.

(Older CMake versions list only the two .cmake names.) The fix is to tell CMake where Boost’s config lives, not to go back to FindBoost:

cmake_minimum_required(VERSION 3.30)   # CMP0167 NEW: use BoostConfig.cmake
project(app CXX)
set(CMAKE_CXX_STANDARD 17)

find_package(Boost 1.83 CONFIG REQUIRED COMPONENTS program_options)

add_executable(app main.cpp)
target_link_libraries(app PRIVATE
    Boost::headers           # all header-only libraries: Asio, Container, ...
    Boost::program_options)  # compiled component
# Boost built from source into /opt/boost-1.86
cmake -S . -B build -DCMAKE_PREFIX_PATH=/opt/boost-1.86
# or point directly at the config directory
cmake -S . -B build -DBoost_DIR=/opt/boost-1.86/lib/cmake/Boost-1.86.0

Notes that save time:

  • Don’t reach for ${Boost_INCLUDE_DIRS}. Link Boost::headers instead; imported targets carry include paths, compile definitions and transitive dependencies.
  • Asio on Linux also needs threads: find_package(Threads REQUIRED) and Threads::Threads. Since glibc 2.34 pthread_create lives in libc itself, so undefined reference to 'pthread_create' mostly shows up on older distributions — but linking Threads::Threads is correct everywhere.
  • Package managers. Distribution packages (libboost-all-dev, or individual ones like libboost-program-options-dev) and vcpkg (boost-asio, boost-program-options, …) both install a CMake config. With vcpkg, pass the toolchain on the command line (-DCMAKE_TOOLCHAIN_FILE=<vcpkg>/scripts/buildsystems/vcpkg.cmake). Setting it inside CMakeLists.txt only works before the first project() call; after that it is silently ignored.

Missing component. Using a compiled library without linking it produces undefined references to its implementation functions, for example undefined reference to 'boost::program_options::options_description::options_description(...)', or references into boost::filesystem::detail:: for filesystem calls. The fix is the matching imported target (Boost::program_options, Boost::filesystem). If you write -lboost_filesystem by hand, GNU ld’s ordering rule applies: libraries go after the objects that use them.

Headers from one version, libraries from another. This is the one that wastes days. A machine has distro Boost in /usr/include and a hand-built newer Boost in /usr/local; the compiler picks up one set of headers and the linker the other set of libraries. Symptoms are undefined references to functions that plainly exist in the library (the signatures, and so the mangled names, changed between versions) or, worse, a clean link followed by a crash at runtime. Whenever a Boost link error doesn’t make sense, I check which version the compiler actually sees and which shared libraries the binary actually loads:

echo '#include <boost/version.hpp>' | g++ -E -dM -x c++ - | grep BOOST_LIB_VERSION
ldd ./app | grep boost

find_package(Boost CONFIG) with a single explicit CMAKE_PREFIX_PATH avoids most of this, because headers and libraries come from the same config file.

Windows auto-linking. With MSVC, Boost headers insert #pragma comment(lib, ...) directives naming a library that encodes toolset, threading, runtime and version, such as libboost_filesystem-vc143-mt-gd-x64-1_86.lib. If that exact file isn’t on the library path, the link fails with LNK1104: cannot open file '...'. The name tells you what’s wrong: vc143 is the toolset, gd means debug runtime, 1_86 the version — a Release-only Boost build and a Debug configuration is the most common mismatch. Install the matching variant, or define BOOST_ALL_NO_LIB and let the CMake imported targets choose libraries explicitly.

API removals between versions. Boost does delete deprecated APIs. boost::filesystem::copy_option::overwrite_if_exists was superseded by copy_options::overwrite_existing in 1.74, and later releases removed the old enum, so code copied from older tutorials stops compiling on a Boost upgrade:

// Boost 1.74+
fs::copy_file(src, dst, fs::copy_options::overwrite_existing);
// std::filesystem spells it the same way
std::filesystem::copy_file(src, dst, std::filesystem::copy_options::overwrite_existing);

If you’re touching code like this and you’re on C++17, that is the moment to switch the file to std::filesystem.

Program_options pitfall: reading an option that isn’t there

Program_options stores values type-erased, and as<T>() on a missing option or with the wrong type throws boost::bad_any_cast. The usual cause is vm["x"].as<T>() for an option with no default that wasn’t passed:

#include <boost/program_options.hpp>
#include <iostream>
#include <string>
namespace po = boost::program_options;

int main(int argc, char* argv[]) {
    po::options_description desc("Options");
    desc.add_options()
        ("help,h", "show help")
        ("input,i", po::value<std::string>()->required(), "input file")
        ("threads,t", po::value<int>()->default_value(4), "worker threads")
        ("config,c", po::value<std::string>(), "optional config file");

    po::variables_map vm;
    try {
        po::store(po::parse_command_line(argc, argv, desc), vm);
        if (vm.count("help")) { std::cout << desc << "\n"; return 0; }
        po::notify(vm);   // throws po::required_option if --input is missing
    } catch (const po::error& e) {
        std::cerr << e.what() << "\n" << desc << "\n";
        return 1;
    }

    int threads = vm["threads"].as<int>();          // safe: has a default
    if (vm.count("config")) {                       // no default: check first
        std::cout << "config: " << vm["config"].as<std::string>() << "\n";
    }
    std::cout << "threads: " << threads << "\n";
}

Call po::notify after store. required() checks and notifier callbacks only run there; forget it and required options silently stop being required.

Deciding: a short checklist

Before adding Boost to a new project, I ask:

  1. Does the standard at my minimum C++ version already have it? If yes, use std.
  2. Is it header-only? Then the cost is mostly compile time — acceptable for Asio or Container.
  3. If it’s compiled, can every developer and CI machine get the same version through one source (vcpkg, Conan, or the distro)? Mixed sources are where version skew comes from.
  4. Is there a smaller standalone alternative? Asio exists standalone (namespace asio, no Boost install), and fmt, CLI11 and nlohmann/json cover jobs Boost also covers.

Boost remains excellent engineering. The mistake is treating it as a default rather than a choice.