diff --git a/poc/CMakeLists.txt b/poc/CMakeLists.txt index 5b71e7a..3c3c3b7 100644 --- a/poc/CMakeLists.txt +++ b/poc/CMakeLists.txt @@ -11,4 +11,5 @@ if(WIN32) add_subdirectory(int_to_string) endif() +add_subdirectory(rvo_optional) add_subdirectory(variadic_template_arguments) diff --git a/poc/rvo_optional/CMakeLists.txt b/poc/rvo_optional/CMakeLists.txt new file mode 100644 index 0000000..43df396 --- /dev/null +++ b/poc/rvo_optional/CMakeLists.txt @@ -0,0 +1,12 @@ +# This file is part of KDToolBox. +# +# SPDX-FileCopyrightText: 2026 Klarälvdalens Datakonsult AB, a KDAB Group company +# +# SPDX-License-Identifier: MIT +# +project(test_rvo_optional LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 20) + +add_executable(test_rvo_optional main.cpp) +target_compile_options(test_rvo_optional PRIVATE -O3) diff --git a/poc/rvo_optional/main.cpp b/poc/rvo_optional/main.cpp new file mode 100644 index 0000000..b6b03b8 --- /dev/null +++ b/poc/rvo_optional/main.cpp @@ -0,0 +1,515 @@ +/* +This file is part of KDToolBox. + + SPDX-FileCopyrightText: 2026 Klarälvdalens Datakonsult AB, a KDAB Group company + Author: Jonatan Wallmander + + SPDX-License-Identifier: MIT +*/ +#include +#include +#include +#include +#include + +// enabling this allows the custom optional class to allow the "bad" behaviour +#define ENABLE_BAD_PATTERN 0 + +// This is inspired by +// C++ Weekly Episode 421 on Youtube +// by Jason Turner +// https://www.youtube.com/watch?v=0yJk5yfdih0 + +namespace KDToolBox +{ +/** + * std::optional replacement which enforces return value optimization and does away with exceptions. + * + * Normally with std::optional, there are several unnecessary copies made. + * Really the only way to avoid that is to use the emplace() method. + * But this means you can still make the mistake. + * + * This implementation solves it by omitting assignment/copy constructors/operators. + * It thus forces the compiler to use the move constructor. + * Note that this is an optimization in the compiler and might not be supported everywhere. + * So if you are about to make the mistake that would lead to a copy - + * your code simply should not compile. + * + * Additionally, instead of throwing exceptions, uses debug build asserts when + * accessing the value in a bad way. + * I.e. + * KDToolBox::optional opt; + * auto v = opt.value(); <-- this will fail and cause the assert() but only in debug builds. + * + * So be careful, there is possible speed gains to be had but your code needs full test coverage. + * + * Bonus: Specialization for bool below which uses 1 byte only. + * + * @tparam T any type to store in the optional + */ +template +class optional +{ +public: + static_assert(!std::is_reference_v, "KDToolBox::optional cannot store reference types"); + static_assert(!std::is_array_v, "KDToolBox::optional cannot store array types"); + ~optional() noexcept { reset(); } + + optional() noexcept + : m_dummy() + { + } + + template + requires(sizeof...(Types) > 0 && + !(sizeof...(Types) == 1 && (std::is_same_v, optional> || ...))) +#if ENABLE_BAD_PATTERN + optional(Types &&...v) noexcept(std::is_nothrow_constructible_v) +#else + explicit optional(Types &&...v) noexcept(std::is_nothrow_constructible_v) +#endif + : m_value(std::forward(v)...) + , m_has_value(true) + { + } + + [[nodiscard]] bool has_value() const noexcept { return m_has_value; } + + [[nodiscard]] explicit operator bool() const noexcept { return m_has_value; } + [[nodiscard]] const T &operator*() const noexcept { return value(); } + [[nodiscard]] const T *operator->() const noexcept { return &value(); } + + [[nodiscard]] const T &value() const noexcept + { + // std::optional will instead throw exception here + // use an assert instead + assert(m_has_value); + return m_value; + } + + void reset() noexcept + { + if (m_has_value) + { + m_value.~T(); + } + + m_has_value = false; + } + + /** + * Compatible with std::optional's emplace method. + */ + template + T &emplace(Types &&...v) noexcept(std::is_nothrow_constructible_v) + { + // destroy the previous value + reset(); + + // construct a new value in-place + new (&m_value) T(std::forward(v)...); + m_has_value = true; + return m_value; + } + + optional(const optional &) = delete; + optional &operator=(const optional &) = delete; + + // This is the method we don't want to have. + // It allows the code in get_opt_bad_1 and get_opt_bad_2 +#if ENABLE_BAD_PATTERN + optional &operator=(optional &&other) noexcept + { + if (this != &other) + { + reset(); + if (other.m_has_value) + { + new (&m_value) T(std::move(other.m_value)); + m_has_value = true; + other.reset(); + } + } + return *this; + } +#endif + optional(optional &&other) noexcept + { + if (other.m_has_value) + { + new (&m_value) T(std::move(other.m_value)); + m_has_value = true; + other.reset(); + } + else + { + m_dummy = dummy_type{}; + } + } + +private: + struct dummy_type + { + // This default constructor is user-provided to avoid zero-initialization when objects are value-initialized. + constexpr dummy_type() noexcept = default; + }; + + union + { + dummy_type m_dummy; + std::remove_cv_t m_value; + }; + bool m_has_value = false; +}; + +// specialization for bool which only uses one byte +template<> +class optional +{ +public: + optional() noexcept = default; + + explicit optional(bool v) noexcept + : m_has_value(true) + , m_value(v) + { + } + + [[nodiscard]] bool has_value() const noexcept { return m_has_value; } + + [[nodiscard]] bool value() const noexcept { return m_value; } + + [[nodiscard]] explicit operator bool() const noexcept { return m_has_value; } + + optional(const optional &) = delete; + optional &operator=(const optional &) = delete; + + void reset() noexcept + { + m_has_value = false; + m_value = false; + } + + optional(optional &&other) noexcept + { + m_value = other.m_value; + m_has_value = true; + other.reset(); + } + + /** + * Differs from the base emplace in that it + * does not return a reference. + * One can not return references to bit fields. + */ + void emplace(bool v) noexcept + { + m_value = v; + m_has_value = true; + } + +private: + bool m_has_value : 7 = false; + bool m_value : 1 = {}; +}; +} + +// test / example code below --------------------- + +#include +#include +#include + +struct pod_struct +{ + uint8_t a; + uint8_t b; +}; + +void print(const std::source_location &location = std::source_location::current()) noexcept +{ + std::puts(location.function_name()); +} + +// Jason Turner's Lifetime class to print behavior with some additions +struct Lifetime +{ + explicit Lifetime(uint8_t a) noexcept + { + m_value = a; + print(); + } + Lifetime() noexcept { print(); } + Lifetime(Lifetime &&) noexcept { print(); } + Lifetime(const Lifetime &) noexcept { print(); } + ~Lifetime() noexcept { print(); } + Lifetime &operator=(const Lifetime &) noexcept + { + print(); + return *this; + } + Lifetime &operator=(Lifetime &&) noexcept + { + print(); + return *this; + } + + uint8_t m_value = 12; +}; + +std::optional get_std_optional_bad_1() +{ + std::optional opt; + opt = Lifetime{42}; + return opt; +} + +std::optional get_std_optional_bad_2() +{ + return Lifetime{42}; +} + +std::optional get_std_optional_good() +{ + std::optional opt; + opt.emplace(42); + return opt; +} + +KDToolBox::optional get_opt_good_1() +{ + KDToolBox::optional opt; + opt.emplace(42); + return opt; +} + +KDToolBox::optional get_opt_good_2() +{ + KDToolBox::optional opt(42); + return opt; +} + +KDToolBox::optional get_opt_good_3() +{ + return KDToolBox::optional(42); +} + +#if ENABLE_BAD_PATTERN +/* +This along with the optional &operator=(optional &&other) +results in the following (extra move and extra destructor): + +__cdecl Lifetime::Lifetime(unsigned char) noexcept +__cdecl Lifetime::Lifetime(struct Lifetime &&) noexcept +__cdecl Lifetime::~Lifetime(void) noexcept +__cdecl Lifetime::~Lifetime(void) noexcept +*/ +KDToolBox::optional get_opt_bad_1() +{ + KDToolBox::optional opt; + opt = {42}; + return opt; +} + +/* +This along with the optional &operator=(optional &&other) +results in the following (some extra move and extra destructor calls): + +__cdecl Lifetime::Lifetime(unsigned char) noexcept +__cdecl Lifetime::Lifetime(struct Lifetime &&) noexcept +__cdecl Lifetime::Lifetime(struct Lifetime &&) noexcept +__cdecl Lifetime::~Lifetime(void) noexcept +__cdecl Lifetime::~Lifetime(void) noexcept +__cdecl Lifetime::~Lifetime(void) noexcept +*/ +KDToolBox::optional get_opt_bad_2() +{ + KDToolBox::optional opt; + opt = Lifetime(42); + return opt; +} +#endif + +void extra_move_and_destructor_examples() +{ +#if ENABLE_BAD_PATTERN + printf("BEGIN: get_opt_bad_1():\n"); + { + auto ret = get_opt_bad_1(); + } + printf("END: get_opt_bad_1():\n"); + + printf("BEGIN: get_opt_bad_2():\n"); + { + auto ret = get_opt_bad_2(); + } + printf("END: get_opt_bad_2():\n"); +#endif +} + +static KDToolBox::optional get_some_struct() +{ + KDToolBox::optional result; + result.emplace(pod_struct{42, 43}); + return result; +} + +struct eight_optional_bools +{ + KDToolBox::optional m_bools[8]; +}; + +static_assert(sizeof(eight_optional_bools) == sizeof(bool) * 8); + +KDToolBox::optional get_good_bool_1() +{ + KDToolBox::optional result; + result.emplace(true); + return result; +} + +KDToolBox::optional get_good_bool_2() +{ + return KDToolBox::optional(true); +} + +KDToolBox::optional get_good_uint8_t() +{ + return KDToolBox::optional(45); +} + +int main() +{ + extra_move_and_destructor_examples(); + + // Bad output looks like: + // __cdecl Lifetime::Lifetime(unsigned char) noexcept + // __cdecl Lifetime::Lifetime(struct Lifetime &&) noexcept + // __cdecl Lifetime::~Lifetime(void) noexcept + // __cdecl Lifetime::~Lifetime(void) noexcept + + // baseline std::optional - bad examples: + printf("BEGIN: get_std_optional_bad_1():\n"); + { + auto ret = get_std_optional_bad_1(); + assert(ret.has_value()); + + // this is 12 and not 42 because original value not copied in Lifetime class + assert(ret.value().m_value == 12); + } + printf("END: get_std_optional_bad_1()\n\n"); + + printf("BEGIN: get_std_optional_bad_2():\n"); + { + auto ret = get_std_optional_bad_2(); + assert(ret.has_value()); + + // this is 12 and not 42 because original value not copied in Lifetime class + assert(ret.value().m_value == 12); + } + printf("END: get_stdopt_bad_2()\n\n"); + + // Good output looks like: + // Lifetime::Lifetime(int) noexcept + // Lifetime::~Lifetime(void) noexcept + + // baseline std::optional - good (uses emplace internally) + printf("BEGIN: get_std_optional_good():\n"); + { + auto ret = get_std_optional_good(); + assert(ret.has_value()); + assert(ret.value().m_value == 42); + } + printf("END: get_std_optional_good()\n\n"); + + // RVO-enforced optional + // validate return value optimization + printf("BEGIN: get_opt_good_1():\n"); + { + auto ret = get_opt_good_1(); + assert(ret.has_value()); + assert(ret.value().m_value == 42); + } + printf("END: get_opt_good_1()\n\n"); + + printf("BEGIN: get_opt_good_2():\n"); + { + auto ret = get_opt_good_2(); + assert(ret.has_value()); + assert(ret.value().m_value == 42); + } + printf("END: get_opt_good_2()\n\n"); + + printf("BEGIN: get_opt_good_3():\n"); + { + auto ret = get_opt_good_3(); + assert(ret.has_value()); + assert(ret.value().m_value == 42); + } + printf("END: get_opt_good_3()\n\n"); + + { + auto ret = get_good_bool_1(); + assert(ret.has_value()); + assert(ret.value() == true); + } + + { + auto ret = get_good_bool_2(); + assert(ret.has_value()); + assert(ret.value() == true); + } + + { + auto ret = get_good_uint8_t(); + assert(ret.has_value()); + assert(ret.value() == 45); + } + + // Bad value access + // This code will not throw an exception but will + // call assert internally in a Debug build. + { + // KDToolBox::optional test; + // auto bad_value = test.value(); + // if this is a release build, the default constructor + // of the Lifetime struct will be called + // assert(bad_value.m_value == 12); + } + + // This code will throw an exception + // { + // std::optional test; + // auto bad_value = test.value(); + // assert(bad_value.m_value == 1); + // } + + // regular optional - 2 bytes + { + KDToolBox::optional opt; + assert(sizeof(opt) == 2); + opt.emplace(42); + assert(opt.has_value()); + assert(opt.value() == 42); + } + + // bool specialization, this is smaller + { + KDToolBox::optional opt; + assert(sizeof(opt) == 1); + assert(!opt.has_value()); + opt.emplace(true); + assert(opt.has_value()); + assert(opt.value() == true); + bool bool_value = false; + opt.emplace(bool_value); + } + + // test with plain-old-data struct + { + auto ret = get_some_struct(); + assert(ret.has_value()); + assert(ret.value().a == 42); + assert(ret.value().b == 43); + } +} diff --git a/poc/rvo_optional/readme.md b/poc/rvo_optional/readme.md new file mode 100644 index 0000000..f399a4c --- /dev/null +++ b/poc/rvo_optional/readme.md @@ -0,0 +1,14 @@ +# Return Value Optimized optional + +It is possible to use std::optional in a way that it incurs +extra move and destructor calls. + +This demonstrates how to enforce the "right way" which is also possible +with std::optional but which is a bit cubmersome. + +This class does away with the convenience move assignment operator. + +# LLM Use + +Please note that it has been written with LLM assistance / review +which does not indemnify the user of the code.