If you try to compile C++23 code that uses std::expected on an older toolchain,
you may hit:
/home/uli/Tether/include/tether/io/TetherIOClient.hpp:33:10: fatal error: expected: No such file or directory
33 | #include <expected>
| ^~~~~~~~~~
compilation terminated.This happens because <expected> is only available in libstdc++ from GCC 12
(and then in a fully usable form from GCC 13). GCC 11, including the default
compiler on Ubuntu 22.04 (GCC 11.4), ships no <expected> header at all —
not even as <experimental/expected>.
The cleanest fix is to fall back to the tl::expected
library: it is the reference implementation that std::expected (P0323) was
based on, so a thin namespace shim makes existing #include <expected> /
std::expected(...) / std::unexpected(...) call sites compile unchanged.
Add tl::expected as a submodule
git submodule add https://github.com/TartanLlama/expected.git dependencies/expected
cd dependencies/expected
git checkout v1.3.1
cd ../..
git add dependencies/expected .gitmodulesCreate a std::expected shim header
Create include/yourproject/expected_shim/expected (the file is literally named
expected so that #include <expected> resolves to it):
#pragma once
#include <tl/expected.hpp>
#include <type_traits>
#include <utility>
namespace std {
using tl::expected;
// NOTE: We cannot simply do `using tl::unexpected;` because GCC < 12's
// <exception> header already declares `void std::unexpected()` — the
// deprecated C++03 unexpected-handler function. A using-declaration cannot
// override an existing function declaration in the same namespace, so
// `std::unexpected(arg)` would resolve to the no-arg function and fail.
//
// Instead we define a function template that overloads it. When called with
// an argument (the only way C++23 code uses std::unexpected), overload
// resolution picks this template over the deprecated void unexpected().
template <class E>
auto unexpected(E&& e) {
return tl::unexpected<std::decay_t<E>>(std::forward<E>(e));
}
} // namespace std
Why not using tl::unexpected;?
GCC 11’s <exception> header declares the deprecated C++03 function
void std::unexpected() (the unexpected-handler). A using tl::unexpected;
declaration cannot override an existing function declaration in the same
namespace, so std::unexpected(arg) resolves to the no-arg function and fails:
/usr/include/c++/11/exception:95:8: note: declared here
95 | void unexpected() __attribute__ ((__noreturn__));
| ^~~~~~~~~~
TetherIOClient.cpp:696:40: error: too many arguments to function 'void std::unexpected()'
696 | if (!result) return std::unexpected(result.error());
| ~~~~~~~~~~~~~^~~~~~~~~~~~~~~~The function template overloads the deprecated function — when called with an argument, overload resolution picks the template.
Auto-detect <expected> in CMake
Add this near the top of your CMakeLists.txt, after project() and after
setting CMAKE_CXX_STANDARD:
cmake_minimum_required(VERSION 3.16)
project(MyProject VERSION 1.0.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
include(CheckIncludeFileCXX)
set(CMAKE_REQUIRED_FLAGS "-std=c++23")
check_include_file_cxx("expected" HAVE_NATIVE_STD_EXPECTED)
unset(CMAKE_REQUIRED_FLAGS)
if(NOT HAVE_NATIVE_STD_EXPECTED)
message(STATUS "Native <expected> not available; using tl::expected shim.")
# Validate submodule
set(EXPECTED_PATH "${CMAKE_CURRENT_SOURCE_DIR}/dependencies/expected")
if(NOT EXISTS "${EXPECTED_PATH}/include/tl/expected.hpp")
message(FATAL_ERROR
"expected submodule missing. Run: git submodule update --init --recursive")
endif()
# Shim must be searched BEFORE system paths so #include <expected> finds it
include_directories(BEFORE
"${CMAKE_CURRENT_SOURCE_DIR}/include/yourproject/expected_shim")
include_directories("${EXPECTED_PATH}/include")
else()
message(STATUS "Native <expected> available; using it directly.")
endif()Keep source code unchanged
Your existing code can continue to use the standard spelling:
#include <expected>
#include <iostream>
#include <string>
struct Error { int code; std::string msg; };
std::expected<int, Error> divide(int a, int b) {
if (b == 0) {
return std::unexpected(Error{1, "division by zero"});
}
return a / b;
}
int main() {
auto result = divide(10, 2);
if (result) {
std::cout << *result << '\n'; // 5
}
auto err = divide(10, 0);
if (!err) {
std::cerr << err.error().msg << '\n';
}
return 0;
}On GCC 15 this compiles with the native <expected>. On GCC 11 the same file
compiles through the tl::expected shim.
What is covered by the shim
The patterns used in real code are all supported:
std::expected<T, E>— value-or-error wrapperstd::expected<void, E>— error-only specializationstd::unexpected(error)— factory relying on CTAD (works becausetl::unexpected<E>has a deduction guide).value(),.error(),.has_value(),operator bool,operator*,operator->
The shim does not need to wrap monadic helpers (and_then, transform,
etc.) unless your code uses them; tl::expected v1.3.1 exposes the older
map / map_error names, so if you need those, use the standard names on
GCC 12+ and keep the polyfill minimal.
How it works
include_directories(BEFORE ...) prepends the shim directory to the compiler’s
include search path. When the compiler sees #include <expected>, it finds the
shim header instead of the (missing) system header. The shim re-exports
tl::expected’s expected and unexpected templates under namespace std,
so the rest of the project never needs to know which implementation is in use.
tl::expected is header-only, so there is no extra library to link.
Summary
| Header | Fallback submodule | Native since | Shim header |
|---|---|---|---|
<expected> | dependencies/expected (tl::) | GCC 12 | expected_shim/expected |
This keeps your project portable across GCC 11 through the latest release
without sprinkling #ifdef guards or tl:: names through the code.