65.9K
CodeProject 正在变化。 阅读更多。
Home

狂野西部编码:E.B.C.O. 压缩

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.64/5 (3投票s)

2021 年 8 月 28 日

CPOL

12分钟阅读

viewsIcon

6390

downloadIcon

51

将压缩对重写为类型列表

引言

如果您曾经需要将一个或多个类型占用的空间降至最低,那么您可能已经考虑过 std::ebco 基类或压缩对。两者都被划线表示仅供内部使用,可能会发生更改,不面向公众消费,仅凭这一点它们就不理想。此外,包含的类型不止一对,会导致一系列冗长的函数调用链和成员访问。因此,需要考虑哪些类型应该被压缩成第二个,哪些应该被压缩成第一个。这需要对压缩本身有深入的了解,才能布局一个能够实现最高效打包安排的格式。可变长度、压缩的类型列表的创建旨在解决这些问题。

背景

空基类优化是针对没有非静态成员或虚函数的类型执行的一种优化。通常,任何对象的最小大小必须为一,以保证同一类型但不同的对象始终是不同的。但这不适用于基类对象,允许其大小被完全优化掉。这可以作为一种压缩技术,类似于 boost::compressed_pair。

设计

压缩类型的通用设计是一个类,它接受可变数量的类型,一个或多个,并且只暴露一种类型。该类型将只公开一个成员。该成员将是一个函数调用 get。它将接受一个类型作为模板参数,并返回该类型的引用。该引用将能够从 get 调用中赋值,或赋值到 get 调用中。get 调用不会产生函数调用开销,并且与使用引用变量至关重要。compressed 暴露的类型可以通过接受每个类型值的构造函数,或默认构造每个给定类型的默认构造函数来构造。提供的所有错误处理都将以编译时错误消息的形式出现,并且是完整的。也就是说,如果它在没有错误的情况下编译,那么它就应该在没有错误的情况下运行。

我们要做的第一件事是设置压缩类型的定义,它将是一个类,因为它们默认是 private,并设置一些错误条件。其中第一个是使用 static if (if constexpr) 的设计选择。它的使用不是绝对必要的,但可以节省大量辅助类型,并使代码在含义上更清晰。对于不熟悉 if constexpr 的人来说,只有在静态求值为 true 的表达式的作用域内的代码才会被编译。这是 C++17 语言扩展,其使用将代码限制在最低限度。如果此解决方案在早期版本的语言中需要,我将很乐意根据需要修改代码。对于我们的宏评估错误和警告,我们将使用一个特性,其代码 在此处 进行了说明。这还将为我们提供一种激进的 constexprinline 形式。这些将确保我们的类型的构造、析构和使用不会产生任何开销。这些也在上一篇文章中进行了说明,超出了本文的范围。除了这个错误,我们将设置一个空类型错误。与其处理一个空的压缩对象,不如直接禁止它。

