Search code examples
c++classconstructordestructormicro-optimization

Controlling class member layout AND destructor order


I have a class with two members.

class C {
    typeA a;
    typeB b;
};

Assume that there is no inheritance. There are three things I'm concerned about with this class.

  1. the order in which the constructors for a and b are called
  2. the order in which the destructors for a and b are called
  3. the layout of a and b in memory

One reason I may care about (1) and (2) is thread synchronization. Some reasons I may care about (3) are:

  1. padding

  2. spatial locality with more than two class members

  3. the first class member has memory offset 0

I want to be able to control (1)-(3) simultaneously. However, it seems that when I choose an order in memory, I also choose the order of the constructors and destructors. Is there a way to have control over (1)-(3) simultaneously?


Solution

  • Yes. Spell each individual member like this:

    union {typeA a;};
    

    This prevents them from being automatically constructed and destructed, allowing you to do it manually (::new((void *)&a) typeA(); to construct, a.~typeA(); to destruct).

    Obligatory reminder that if you have exceptions enabled and C's constructor is not noexcept, and one of the constructors throws, you need to destroy the members that were already constructed.


    Took this as a challenge, wrote a tuple class with this feature:

    OrderedTuple<
        //                          construction order
        //               key        |  type
        //               |          |  |
        OrderedTupleElem<"a"_const, 1, int>,
        OrderedTupleElem<"b"_const, 2, std::string>,
        OrderedTupleElem<"c"_const, 0, float>
    > foo;
    
    foo = {42, "bar", 1.23f};
    
    std::cout << foo.get<"b"_const>() << '\n'; // "bar"
    

    Implementation is below.

    While it seems to work well so far, even in presence of exceptions, if you're going to use it in production, it's your job to cover it with tests to make sure I didn't mess up the RAII.

    This requires C++23, but backporting to C++20 shouldn't be hard. (Or to C++17 if you get rid of the string keys.)

    #include <algorithm>
    #include <array>
    #include <cstddef>
    #include <tuple>
    #include <type_traits>
    #include <utility>
    
    template <auto>
    struct ValueTag {};
    
    template <
        auto K, // Arbitrary key.
        std::size_t I, // Construction order index.
        typename T // Element type.
    >
    struct OrderedTupleElem
    {
        static constexpr std::size_t index = I;
        using type = T;
    
        union {T value;};
    
        constexpr OrderedTupleElem() {}
        constexpr ~OrderedTupleElem() {}
    
        constexpr T &DetailGetElem(ValueTag<K>) {return value;}
    };
    
    namespace detail
    {
        template <typename T>
        constexpr bool IsOrderedTupleElemVar = false;
        template <auto K, std::size_t I, typename T>
        constexpr bool IsOrderedTupleElemVar<OrderedTupleElem<K, I, T>> = true;
        template <typename T>
        concept IsOrderedTupleElem = IsOrderedTupleElemVar<T>;
    
        template <typename T, typename...>
        struct FirstType {using type = T;};
    }
    
    template <detail::IsOrderedTupleElem ...P>
    class OrderedTuple : P...
    {
        using P::DetailGetElem...;
    
        static consteval auto MakeFuncArray(auto &&generate)
        {
            static_assert(sizeof...(P) > 0);
            std::array<decltype(+generate.template operator()<typename detail::FirstType<P...>::type, 0>()), sizeof...(P)> ret{};
            [&]<std::size_t ...I>(std::index_sequence<I...>)
            {
                ([&]{
                    if (ret[P::index])
                        throw "Duplicate element index in `OrderedTuple<...>` template arguments.";
                    ret[P::index] = +generate.template operator()<P, I>();
                }(), ...);
            }(std::make_index_sequence<sizeof...(P)>{});
            return ret;
        }
    
        constexpr void DestroyFromIndex(std::size_t i)
        {
            if constexpr (sizeof...(P) > 0)
            {
                static constexpr auto arr = MakeFuncArray([]<typename T, std::size_t>
                {
                    return [](OrderedTuple &self) noexcept
                    {
                        using MemberType = T::type;
                        static_cast<T &>(self).value.~MemberType();
                    };
                });
    
                while (i > 0)
                {
                    i--;
                    arr[i](*this);
                }
            }
        }
    
        struct ConstructorGuard
        {
            OrderedTuple &self;
            std::size_t i = 0;
    
            constexpr ConstructorGuard(OrderedTuple &self) : self(self) {}
    
            ConstructorGuard(const ConstructorGuard &) = delete;
            ConstructorGuard &operator=(const ConstructorGuard &) = delete;
    
            constexpr ~ConstructorGuard()
            {
                self.DestroyFromIndex(i);
            }
    
            void Finish() {i = 0;}
    
            template <typename T, typename ...Q>
            constexpr void Construct(Q &&... params)
            {
                if constexpr (sizeof...(P) > 0)
                {
                    ::new((void *)&static_cast<T &>(self).value) T::type(std::forward<Q>(params)...);
                    i++; // On a separate line, to be extra sure it doesn't happen if `Construct()` throws.
                }
            }
        };
    
        static constexpr void InOrder(auto &&func)
        {
            if constexpr (sizeof...(P) > 0)
            {
                static constexpr auto arr = MakeFuncArray([]<typename T, std::size_t I>
                {
                    return [](decltype((func)) func) {func.template operator()<T, I>();};
                });
                for (auto x : arr)
                    x(func);
            }
        }
    
      public:
        constexpr OrderedTuple()
        noexcept((std::is_nothrow_default_constructible_v<typename P::type> && ...))
        requires(std::is_default_constructible_v<typename P::type> && ...)
        {
            ConstructorGuard guard(*this);
            InOrder([&]<typename T, std::size_t>{guard.template Construct<T>();});
            guard.Finish();
        }
    
        constexpr OrderedTuple(const OrderedTuple &other)
        noexcept((std::is_nothrow_copy_constructible_v<typename P::type> && ...))
        requires(std::is_copy_constructible_v<typename P::type> && ...)
        {
            ConstructorGuard guard(*this);
            InOrder([&]<typename T, std::size_t>{guard.template Construct<T>(static_cast<const T &>(other).value);});
            guard.Finish();
        }
    
        constexpr OrderedTuple(OrderedTuple &&other)
        noexcept((std::is_nothrow_move_constructible_v<typename P::type> && ...))
        requires(std::is_move_constructible_v<typename P::type> && ...)
        {
            ConstructorGuard guard(*this);
            InOrder([&]<typename T, std::size_t>{guard.template Construct<T>(std::move(static_cast<T &>(other).value));});
            guard.Finish();
        }
    
        constexpr OrderedTuple &operator=(const OrderedTuple &other)
        noexcept((std::is_nothrow_copy_assignable_v<typename P::type> && ...))
        requires(std::is_copy_assignable_v<typename P::type> && ...)
        {
            InOrder([&]<typename T, std::size_t>
            {
                static_cast<T &>(*this).value = static_cast<const T &>(other).value;
            });
            return *this;
        }
    
        constexpr OrderedTuple &operator=(OrderedTuple &&other)
        noexcept((std::is_nothrow_move_assignable_v<typename P::type> && ...))
        requires(std::is_move_assignable_v<typename P::type> && ...)
        {
            InOrder([&]<typename T, std::size_t>
            {
                static_cast<T &>(*this).value = std::move(static_cast<T &>(other).value);
            });
            return *this;
        }
    
        constexpr ~OrderedTuple() noexcept
        {
            DestroyFromIndex(sizeof...(P));
        }
    
        template <typename ...Q>
        constexpr OrderedTuple(Q &&... params)
        noexcept((std::is_nothrow_constructible_v<typename P::type, Q &&> && ...))
        requires(std::is_constructible_v<typename P::type, Q &&> && ...)
        {
            ConstructorGuard guard(*this);
            InOrder([&]<typename T, std::size_t I>
            {
                guard.template Construct<T>(std::get<I>(std::forward_as_tuple(std::forward<Q>(params)...)));
            });
            guard.Finish();
        }
    
        template <auto K>
        static constexpr bool contains_key = []{
            if constexpr (sizeof...(P) == 0)
                return false;
            else
                return requires(OrderedTuple &self){self.DetailGetElem(ValueTag<K>{});};
        }();
    
        template <auto K> requires contains_key<K> [[nodiscard]]       auto & get()       &  {return           this->DetailGetElem(ValueTag<K>{}) ;}
        template <auto K> requires contains_key<K> [[nodiscard]] const auto & get() const &  {return           this->DetailGetElem(ValueTag<K>{}) ;}
        template <auto K> requires contains_key<K> [[nodiscard]]       auto &&get()       && {return std::move(this->DetailGetElem(ValueTag<K>{}));}
        template <auto K> requires contains_key<K> [[nodiscard]] const auto &&get() const && {return std::move(this->DetailGetElem(ValueTag<K>{}));}
    };
    
    // ---
    
    #include <iostream>
    
    template <std::size_t N>
    struct ConstString
    {
        char array[N]{};
        consteval ConstString(const char (&source)[N])
        {
            std::copy_n(source, N, array);
        }
    };
    
    template <ConstString S>
    [[nodiscard]] consteval decltype(S) operator""_const()
    {
        return S;
    }
    
    #define MAKE_TEST_CLASS(A, copy_throws_) \
        struct A \
        { \
            A() {std::cout << #A "()\n";} \
            A(const A &) {std::cout << #A "(const " #A " &)\n"; if (copy_throws_) throw 42;} \
            A(A &&) {std::cout << #A "(" #A " &&)\n"; if (copy_throws_) throw 42;} \
            A &operator=(const A &) {std::cout << #A " &operator=(const " #A " &)\n"; return *this;} \
            A &operator=(A &&) {std::cout << #A " &operator=(" #A " &&)\n"; return *this;} \
            ~A() {std::cout << "~" #A "()\n";} \
        };
    
    MAKE_TEST_CLASS(A, false)
    MAKE_TEST_CLASS(B, false)
    MAKE_TEST_CLASS(C, false)
    MAKE_TEST_CLASS(X, true)
    
    int main()
    {
        {
            OrderedTuple<
                OrderedTupleElem<"foo"_const, 1, A>,
                OrderedTupleElem<"bar"_const, 2, B>,
                OrderedTupleElem<"baz"_const, 0, C>
            > x, y(x), z(std::move(x)), w(A(), B(), C{});
            x = y;
            x = std::move(y);
    
            (void)x.get<"foo"_const>();
            (void)x.get<"bar"_const>();
            (void)x.get<"baz"_const>();
    
            OrderedTuple<> empty1, empty2(empty1), empty3(std::move(empty1));
            empty1 = empty2;
            empty1 = std::move(empty2);
        }
    
        // Check partial destruction:
        try
        {
            OrderedTuple<
                OrderedTupleElem<"a"_const, 2, A>,
                OrderedTupleElem<"b"_const, 1, X>,
                OrderedTupleElem<"c"_const, 0, C>
            > foo(A(), X(), C{});
        }
        catch (...) {}
    }