Skip to content

optional and expected never provide a trivial assignment operator, so neither is ever trivially copyable #308

Description

@Bronek

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingrelease-0.1Planned for release 0.1

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions