从类型列表中递归删除重复项导致编译器堆外 space 错误(VS2017)

Recursive duplicate removal from a type list causes compiler out heap space error (VS2017)

我试图从我的实际项目中尽可能多地删除代码。这是重现此错误所需的最少代码。 似乎编译器需要成倍增加 space 添加到基本类型列表的类型越多。

这是为什么?有什么办法解决这个限制吗?

该代码根据其他类型列表和类型构建一个扁平化类型列表,并删除重复项以为列表中注册的每个类型提供唯一 ID。

我需要此代码来处理类型列表中至少 30-50 种类型。代码的目的是为显式列出的类型提供运行时类型反射系统,如本例所示:

#include <tuple>

// Utils
// Type for concatenated tuples
template<typename... input_t> using tuple_cat_t = decltype(std::tuple_cat(std::declval<input_t>()...));

// Helper to check if a type is contained in a tuple already
template <typename T, typename Tuple> struct contains;
template <typename T, typename... Us> struct contains<T, std::tuple<Us...>> : std::disjunction<std::is_same<T, Us>...> {};
template< class T, class U > inline constexpr bool contains_v = contains<T, U>::value;

// Filter Code:
template <class Out, class In> struct filter_duplicates;

// Partial spezialization for the recursive deduction to end (when input tuple is empty)
template <class Out> struct filter_duplicates<Out /*finished filtered output tuple*/, std::tuple<> /*empty input tuple*/>
{
    using filtered_tuple_t = Out;

    // The filter function deduction ends here if the input tuple is empty (every element checked). In this case _In is the found filtered output tuple
    static constexpr Out& filter(Out&& _In) { return _In; }
};

/* Filter template that is used as long the input tuple is not empty. 
It builds the output tuple by recursively checking every element in the 
input tuple and deciding whether is can be added to the output tuple and 
then continuous deduction with the next input element until the 
filter_duplicates definition above matches. */
template <class... OutTypes, class InTest, class... InRest>
struct filter_duplicates<std::tuple<OutTypes...>, std::tuple<InTest, InRest...>>
{
    // contained is true if the InTest element from the input tuple is contained in the output tuple already
    static constexpr bool contained = contains_v<InTest, std::tuple<OutTypes...>>;
    // depending on the condition above either add_t or rem_t is used (which adds InTest to the output tuple or not
    using add_t = filter_duplicates<std::tuple<OutTypes..., InTest>, std::tuple<InRest...>>;
    using rem_t = filter_duplicates<std::tuple<OutTypes...        >, std::tuple<InRest...>>;
    // These types resolve to a tuple<...> with either added or remove InTest type
    using add_tuple_t = typename add_t::filtered_tuple_t;
    using rem_tuple_t = typename rem_t::filtered_tuple_t;
    // This type is the result of the check if InTest is contained, so its either add_tuple_t or rem_tuple_t
    using filtered_tuple_t = std::conditional_t<contained, rem_tuple_t, add_tuple_t>;
    // This type is the result of the check if InTest is contained, so its either the filter_duplicate type containing InTest in the OutTypes or not
    using filter_t = std::conditional_t<contained, rem_t, add_t>;

    // The function takes the unfiltered tuple instance and returns the filtered tuple instance (duplicate tuple entries are filtered)
    static constexpr auto filter(std::tuple<OutTypes..., InTest, InRest...>&& _In)
    {
        return filter_seq<contained>(std::make_index_sequence<sizeof...(OutTypes)>{}, std::make_index_sequence<sizeof...(InRest) + 1 - contained>{}, std::move(_In));
    }

