控制类成员布局和析构函数顺序

Bob*_*bby 1 c++ constructor destructor class micro-optimization

我的班级有两名成员。

class C {
    typeA a;
    typeB b;
};
Run Code Online (Sandbox Code Playgroud)

假设没有继承。对于这门课,我担心三件事。

  1. a和 的构造函数的b调用顺序
  2. a和 的析构函数的b调用顺序
  3. ab在内存中的布局

我可能关心(1)和(2)的原因之一是线程同步。我可能关心 (3) 的一些原因是:

  1. 填充

  2. 具有两个以上类成员的空间局部性

  3. 第一个类成员的内存偏移量为 0

我希望能够同时控制(1)-(3)。然而,似乎当我选择内存中的顺序时,我也选择了构造函数和析构函数的顺序。有没有办法同时控制(1)-(3)?

Hol*_*Cat 6

是的。像这样拼写每个单独的成员:

union {typeA a;};
Run Code Online (Sandbox Code Playgroud)

这可以防止它们被自动构造和破坏,从而允许您手动执行(::new((void *)&a) typeA();构造、a.~typeA();破坏)。

必须提醒的是,如果您启用了异常,而C的构造函数未启用noexcept,并且其中一个构造函数抛出异常,则需要销毁已构造的成员。


以此为挑战,编写了一个具有此功能的元组类:

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"
Run Code Online (Sandbox Code Playgroud)

实施如下。

虽然到目前为止它似乎运行良好,即使存在异常,但如果您打算在生产中使用它,您的工作就是通过测试来覆盖它,以确保我不会弄乱 RAII。

这需要 C++23,但向后移植到 C++20 应该不难。(或者如果您去掉了字符串键,则转为 C++17。)

#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 (...) {}
}
Run Code Online (Sandbox Code Playgroud)