optional and expected never provide a trivial assignment operator, so neither is ever trivially copyable — where the standard mandates both.
[optional.assign] says of the copy assignment operator:
If is_trivially_copy_constructible_v<T> && is_trivially_copy_assignable_v<T> && is_trivially_destructible_v<T> is true, this assignment operator is trivial.
and the same for the move assignment operator, with the move traits. [expected.object.assign] mandates the same over both T and E. We provide neither, for any T, so the whole type is never trivially copyable:
| type |
trivially copyable |
trivially dtor |
trivial copy ctor |
trivial move ctor |
trivial copy assign |
trivial move assign |
std::optional<int> |
yes |
yes |
yes |
yes |
yes |
yes |
pfn::optional<int> |
no |
yes |
yes |
yes |
no |
no |
fn::optional<int> |
no |
yes |
yes |
yes |
no |
no |
std::expected<int, int> |
yes |
yes |
yes |
yes |
yes |
yes |
pfn::expected<int, int> |
no |
yes |
yes |
yes |
no |
no |
fn::expected<int, int> |
no |
yes |
yes |
yes |
no |
no |
expected<void, int> behaves the same way. So a user who relies on is_trivially_copyable_v — for passing in registers, for memcpy, or for any of the fast paths the standard library selects on it — gets none of them from our types, and gets all of them from the standard's.
#include <pfn/optional.hpp>
#include <type_traits>
// mandated by [optional.assign], and true of std::optional<int>
static_assert(std::is_trivially_copy_assignable_v<pfn::optional<int>>); // fails
static_assert(std::is_trivially_copyable_v<pfn::optional<int>>); // fails
The omission is confined to assignment: the constructors and the destructor already carry their conditionally-trivial arms (pfn/optional.hpp:305-327, 1056-1069; fn/expected.hpp:502-518), and the reference specializations already default their copy assignment (pfn/optional.hpp:1345, fn/optional.hpp:640). The value specializations simply declare a user-provided operator= with no defaulted twin, so it can never be trivial. P0848 is available on every supported compiler, and the codebase already uses it — the destructor at pfn/optional.hpp:495-498 is the pattern to follow.
Four sites need it, because fn re-declares its own assignment rather than inheriting pfn's (it derives from pfn::detail::_optional_base / the expected equivalent):
pfn/optional.hpp:1121, 1128
pfn/expected.hpp:1269, 1280 (value) and 1555, 1563 (void)
fn/optional.hpp:365, 372
fn/expected.hpp:555, 565 (value) and 922, 929 (void)
Fix direction: constrain each existing user-provided operator on the negation of the triviality condition, and add a defaulted twin for the trivial case.
Note for whoever writes the tests: no test in the suite asserts is_trivially_copy_assignable or is_trivially_move_assignable for anything, which is why this went unseen. The assertion is self-catching once written — tests/pfn/optional.cpp and tests/pfn/expected.cpp also run against std::optional and std::expected in the validation builds, so an assertion of the standard's answer passes there and fails against the polyfill today.
This is the conformance half of the triviality picture; fn::sum supports no trivial operation at all, which is a design question rather than a defect — #309.
Reproduced at 5e76148 with gcc 16.1.1 and clang 22.1.6 (x86-64 Linux), against libstdc++ for both std::optional and std::expected.
optionalandexpectednever provide a trivial assignment operator, so neither is ever trivially copyable — where the standard mandates both.[optional.assign] says of the copy assignment operator:
and the same for the move assignment operator, with the move traits. [expected.object.assign] mandates the same over both
TandE. We provide neither, for anyT, so the whole type is never trivially copyable:std::optional<int>pfn::optional<int>fn::optional<int>std::expected<int, int>pfn::expected<int, int>fn::expected<int, int>expected<void, int>behaves the same way. So a user who relies onis_trivially_copyable_v— for passing in registers, formemcpy, or for any of the fast paths the standard library selects on it — gets none of them from our types, and gets all of them from the standard's.The omission is confined to assignment: the constructors and the destructor already carry their conditionally-trivial arms (
pfn/optional.hpp:305-327,1056-1069;fn/expected.hpp:502-518), and the reference specializations already default their copy assignment (pfn/optional.hpp:1345,fn/optional.hpp:640). The value specializations simply declare a user-providedoperator=with no defaulted twin, so it can never be trivial. P0848 is available on every supported compiler, and the codebase already uses it — the destructor atpfn/optional.hpp:495-498is the pattern to follow.Four sites need it, because
fnre-declares its own assignment rather than inheritingpfn's (it derives frompfn::detail::_optional_base/ the expected equivalent):pfn/optional.hpp:1121,1128pfn/expected.hpp:1269,1280(value) and1555,1563(void)fn/optional.hpp:365,372fn/expected.hpp:555,565(value) and922,929(void)Fix direction: constrain each existing user-provided operator on the negation of the triviality condition, and add a defaulted twin for the trivial case.
Note for whoever writes the tests: no test in the suite asserts
is_trivially_copy_assignableoris_trivially_move_assignablefor anything, which is why this went unseen. The assertion is self-catching once written —tests/pfn/optional.cppandtests/pfn/expected.cppalso run againststd::optionalandstd::expectedin the validation builds, so an assertion of the standard's answer passes there and fails against the polyfill today.This is the conformance half of the triviality picture;
fn::sumsupports no trivial operation at all, which is a design question rather than a defect — #309.Reproduced at 5e76148 with gcc 16.1.1 and clang 22.1.6 (x86-64 Linux), against libstdc++ for both
std::optionalandstd::expected.