    // The input tuple for the next deduction step is built by getting all tuple elements except "InTest" in the case it is in the output list already
    template<size_t _Skip, size_t... TIndicesOut, size_t... TIndicesIn, class... In>
    static constexpr auto filter_seq(std::index_sequence<TIndicesOut...>, std::index_sequence<TIndicesIn...>, std::tuple<In...>&& _In)
    {
        return filter_t::filter(std::make_tuple(std::move(std::get<TIndicesOut>(_In))..., std::move(std::get<sizeof...(TIndicesOut) + _Skip + TIndicesIn>(_In))...));
    }
};
// Some declarations for easier use
template <class T> using no_duplicates_tuple_t = typename filter_duplicates<std::tuple<>, T>::filtered_tuple_t;
template <class T> using no_duplicates_filter = filter_duplicates<std::tuple<>, T>;
// Function to return a filtered tuple given an unfiltered tuple. It uses the filter type above to construct the new tuple with correct type
template<class... In> constexpr auto make_tuple_no_duplicates(std::tuple<In...>&& _In)
{
    return no_duplicates_filter<std::tuple<In...>>::filter(std::move(_In));
}

// Type info wrapper (In my project it contains functions like construct or copy and much more for the runtime type reflection)
struct IType {};
struct STypeUnknown : IType { using TType = void; };
template<typename T> struct SType : IType { using TType = T; };

// STypeList forward declation
template <typename...> struct STypeList;

// This type unwrappes a given STypeList into a flattened plain tuple, so it can be concatenated with other STypeLists or Types

// In case the given type is just a normal type, it is converted into a tuple with a single element: tuple<T> (so tuple_cat() can be used later)
template<typename T> struct SUnwrapTypeList
{
    using tuple_type = std::tuple<T>;
    tuple_type as_tuple;
    SUnwrapTypeList(T& _Value) : as_tuple{ _Value } {}
};
// In case the Type is a STypeList its filtered and flattened tuple is used
template<typename... TypeDefs> struct SUnwrapTypeList<STypeList<TypeDefs...>>
{
    using TypeList = STypeList<TypeDefs...>;
    using tuple_type = typename TypeList::TTupleDef;
    tuple_type as_tuple;
    SUnwrapTypeList(TypeList& _Value) : as_tuple(_Value.infos) {}
};

// The actual STypeList that can be constructed from other STypeList instances
template<typename... TypeDefs> struct STypeList
{
    // Defines the actual underlying flattened and filtered tuple<...>
    using TTupleDef = no_duplicates_tuple_t<tuple_cat_t<typename SUnwrapTypeList<TypeDefs>::tuple_type...>>;
    // Type count after flattening and filtering
    static constexpr size_t TypeCount() { return std::tuple_size<TTupleDef>::value; }
    // helper to get an index_sequence for the filtered flattened tuple
    static constexpr auto type_seq = std::make_index_sequence<TypeCount()>{};

    // All type infos given via the STypeList constructor, flattened and filtered
    TTupleDef infos;

    // This constructor is used in the example below. It can take arbitrary STypeLists or SType<XXX> lists and flattens and filteres them using the code above
    STypeList(TypeDefs... _Types) noexcept : STypeList(type_seq, std::tuple_cat(SUnwrapTypeList<TypeDefs>(_Types).as_tuple...)) {}
    STypeList(TTupleDef& _Types) noexcept : STypeList(type_seq, _Types) {}
    STypeList(STypeList&& _Move) noexcept : STypeList(type_seq, _Move.infos) {}
    STypeList(const STypeList& _Copy) noexcept : STypeList(type_seq, _Copy.infos) {}

private:
    // Final constructor initializing infos. TListIndices is the index_sequence for the filtered and flattened tuple, while TType is the unfiltered flattened tuple from the constructors above
    template <size_t... TListIndices, typename... TTypes>
    STypeList(std::index_sequence<TListIndices...>, std::tuple<TTypes...>&& _Types) noexcept
        : infos{ make_tuple_no_duplicates(std::move(_Types)) }
    { }

    // Final constructor initializing infos via copy
    template <size_t... TListIndices>
    STypeList(std::index_sequence<TListIndices...>, const TTupleDef& _Infos) noexcept
        : infos(_Infos)
    { }
};

// Test Code:
struct STestType1 { };
struct STestType2 { };