template<typename... Types>
class compressed
{
 
#if !defined(__cpp_constexpr)    
    JOE((error("C++ if constexpr required.")))
#elif defined(__cpp_constexpr) && !(__cpp_constexpr >= 201603L)     
#   ifndef COMPRESSED_SILENCE_STATIC_IF_WARNING
        JOE((warning("C++ if constexpr required, 
        check your compiler's documentation for availability. 
        Silence with : COMPRESSED_SILENCE_STATIC_IF_WARNING")))
#   endif // COMPRESSED_SILENCE_STATIC_IF_WARNING
#endif
    static_assert((sizeof...(Types) > 0), 
                   "The given types for compressed must not be empty");

所有辅助类型和特性都将包含在 compressed 类中。由于解决方案的一部分依赖于类型转换来调用函数,因此这些类型必须是 public 才能访问调用。我们可以使用默认为 publicstruct,并允许该类型的每个成员都为 public,以简化此操作。所有这些辅助类型尽管在定义中是 public 的,但从 compressed 外部访问它们将是 private 的。这将确保它们不会被滥用。

template <class _Ty>
using remove_cvref_t = typename std::remove_cv
                       <typename std::remove_reference<_Ty>::type>::type;
 
template <typename Type, typename... Types>
using is_any_of = std::disjunction<std::is_same<Type, Types>...>;
 
template <typename _Type, typename _Other>
using EnableIf = typename std::enable_if<!std::is_same
                 <remove_cvref_t<_Other>, _Type>::value, int>::type;
 
template< typename ..._Types>
struct AutoPair;
template< typename ..._Types>
class PairCat;

我们将创建的第一个辅助类型是 EbcoBase struct。该类型的任务是决定给定类型是否可以受益于空基类优化,并在任何一种情况下提供单一接口。它与 std::ebco 基类不同,因为它提供了一个默认构造函数,这是提供压缩类型的默认构造所必需的。添加了复制构造函数和移动构造函数,这是转发给定值沿构造函数链所必需的。值构造函数受到 enable_if 的保护,该保护用于确保它不会被用作复制构造函数或移动构造函数。它还有一个 static assert 来在提供错误的类型值进行构造时阐明构造函数问题。这似乎也改进了编译器产生的一些错误。在继承自它的其他类型中,我们只需转发可变数量的参数以简化构造函数错误消息。所有成员都用 constexpr 特性装饰,析构函数除外。这在 C++20 及更高版本中可以是 constexpr,但我们将 C++17 作为我们的限制因素。因此,我们使用强制内联。即便如此,只有单个类型的构造函数和析构函数会运行。我们的辅助类型构造函数和析构函数不会运行。我们唯一的 get_value 调用提供了 const 和非 const 重载。我们所有的辅助类型都将遵循此模式。

template<typename _Type, bool = 
         !std::is_final<_Type>::value && std::is_empty<_Type>::value >
struct EbcoBase : private _Type
{ // Empty Base Class Optimization, active
 
    JOE((constexpr)) EbcoBase() noexcept = default;
    JOE((inline))   ~EbcoBase() noexcept = default;
 
    template <class _Other, EnableIf<EbcoBase, _Other> = 0>
    JOE((constexpr))
        explicit EbcoBase(_Other&& _Val) noexcept
        (std::is_nothrow_constructible<_Type, _Other>::value)
        : _Type(std::forward<_Other>(_Val)) {
        static_assert(std::is_constructible<_Type, _Other>::value, 
        "The given type can not be constructed with the provided value");
    }
 
    JOE((constexpr)) EbcoBase(const EbcoBase&) noexcept = default;
    JOE((constexpr)) EbcoBase(EbcoBase&&) noexcept = default;
 
    JOE((constexpr)) _Type& get_value() noexcept { return *this; }
    JOE((constexpr)) const _Type& get_value() const noexcept { return *this; }
};
template<typename _Type>
struct EbcoBase<_Type, false>
{ // Empty Base Class Optimization, inactive
 
    JOE((constexpr)) EbcoBase() noexcept = default;
    JOE((inline))   ~EbcoBase() noexcept = default;
 
    template <class _Other, EnableIf<EbcoBase, _Other> = 0>
    JOE((constexpr))
        explicit EbcoBase(_Other&& _Val) 
        noexcept(std::is_nothrow_constructible<_Type, _Other>::value)
        : value(std::forward<_Other>(_Val)) {
        static_assert(std::is_constructible<_Type, _Other>::value, 
        "The given type can not be constructed with the provided value");
    }
 
    JOE((constexpr)) EbcoBase(const EbcoBase&) noexcept = default;
    JOE((constexpr)) EbcoBase(EbcoBase&&) noexcept = default;
 
    JOE((constexpr)) _Type& get_value() noexcept { return value; }
    JOE((constexpr)) const _Type& get_value() 
                                    const noexcept { return value; }
private:
    _Type value;
};

我们构建的下一个类型 AutoPair 将负责使用多重继承将 EbcoBases 粘合在一起形成一个单一类型。它公开了一个单一的调用 get_type,其任务是构建一个到 get_type 模板参数给定的类型的相应 EbcoBase 的转换。这会调用匹配的 EbcoBase 中相应的 get_value 调用,返回该类型的正确引用。如果给定的类型不是对创建的类型之一,则转换将失败。我们暂时不保护这一点,因为上层还会进行其他调用。

虽然使用相同类型进行多重继承没有问题,但在检索类型值时会出现一个问题。组合到 EbcoBase 的转换,对于任何重复出现的类型,它将转换为相同的类型,并返回相同的值。因此,我们禁止重复类型,因为第二或更多实例将无法通过此方法检索。static assert 确保编译会因重复类型而中断。

我们可以直接暴露 AutoPair 并将其用作最终类型,因为它能够处理无限数量的类型。但是,对于我们继承的每个空类,在第一个之后,编译器将回退到为每种类型分配至少一个字节,以确保每个 subobject 保持独特。由于 ebco 压缩受打包和对齐说明符的影响,这可能会添加任何空类所需的最小说明符大小。例如,假设我们有一个字节对齐和一个打包。我们有三种类型,两个空类和一个指针。在 x64 上,指针是八个字节,我们的第一个空类将受益于 ebco,不增加大小,第二个将增加一个字节。这加起来是九个。如果对齐只在一个类上指定为八个字节,那么我们的总大小将从九个字节跳到十六个字节。因为继承是多重继承,所以对齐说明符将由 compressed_t type 继承。打包说明符为八个字节会产生相同的影响,因为它会像对齐说明符一样添加填充。这就是为什么压缩超过两种类型需要制作类型和对的配对。稍后将详细介绍。

template<typename _Type>
struct AutoPair<_Type> : protected EbcoBase<_Type>
{
    JOE((constexpr)) AutoPair() noexcept = default;
    JOE((inline))   ~AutoPair() noexcept = default;
 
    JOE((constexpr)) AutoPair(const AutoPair&) noexcept = default;
    JOE((constexpr)) AutoPair(AutoPair&&) noexcept = default;
 
    template <class _Other, EnableIf<AutoPair, _Other> = 0>
    JOE((constexpr)) explicit AutoPair(_Other&& _Arg) 
    noexcept(std::is_nothrow_constructible<EbcoBase<_Type>, _Other>::value)
        : EbcoBase<_Type>(std::forward<_Other>(_Arg)) {};
 
    template<typename Type>
    JOE((constexpr)) Type& get_type() noexcept {
        return static_cast<EbcoBase<Type>*>(this)->get_value();
    }
    template<typename Type>
    JOE((constexpr)) const Type& get_type() const noexcept {
        return static_cast<const EbcoBase<Type>*>(this)->get_value();
    }
};
template<typename _Type, typename ..._Types>
struct AutoPair<_Type, _Types...> : 
protected EbcoBase<_Type>, public AutoPair< _Types...>
{
    static_assert(!is_any_of<_Type, _Types...>::value, 
    "The given types for compression must be unique.");
 
    JOE((constexpr)) AutoPair() noexcept = default;
    JOE((inline))   ~AutoPair() noexcept = default;
 
    JOE((constexpr)) AutoPair(const AutoPair&) noexcept = default;
    JOE((constexpr)) AutoPair(AutoPair&&) noexcept = default;
 
    template <typename _Other, typename ..._Others, EnableIf<AutoPair, _Other> = 0>
    JOE((constexpr)) explicit AutoPair(_Other&& _Arg, _Others&&... _Args) 
        noexcept(std::is_nothrow_constructible<EbcoBase<_Type>, _Other>::value && 
        std::is_nothrow_constructible<AutoPair< _Types...>, _Others...>::value)
        : EbcoBase<_Type>(std::forward<_Other>(_Arg))
        , AutoPair< _Types...>(std::forward<_Others>(_Args)...) {};
 
    template<typename Type>
    JOE((constexpr)) Type& get_type() noexcept {
        return static_cast<EbcoBase<Type>*>(this)->get_value();
    }
    template<typename Type>
    JOE((constexpr)) const Type& get_type() const noexcept {
        return static_cast<const EbcoBase<Type>*>(this)->get_value();
    }
};

现在我们有了一种按类型检索值的方法,并且自动应用了适当的压缩。接下来,我们需要合并无限数量的对,并提供一个统一的访问点。这是 PairCat 的责任。它将公开 get 函数调用作为访问类型值的手段。对于单个类型,我们直接继承自 EbcoBase。我们的 get 调用构建到 EbcoBase 指针的 static cast 并调用 get_value,返回给定类型的值。在此层,我们通过 static assert 保护任何转换问题,这也有助于阐明类型失败的原因。

template<typename _Type>
class PairCat<_Type> : private EbcoBase<_Type>
{
public:
    JOE((constexpr)) PairCat() noexcept = default;
    JOE((inline))   ~PairCat() noexcept = default;
 
    JOE((constexpr)) PairCat(const PairCat&) noexcept = default;
    JOE((constexpr)) PairCat(PairCat&&) noexcept = default;
 
    template <class _Other, EnableIf<PairCat, _Other> = 0>
    JOE((constexpr)) explicit PairCat(_Other&& _Arg) 
    noexcept(std::is_nothrow_constructible<EbcoBase<_Type>, _Other>::value)
        : EbcoBase<_Type>(std::forward<_Other>(_Arg)) {};
 
    template<typename Type>
    JOE((constexpr)) Type& get() noexcept {
        static_assert(std::is_same<Type, _Type>::value, 
        "The given type for get must be one of the compression types.");
        return static_cast<EbcoBase<Type>*>(this)->get_value();
    }
    template<typename Type>
    JOE((constexpr)) const Type& get() const noexcept {
        static_assert(std::is_same<Type, _Type>::value, 
        "The given type for get must be one of the compression types.");
        return static_cast<const EbcoBase<Type>*>(this)->get_value();
    }
private:
    template<typename...>
    friend class PairCat;
};

对于两个对象,我们只需继承自 AutoPair 并将 get_type 的调用包装在 get 中。我们私有继承 AutoPair,切断了继承链中到 get_typeget_value 的所有调用,只留下 get 作为访问手段。此模板与自身成为 friend,以允许其他特化访问该特定特化的此特定 get_type 调用,因为它们都将成为彼此的 friend。

template<typename _Ty1, typename _Ty2>
class PairCat< _Ty1, _Ty2> : private AutoPair<_Ty1, _Ty2>
{
    static_assert(!std::is_same<_Ty1, _Ty2>::value, 
    "The given types for compression must be unique.");
    using Base = AutoPair<_Ty1, _Ty2>;
public:
    using Base::Base;
 
    JOE((inline)) ~PairCat() noexcept = default;
 
    JOE((constexpr)) PairCat(const PairCat&) noexcept = default;
    JOE((constexpr)) PairCat(PairCat&&) noexcept = default;
 
    template<typename Type>
    JOE((constexpr)) Type& get() noexcept {
        static_assert(is_any_of<Type, _Ty1, _Ty2>::value, 
        "The given type for get must be one of the compression types.");
        return static_cast<Base*>(this)->get_type<Type>();
    }
    template<typename Type>
    JOE((constexpr)) const Type& get() const noexcept {
        static_assert(is_any_of<Type, _Ty1, _Ty2>::value, 
        "The given type for get must be one of the compression types.");
        return static_cast<const Base*>(this)->get_type<Type>();
    }
private:
    template<typename...>
    friend class PairCat;
};

看起来我们又添加了一个类型来包装 AutoPair,但没有新的功能。但等等,还有更多!我们的下一个特化将消耗三种类型并创建一个类型和对的配对。在我们的 get 调用中,我们必须以不同的方式将调用分派到 get_type。如果类型与第一个类型匹配,则可以直接使用到基类的转换中的 get_type 调用。如果它是其他两种类型之一,那么我们首先必须检索内部对或“PairBase”,并使用该内部对调用 get_type。这时 static if 的使用就派上用场了。

template<typename _Ty1, typename _Ty2, typename _Ty3>
class PairCat< _Ty1, _Ty2, _Ty3> : private AutoPair<_Ty1, AutoPair< _Ty2, _Ty3 > >
{
    static_assert(!is_any_of<_Ty1, _Ty2, _Ty3>::value, 
    "The given types for compression must be unique.");
    using PairBase = AutoPair< _Ty2, _Ty3 >;
    using Base = AutoPair<_Ty1, PairBase >;
public:
    template <typename _Other1, typename _Other2, typename _Other3>
    JOE((constexpr)) explicit PairCat(_Other1&& _Arg1, 
    _Other2&& _Arg2, _Other3&& _Arg3) 
    noexcept(std::is_nothrow_constructible<Base>::value)
        : Base(std::forward<_Other1>(_Arg1)
            , std::forward< PairBase >(
                PairBase(std::forward<_Other2>(_Arg2), 
                std::forward<_Other3>(_Arg3))
                )
        ) { }
    JOE((constexpr)) PairCat() noexcept = default;
    JOE((inline))   ~PairCat() noexcept = default;
 
    JOE((constexpr)) PairCat(const PairCat&) noexcept = default;
    JOE((constexpr)) PairCat(PairCat&&) noexcept = default;
 
    template<typename Type>
    JOE((constexpr)) Type& get() noexcept {
        static_assert(is_any_of<Type, _Ty1, _Ty2, _Ty3>::value, 
        "The given type for get must be one of the compression types.");
        if constexpr (std::is_same<Type, _Ty1>::value)
            return static_cast<Base*>(this)->template get_type<Type>();
        else
            return static_cast<Base*>(this)->template 
            get_type<PairBase>().template get_type<Type>();
    }
    template<typename Type>
    JOE((constexpr)) const Type& get() const noexcept {
        static_assert(is_any_of<Type, _Ty1, _Ty2, _Ty3>::value, 
        "The given type for get must be one of the compression types.");
        if constexpr (std::is_same<Type, _Ty1>::value)
            return static_cast<const Base*>(this)->template get_type<Type>();
        else
            return static_cast<const Base*>(this)->template 
            get_type<PairBase>().template get_type<Type>();
    }
private:
    template<typename...>
    friend class PairCat;
};

下一个特化将是一个递归模板,一次弹出两个类型。这将处理所有其他大于三种类型的数量。它将继承自 typeAutoPairtypePairCatAutoPairPairCat 用于最内层的对,这会无限扩展递归。只有当类型数量为四个或更多时,才会命中此模板。它将弹出其中两个,然后继续到下一个 PairCat。我们的 get 调用将如下分派:如果类型与类型一相同,则调用 get_type 并传入该 type。如果 typetype 二相同,则先检索内部对,然后从中调用 get_type。所有其他类型将通过首先检索内部对,然后检索最内层的对,然后从中调用 get 来处理,以使用该 PairCat 提供的分派。

template<typename _Ty1, typename _Ty2, typename ..._Types>
class PairCat< _Ty1, _Ty2, _Types...> : private AutoPair<_Ty1, 
AutoPair< _Ty2, PairCat<_Types...> > >
{
    static_assert(!is_any_of<_Ty1, _Ty2, _Types...>::value, 
    "The given types for compression must be unique.");
    using CatBase = PairCat<_Types...>;
    using PairBase = AutoPair< _Ty2, CatBase >;
    using Base = AutoPair<_Ty1, PairBase >;
public:
    template <typename _Other1, typename _Other2, typename ..._Others>
    JOE((constexpr)) explicit PairCat(_Other1&& _Arg1, 
    _Other2&& _Arg2, _Others&& ..._Args) 
    noexcept(std::is_nothrow_constructible<Base>::value)
        : Base(std::forward<_Other1>(_Arg1)
            , std::forward< PairBase >(
                PairBase(std::forward<_Other2>(_Arg2),
                    std::forward<PairCat<_Others...> 
                    >(PairCat<_Others...>(std::forward<_Others>(_Args)...))
                )
                )
        ) { }
    JOE((constexpr)) PairCat() noexcept = default;
    JOE((inline))   ~PairCat() noexcept = default;
 
    JOE((constexpr)) PairCat(const PairCat&) noexcept = default;
    JOE((constexpr)) PairCat(PairCat&&) noexcept = default;
 
    template<typename Type>
    JOE((constexpr)) Type& get() noexcept {
        static_assert(is_any_of<Type, _Ty1, _Ty2, _Types...>::value, 
        "The given type for get must be one of the compression types.");
        if constexpr (std::is_same<Type, _Ty1>::value)
            return static_cast<Base*>(this)->template get_type<Type>();
        else if constexpr (std::is_same<Type, _Ty2>::value)
            return static_cast<Base*>(this)->template 
            get_type<PairBase>().template get_type<Type>();
        else
            return static_cast<Base*>(this)->template 
            get_type<PairBase>().template get_type<CatBase>().template get<Type>();
    }
    template<typename Type>
    JOE((constexpr)) const Type& get() const noexcept {
        static_assert(is_any_of<Type, _Ty1, _Ty2, _Types...>::value, 
        "The given type for get must be one of the compression types.");
        if constexpr (std::is_same<Type, _Ty1>::value)
            return static_cast<const Base*>(this)->template get_type<Type>();
        else if constexpr (std::is_same<Type, _Ty2>::value)
            return static_cast<Base*>(this)->template 
            get_type<PairBase>().template get_type<Type>();
        else
            return static_cast<const Base*>(this)->template 
            get_type<PairBase>().template get_type<CatBase>().template get<Type>();
    }
private:
    template<typename...>
    friend class PairCat;
};

现在剩下要做的就是通过 public using 语句/typedef 将我们的 PairCat 公开为一个完成的类型。

public:
    using type = PairCat<Types...>;
};
 
// uses empty base class optimization compression
template<typename... Types>
using compressed_t = typename compressed<Types...>::type;

请注意,PairCat 以及所有其他辅助类型和特性都是 compressed 的 private types。只有 finished type 始终是 public 的。通过添加一个帮助类型 compressed_t 来提取 using 语句,我们的 compressed type 就完成了。

Using the Code

compressed_t 类型有很多用例。例如,作为类的成员,或作为一个单独的变量,或通过直接继承。带有单个类型的 compressed_t 将继承自 EbcoBase。如果给定的类型合适,这将允许动态地进行空基类优化,否则将其作为成员。作为类的成员,它可以用于将多个对象压缩到任何给定类型集合所需的最小空间。作为一个变量,嗯,你只是在吝啬,但可以做到。让我们看一下 std 代码片段的现状。

using MyAlloc = Rebind_alloc_t<Alloc, Ref_count_resource_alloc>;
 
virtual void _Destroy() noexcept override {  
    _Mypair._Get_first()(_Mypair._Myval2._Myval2); // deleter in first, 
                                                   // resource in second's second
}
 
virtual void _Delete_this() noexcept override { 
    MyAlloc Al = _Mypair._Myval2._Get_first();     // allocator in second's first
    this->~_Ref_count_resource_alloc();
    std::_Deallocate_plain(Al, this);
}
 
std::_Compressed_pair<Dx, std::_Compressed_pair<Myalty, 
Resource>> _Mypair; // pair of type and pair

这只有三种类型。随着每增加一个类型,语法会变得越来越困难。五种或更多类型,你就只是一团糟。现在让我们看一下替代语法。

using MyAlloc = Rebind_alloc_t<Alloc, Ref_count_resource_alloc>;
 
virtual void _Destroy() noexcept override {  
    comp.get<Dx>()(comp.get<Resource>());          // deleter in get,
                                                   // resource in get
}

virtual void _Delete_this() noexcept override {  
    MyAlloc Al = comp.get<MyAlloc>();               // allocator in get
    this->~_Ref_count_resource_alloc();
    std::_Deallocate_plain(Al, this);
}
 
joe::compressed_t<Dx, MyAlloc, Resource> comp;  // type list

通常,使用压缩对时,您必须考虑哪一对应该进入哪个成员变量。因为压缩对的第二个总是成员变量。有了 compressed_t,ebco 是动态的,尽管在底层,我们仍在制作类型和对的配对。但是,对压缩一个空类型与一个非空类型的更深层次的理解,将一个空类型放在前面,然后所有其他类型作为空类型和类型的成员变量……这可以通过一个简单的经验法则来避免。所有空类型都放在列表的前面(顺序任意),而所有非空类型都放在列表的最后(顺序任意)。因此……

compressed_t<Dx, MyAlloc, MyExtra, int*, double*> comp;

与……的大小和访问方式相同。

compressed_t<MyAlloc, MyExtra, Dx, double*, int*> comp;

现在让我们看看构造上的区别。

_Ref_count_resource_alloc(Resource Px, Dx Dt, const Alloc& Ax)
    : _Ref_count_base(),
      _Mypair(std::_One_then_variadic_args_t{}, std::move(Dt), 
              std::_One_then_variadic_args_t{}, Ax, Px) {}
_Ref_count_resource_alloc(Resource Px, Dx Dt, const Alloc& Ax)
    : _Ref_count_base(),
      comp(std::move(Dt), Ax, Px) {}

构造起来要简单得多。在构造 compressed_t 期间,没有任何辅助类型的构造函数会运行。相反,所有给定类型的构造函数都会就地运行,并且只有它们的值会被传递给 compressed_t。但是,如果使用了未修饰的前向转发,您将在调试版本中看到完美转发的结果。使用已修饰的前向转发将改变这一点。否则,在发布版本或 O2 或更高级别的任何版本中,它都会被完全优化掉。我提到这一点只是因为一些公司出于某种原因只发布调试版本。辅助类型的析构函数也是如此。只有给定类型的析构函数才会被调用。get 调用的开销与使用就地引用变量的开销完全相同。也就是说,没有函数调用开销。由于它们被视为引用,并且如果它们的值可以在编译时证明,那么它们也有可能被优化掉。这使得它在运行时使用以及在大小方面都成为一种高效的解决方案。

为了证明我们的说法……

#pragma pack(push, 1)
#include <type_traits>
#include <iostream>
#include <stdexcept>
#include <memory>
 
#include "compressed.h"
 
using MyAlloc = std::allocator<int>;

// this type was added not for need, but to show off both the extent of compression, 
// and simplicity of syntax
using MyCAlloc = std::allocator<char>;
using Dx = std::default_delete<int>;

// this type was added for the same reason as MyCAlloc
using DxC = std::default_delete<char>;
using Resource = int*;
 
int main()
{
    // 5-types, the "mess threshold"
  joe::compressed_t<Dx, DxC, MyAlloc, MyCAlloc, 
  Resource> comp(Dx{}, DxC{}, MyAlloc{}, MyCAlloc{}, new int(10));
  //joe::compressed_t<Dx, DxC, MyAlloc, MyCAlloc, Resource> comp;
 
  std::cout << "sizeof of compressed_t : 
  " << sizeof(decltype(comp)) << std::endl; 
  // size : should always be confirmed by a runtime test, 
  // after all alignments and packing have been applied

    if (comp.get<Resource>() != nullptr) {
        // sanity check is unneeded, but does prove the values 
        // are being passed along the constructor chains,
        // this was a problem when trying to support default construction. 
        // Tweaking the enable if and adding copy and move to the helper types 
        // corrected this and now allows for both value construction, 
        // and default construction, provided the types themselves do.
        
        std::cout << "valueof of compressed_t Resource : " 
                  << *comp.get<Resource>() << std::endl;
        comp.get<Dx>()(comp.get<Resource>());
        comp.get<Resource>() = comp.get<MyAlloc>().allocate(1);
        (*comp.get<Resource>()) = 11;
        std::cout << "valueof of compressed_t Resource : "
                  << *comp.get<Resource>() << std::endl;
        comp.get<Dx>()(comp.get<Resource>());

        // these are some of the common things a custom smart pointer might do, 
        // we didn't wrap them in a type, so that we don't get lost in optimizations
        // that would normally be performed on the custom type 
        // vs optimizations on the compressed_t.
    }
    else {
        comp.get<Resource>() = comp.get<MyAlloc>().allocate(1);
        (*comp.get<Resource>()) = 11;
        std::cout << "valueof of compressed_t Resource : " 
                  << *comp.get<Resource>() << std::endl;
        comp.get<Dx>()(comp.get<Resource>());
    }
    std::cin.ignore();
    return 0;
}
#pragma pack(pop)
#undef JOE

这里是 Compiler Explorer 的链接,可以用来玩代码示例

有关在 CE 上使用 MSVC 编译的详细信息,请参阅狂野西部文章 MACROs。如果您直接在 MSVC 上使用它,则无需进行任何更改。否则,我们的目标平台是 Windows、Linux 和 Mac。目标编译器是 GCC、Clang 和 MSVC。

关注点

本文是为了回应 ISO 标准委员会正在研究使用 tuple 作为压缩对替代品的传言而写的。这提供了一种替代方案。由于它使用了鲜为人知的 std::get<Type>() 语法,因此与使用 tuple 差别不大。然而,tuple 不需要使用 ebco 压缩。这提供了两全其美。类型列表、get 语法和 ebco 压缩。但认为普通 C++ 用户,“普通乔”,一个人无法影响委员会,这是一种轻描淡写。这就是您作为读者和 C++ 社区成员可以带来变革的地方。您认为它如何堆叠。所有评论,无论好坏,都欢迎。我只请求如果您有设计上的不满,请发布一个可能的替代方案。这样,我们可以共同发展解决方案。

历史

  • 2021 年 8 月 28 日:初始版本
© . All rights reserved.