C++ Empty Class Optimization

kaleidopink發表於2024-04-13

C++ ECO 是一種對於空型別的記憶體佈局最佳化策略. 在 C++ 中不允許存在大小為零的型別, 即便是空類也會佔有一個位元組的大小, 像 void 和沒有定義的型別稱為「非完備型別」(Incomplete Type).

​ 這帶來一個問題, 如果將空類作為成員變數的型別, 則每個成員都會佔用至少一個位元組的大小:

struct NE1 {
    E1 e1;
    E2 e2;
};

int main() {
    printf("%zd\n", sizeof(NE1)); // 2
}

大多數情況下, 還會因為記憶體對齊的原因, 導致所佔用的空間還要大於一個位元組:

struct NE2 {
    int i;
    E1 e1;
    E2 e2;
};

int main() {
  printf("%zd\n", sizeof(NE2)); // 8
}

為了解決這個問題, 可以使用 ECO 最佳化, 即 Empty Base Optimization. 原理在於將空類作為另一個型別的基類時, 該基類將不會佔用多餘的空間.

struct BE1: E1, E2 {};
struct BE2: E1, E2 {
    int i;
};

int main() {
    printf("%zd\n", sizeof(BE1)); // 1
    printf("%zd\n", sizeof(BE2)); // 4
}

​ 基於此特性, 可以用來實現一些複合模板類, 因為其型別引數可能為任意型別, 也可能為空類, 適合進行 ECO 最佳化. 例如 boost::compressed_pair 就使用了ECO.

​ 基於 ECO 設計一個 compressed_pair:

namespace evo {

template <typename T1, typename T2>
struct compressed_pair;

template <typename T, size_t I, bool CanBeEmptyBase = 
    std::is_empty_v<T> && !std::is_final_v<T>>
struct compressed_pair_elem {
    typedef T& reference;
    typedef T const& const_reference;

    template <typename U>
    requires (!evo::is_same_v<compressed_pair_elem, evo::decay<U>>)
    explicit compressed_pair_elem(U&& u):
        value(evo::forward<U>(u)) {}

    reference get() noexcept {
        return this->value;
    }

    const_reference get() const noexcept {
        return this->value;
    }

private:
    T value;
};

// Empty Base Optimization
// A empty, non-final type should be inherited instead of 
// put inside the compressed_pair_elem.
template <typename T, size_t I>
struct compressed_pair_elem<T, I, true>: public T {
    typedef T value_type;
    typedef T& reference;
    typedef T const& const_reference;

    constexpr explicit compressed_pair_elem() = default;

    template <typename U>
    requires (!evo::is_same_v<compressed_pair_elem, evo::decay<U>>)
    explicit compressed_pair_elem(U&& u):
        value_type(evo::forward<U>(u)) {}

    reference get() noexcept {
        return *this; // implicit conversion to the base reference.
    }

    const_reference get() const noexcept {
        return *this;
    }
};

template <typename T1, typename T2>
struct compressed_pair: 
    compressed_pair_elem<T1, 1>,
    compressed_pair_elem<T2, 2> 
{
    static_assert((!is_same<T1, T2>::value),
            "compressed_pair cannot be instantiated when T1 and T2 are the same type; ");

    typedef compressed_pair_elem<T1, 1> Base1;
    typedef compressed_pair_elem<T2, 2> Base2;

    explicit compressed_pair()
    requires (
        evo::is_default_constructible_v<T1> &&
        evo::is_default_constructible_v<T2>
    ) = default;

    template <typename U1, typename U2>
    requires (
        evo::is_constructible_v<T1, U1> && 
        evo::is_constructible_v<T2, U2>
    )
    explicit compressed_pair(U1&& u1, U2&& u2):
        Base1(std::forward<U1>(u1)), 
        Base2(std::forward<U2>(u2)) {}

    typename Base1::reference first() noexcept {
        return static_cast<Base1&>(*this).get();
    }
    typename Base1::const_reference first() const noexcept {
        return static_cast<Base1 const&>(*this).get();
    }

    typename Base2::reference second() noexcept {
        return static_cast<Base2&>(*this).get();
    }
    typename Base2::const_reference second() const noexcept {
        return static_cast<Base2 const&>(*this).get();
    }

    void swap(compressed_pair& other) {
        evo::swap(first(), other.first());
        evo::swap(second(), other.second());
    }
};

template <typename T1, typename T2>
void swap(compressed_pair<T1, T2>& p1, compressed_pair<T1, T2>& p2) {
    evo::swap(p1.first(), p2.first());
    evo::swap(p1.second(), p2.second());
}

相關文章