static inline auto TypeListBase = STypeList
(
    STypeUnknown()
    ,SType<bool>()
    ,SType<float>()
    ,SType<double>()
    ,SType<int>()
    ,SType<long>()
    ,SType<char>() //<- comment in to produce error
);

static inline auto TypeListA = STypeList
(
    TypeListBase
    ,SType<STestType1>()
);

static inline auto TypeListB = STypeList
(
    TypeListBase
    ,SType<STestType2>()
);

static inline auto TypeListAB = STypeList
(
    TypeListA,
    TypeListB
);

int main()
{

}

尝试编译输出时(使用 Visual Studio 2017,gcc 和 VS2019 中似乎也会出现问题):

1>------ Build started: Project: VsTest, Configuration: Debug x64 ------
1>VsTest.cpp
1>XXX\vstest.cpp(20): fatal error C1060: compiler is out of heap space
1>Done building project "VsTest.vcxproj" -- FAILED.
========== Build: 0 succeeded, 1 failed, 0 up-to-date, 0 skipped ==========

我看到的问题是你的 filter_duplicate,递归特化,总是遵循两种情况:InTestOutTypes... 列表中的情况和它不在的情况' t.

这迫使编译器遵循大量死例。

建议:将递归特化拆分为两个递归特化:"is in list" 版本和 "isn't in list" 版本。

我建议使用额外的辅助模板

template <typename, typename>
struct with_duplicate : public std::false_type
 { };

template <typename OTuple, typename IT0, typename ... ITs>
struct with_duplicate<OTuple, std::tuple<IT0, ITs...>>
   : public contains<IT0, OTuple>
 { };

现在 filter_duplicates 声明,带有一个额外的模板默认参数(基于 with_duplicate

template <typename Out, typename In, bool = with_duplicate<Out, In>::value>
struct filter_duplicates;

基本情况几乎保持不变(只是添加了第三个模板参数)

// ground case
template <typename Out>
struct filter_duplicates<Out, std::tuple<>, false>
 {
   using filtered_tuple_t = Out;
   static constexpr Out& filter(Out&& _In) { return _In; }
 };

现在是递归的情况"is in list"

// with duplicate case
template <typename... OutTypes, typename InTest, typename... InRest>
struct filter_duplicates<std::tuple<OutTypes...>,
                         std::tuple<InTest, InRest...>,
                         true>
 {
   using filter_t = filter_duplicates<std::tuple<OutTypes...>,
                                      std::tuple<InRest...>>;

   using filtered_tuple_t = typename filter_t::filtered_tuple_t;

   static constexpr auto
      filter (std::tuple<OutTypes..., InTest, InRest...>&& _In)
    { return filter_seq(
          std::make_index_sequence<sizeof...(OutTypes)>{},
          std::make_index_sequence<sizeof...(InRest)>{},
          std::move(_In)); }

   template <std::size_t... TIndicesOut,
             std::size_t... TIndicesIn, typename... In>
   static constexpr auto filter_seq (std::index_sequence<TIndicesOut...>,
                                     std::index_sequence<TIndicesIn...>,
                                     std::tuple<In...>&& _In)
    { return filter_t::filter(std::make_tuple(
             std::move(std::get<TIndicesOut>(_In))...,
             std::move(std::get<sizeof...(TIndicesOut) + 1u + TIndicesIn>(_In))...)); }
 };

和递归情况"isn't in list"

// without duplicate case
template <typename... OutTypes, typename InTest, typename... InRest>
struct filter_duplicates<std::tuple<OutTypes...>,
                         std::tuple<InTest, InRest...>,
                         false>
 {
   using filter_t = filter_duplicates<std::tuple<OutTypes..., InTest>,
                                      std::tuple<InRest...>>;

   using filtered_tuple_t = typename filter_t::filtered_tuple_t;

   static constexpr auto
      filter (std::tuple<OutTypes..., InTest, InRest...>&& _In)
    { return filter_seq(
          std::make_index_sequence<sizeof...(OutTypes)>{},
          std::make_index_sequence<sizeof...(InRest) + 1u>{},
          std::move(_In)); }

   template <std::size_t... TIndicesOut,
             std::size_t... TIndicesIn, typename... In>
   static constexpr auto filter_seq (std::index_sequence<TIndicesOut...>,
                                     std::index_sequence<TIndicesIn...>,
                                     std::tuple<In...>&& _In)
    { return filter_t::filter(std::make_tuple(
             std::move(std::get<TIndicesOut>(_In))...,
             std::move(std::get<sizeof...(TIndicesOut) + TIndicesIn>(_In))...)); }
 };

不确定 filter()filter_seq() 方法中的所有索引是否正确(检查你自己)但是,这样,编译器应该避免很多无用的实现。

在我的 Linux 平台中,拆分递归专业化,我将编译时间(clang++ 和 g++,依次)从 51",1 减少到 1",6。没有关于内存消耗的确切数据,但我可以说它大大减少了。

现在...完全不同的东西...

如何避免 filter()filter_seq() 的递归序列调用,使用 bool 的可变列表来说明哪些元素提取和哪些重复?

我的意思是...在另一个答案class with_duplicate 中给出了相同的助手

template <typename, typename>
struct with_duplicate : public std::false_type
 { };

template <typename OTuple, typename IT0, typename ... ITs>
struct with_duplicate<OTuple, std::tuple<IT0, ITs...>>
   : public contains<IT0, OTuple>
 { };

我建议 filter_duplicate 声明稍微复杂一点

template <typename Out, typename In,
          typename = std::integer_sequence<bool>,
          typename = std::index_sequence<>,
          bool = with_duplicate<Out, In>::value>
struct filter_duplicates;

现在两个递归案例,案例 "is in list" 和 "isn't in list",它们简单地继承自旧的 filter_t 类型

template <typename... OutTypes, typename InTest, typename... InRest,
          typename Is, bool ... Bs>
struct filter_duplicates<std::tuple<OutTypes...>,
                         std::tuple<InTest, InRest...>,
                         std::integer_sequence<bool, Bs...>,
                         Is, true>
   : public filter_duplicates<std::tuple<OutTypes...>,
                              std::tuple<InRest...>,
                              std::integer_sequence<bool, Bs..., true>,
                              std::make_index_sequence<sizeof...(Bs)+1u>>
 { };

template <typename... OutTypes, typename InTest, typename... InRest,
          typename Is, bool ... Bs>
struct filter_duplicates<std::tuple<OutTypes...>,
                         std::tuple<InTest, InRest...>,
                         std::integer_sequence<bool, Bs...>,
                         Is, false>
   : public filter_duplicates<std::tuple<OutTypes..., InTest>,
                              std::tuple<InRest...>,
                              std::integer_sequence<bool, Bs..., false>,
                              std::make_index_sequence<sizeof...(Bs)+1u>>
 { };

现在复杂的部分是接收可变列表 bool 的基本情况,原始元组中的每个类型 Bs...,其中 true 是 "duplicate type" 并且 false 是 "no duplicate type"

// ground case
template <typename Out, bool ... Bs, std::size_t ... Is>
struct filter_duplicates<Out, std::tuple<>,
                         std::integer_sequence<bool, Bs...>,
                         std::index_sequence<Is...>, false>
 {
   using filtered_tuple_t = Out;

   template <bool B, std::size_t I, typename Tpl>
   static constexpr auto getTplCond (Tpl && t)
    {
      if constexpr ( B ) // duplicate
         return std::tuple<>{};
      else // not duplicate
         return std::tuple{std::get<I>(t)};
    }

   template <typename ... Ts>
   static constexpr std::enable_if_t<sizeof...(Ts) == sizeof...(Bs), Out>
       filter (std::tuple<Ts...> && _In)
    { return std::tuple_cat( getTplCond<Bs, Is>(_In)... ); }
 };

这样您就可以调用单个 filter() 方法,该方法不是递归的,而是递归继承的。

filtered_type_t是递归继承的