【问题标题】:Passing std::array argument with size restricted to expandable set of sizes传递大小限制为可扩展大小集的 std::array 参数
【发布时间】:2018-05-20 03:54:29
【问题描述】:

如何最好地实现一个接受两个std::array<int, [size]> 参数的函数,每个参数的大小约束 由一组在编译时已知的相应值限制?

  • 该函数只能接受大小源自给定集合(枚举/宏/等)的数组
  • 允许的数组“大小”集将来可能会更改并且可能很大(有效地排除函数重载)
  • 无论允许的数组“大小”集发生什么变化,函数本身都应保持固定

问题“Passing a std::array of unknown size to a function”虽然相似,但似乎并不直接适用。

以下在 C++14 中有效,但似乎不必要地冗余和混乱:

#include <type_traits>
#include <array>

// Add legal/allowable sizes for std::array<> "types" here
// Note: Not married to this; perhaps preprocessor instead?
enum class SizesForArrayX : size_t { Three = 3, Four, Forty = 40 };
enum class SizesForArrayY : size_t { Two = 2, Three, EleventyTwelve = 122 };

// Messy, compile-time, value getter for the above enum classes
template <typename S>
constexpr size_t GetSizeValue(const S size)
{ return static_cast<std::underlying_type_t<S>>(size); }

// An example of the function in question; is Template Argument Deduction
//  possible here?
// Note: only arrays of "legal"/"allowable" sizes should be passable
template <SizesForArrayX SX, SizesForArrayY SY>
void PickyArrayHandler(
    const std::array<int, GetSizeValue(SX)>& x,
    const std::array<int, GetSizeValue(SY)>& y)
{
    // Do whatever
    for (auto& i : x) i = 42;
    for (auto& i : y) while (i --> -41) i = i;
}

调用上面的:

int main()
{
    // Declare & (value-)initialize some arrays
    std::array<int, GetSizeValue(SizesForArrayX::Forty)> x{};
    std::array<int, GetSizeValue(SizesForArrayY::Two>) y{};

    //PickyArrayHandler(x, y); // <- Doesn't work; C2672, C2783

    // This works & handles arrays of any "allowable" size but the required
    //  template params are repetitions of the array declarations; ick
    PickyArrayHandler<SizesForArrayX::Forty, SizesForArrayY::Two>(x, y);
}

...丑陋、不优雅、编译缓慢,并且要求声明的数组大小与传递给PickyArrayHandler 函数模板的显式“大小”相匹配

  1. 对于上面的具体示例:PickyArrayHandler 模板有没有办法推导出传递的数组的大小?

  2. 一般来说:有不同的更好的方法吗?

【问题讨论】:

  • 如果从枚举中删除“类”,则不需要“GetSizeValue”和演员表
  • static_assert 在函数内部。
  • @skeller 没有强类型枚举,如何限制数组大小?
  • @Henri 谢谢。我不明白这将如何解决第 2 和第 3 个项目符号
  • @quasinormalized 使用枚举是不可能的。您不能迭代不连续的枚举。

标签: c++ templates c++14 stdarray


【解决方案1】:

由于您似乎对如何定义有效尺寸并不挑剔,因此您可以使用类型特征

#include <array>

template <size_t N> struct valid_size1 { enum { value = false }; };
template <size_t N> struct valid_size2 { enum { value = false }; };

template <> struct valid_size1<3> { enum { value = true }; };
template <> struct valid_size1<4> { enum { value = true }; };
template <> struct valid_size1<40> { enum { value = true }; };

template <> struct valid_size2<2> { enum { value = true }; };
template <> struct valid_size2<122> { enum { value = true }; };

template <size_t TX, size_t TY>
void PickyArrayHandler(const std::array<int, TX> &x,
                       const std::array<int, TY> &y)
{
  static_assert(valid_size1<TX>::value, "Size 1 is invalid");
  static_assert(valid_size2<TY>::value, "Size 2 is invalid");
    // Do whatever
}

int main()
{
    // Declare & (value-)initialize some arrays
    std::array<int, 40> x{};
    std::array<int, 2> y{};

    PickyArrayHandler(x, y);
    PickyArrayHandler(std::array<int, 4>{}, std::array<int, 2>{});
    // PickyArrayHandler(std::array<int, 1>{}, std::array<int, 5>{}); // BOOM!
}

这是一个使用数组的解决方案:

#include <iostream>
#include <array>

constexpr size_t valid_1[] = { 3, 4, 40 };
constexpr size_t valid_2[] = { 2, 122 };

template <size_t V, size_t I=0> 
struct is_valid1 { static constexpr bool value = V==valid_1[I] || is_valid1<V,I+1>::value; };

template <size_t V, size_t I=0> 
struct is_valid2 { static constexpr bool value = V==valid_2[I] || is_valid2<V,I+1>::value; };

template <size_t V>
struct is_valid1<V, sizeof(valid_1)/sizeof(valid_1[0])>
{static constexpr bool value = false; };

template <size_t V>
struct is_valid2<V, sizeof(valid_2)/sizeof(valid_2[0])>
{static constexpr bool value = false; };

template <size_t TX, size_t TY>
void PickyArrayHandler(const std::array<int, TX> &x,
                       const std::array<int, TY> &y)
{
  static_assert(is_valid1<TX>::value, "Size 1 is invalid");
  static_assert(is_valid2<TY>::value, "Size 2 is invalid");
    // Do whatever
}

【讨论】:

  • 这可行,尽管对于 'isValid' 有未引用的本地 var 警告。从未涉足过类型特征,您能否更详细地解释一下{ typename valid_size1&lt;TX&gt;::type isValid; } 发生的情况?
  • @quasinormalized 为了实例化模板,它需要能够创建类型为 valid_size1::type 的变量。如果类型 valid_size1::type 存在,则成功。否则不会。未使用变量,因此您会收到警告。
  • 这很优雅,我喜欢 :)
  • @agbinfo 酷,谢谢!我目前正在玩弄这个答案和斯蒂芬的混合体,看看我是否可以消除编译器警告。你有什么进一步的见解吗?
  • 更新删除警告
【解决方案2】:

转了一圈,让这个减少了的工作:也许它有帮助:

enum SizesForArrayX : size_t { Three = 3, Four, Forty = 40 };
enum SizesForArrayY : size_t { Two = 2, EleventyTwelve = 122 };

template <size_t TX, size_t TY>
void PickyArrayHandler(
    const std::array<int, TX>& x,
    const std::array<int, TY>& y)
{
    // Do whatever
}

int main()
{
    // Declare & (value-)initialize some arrays
    std::array<int, SizesForArrayX::Forty> x{};
    std::array<int, SizesForArrayY::Two> y{};

    PickyArrayHandler(x, y); 

    return 0;
}

【讨论】:

  • 但是你也可以用任何大小的数组来做;它不限制任何东西。
【解决方案3】:

不幸的是,您的枚举不是连续的,因此您不能简单地迭代枚举,您必须单独处理所有情况。由于大小在编译时是已知的,您可以 static_assert 获取它。

#include <array>

enum SizesForArrayX : size_t { Three = 3, Four, Forty = 40 };
enum SizesForArrayY : size_t { Two = 2, EleventyTwelve = 122 };

template <size_t TX, size_t TY>
void PickyArrayHandler(const std::array<int, TX> &x,
                       const std::array<int, TY> &y)
{
    static_assert(TX == Three || TX == Four || TX == Forty,
                  "Size mismatch for x");
    static_assert(TY == Two || TY == EleventyTwelve, "Size mismatch for y");
    // Do whatever
}

int main()
{
    // Declare & (value-)initialize some arrays
    std::array<int, SizesForArrayX::Forty> x{};
    std::array<int, SizesForArrayY::Two> y{};

    PickyArrayHandler(x, y);
    PickyArrayHandler(std::array<int, 4>{}, std::array<int, 2>{});
  //PickyArrayHandler(std::array<int, 1>{}, std::array<int, 5>{}); // BOOM!
}

【讨论】:

    【解决方案4】:

    我认为解决此问题的最佳方法是编写自定义类型特征:

    template <std::underlying_type_t<SizesForArrayX> SX>
    struct is_size_x {
        static constexpr bool value = false;
    };
    
    template <>
    struct is_size_x<static_cast<std::underlying_type_t<SizesForArrayX>>(SizesForArrayX::Forty)>{
    
        static constexpr bool value = true;
    };
    

    我会把这些放在enum class 声明下,这样很容易检查你是否得到了所有这些。比我更聪明的人可能会想出一种方法来使用可变参数 templates 做到这一点,所以你只需要一个专业化。

    虽然乏味,但如果您有一小部分值,这应该足够快并且易于进行单元测试。这种方法的另一个好处是,如果您有多个功能需要这些特殊尺寸之一,您不必复制/粘贴static_asserts。

    使用类型特征,您的函数变得微不足道:

    template <std::size_t SX, std::size_t SY>
    void PickyArrayHandler(
        std::array<int, SX>& x,
        std::array<int, SY>& y)
    {
        static_assert(is_size_x<SX>::value, "Invalid size SX");
        static_assert(is_size_y<SY>::value, "Invalid size SY");
        // Do whatever
        for (auto& i : x) i = 42;
        for (auto& i : y) while (i --> -41) i = i;
    }
    

    最后,您可以先创建一个类型别名以避免创建无效的arrays:

    template <typename T, SizesForArrayX SIZE>
    using XArray =
        std::array<T, static_cast<std::underlying_type_t<SizesForArrayX>>(SIZE)>;
    
    template <typename T, SizesForArrayY SIZE>
    using YArray =
        std::array<T, static_cast<std::underlying_type_t<SizesForArrayY>>(SIZE)>;
    

    这将阻止您声明 array 如果它不是批准的大小:

    XArray<int, SizesForArrayX::Forty> x{};
    YArray<int, SizesForArrayY::Two> y{};
    

    【讨论】:

    • 这个出色的答案比我的 15 岁以下的无形投票所能提供的更多。当我尝试混合这个和@agbinfo 的答案时,有任何消除多余专业化的提示?枚举类只是我对问题的第一次尝试,只要数组大小受到限制,就不需要持续存在。
    • 我不确定如何删除特化,因为这是类型特征背后的技巧之一。如果您有许多值要处理,我建议编写一个简短的脚本(python 可以很好地工作),它可以使用enums 和专业化生成适当的头文件。如果我想出更好的解决方案,我会更新这个答案。
    【解决方案5】:

    就我个人而言,我只是手动将允许的大小输入到static_assert 内的PickyArrayHandler 中。如果这不是一个选项,因为大小将用于程序的其他部分,并且您遵守 DRY 原则,那么我会使用预处理器。

    #define FOREACH_ALLOWABLE_X(SEP_MACRO) \
        SEP_MACRO(3)    \
        SEP_MACRO(4)    \
        SEP_MACRO(40)   \
    
    #define FOREACH_ALLOWABLE_Y(SEP_MACRO) \
        SEP_MACRO(2)    \
        SEP_MACRO(3)    \
        SEP_MACRO(122)  \
    
    
    #define COMMA_SEP(NUM) NUM,
    #define LOGIC_OR_SEP_X(NUM) N1 == NUM ||
    #define LOGIC_OR_SEP_Y(NUM) N2 == NUM ||
    #define END_LOGIC_OR false
    
    // some arrays with your sizes incase you want to do runtime checking
    namespace allowable_sizes
    {
        size_t x[] {FOREACH_ALLOWABLE_X(COMMA_SEP)};
        size_t y[] {FOREACH_ALLOWABLE_Y(COMMA_SEP)};
    }
    
    template <size_t N1, size_t N2>
    void PickyArrayHandler(const std::array<int, N1>& x, const std::array<int, N2>& y)
    {
        static_assert(FOREACH_ALLOWABLE_X(LOGIC_OR_SEP_X) END_LOGIC_OR);
        static_assert(FOREACH_ALLOWABLE_Y(LOGIC_OR_SEP_Y) END_LOGIC_OR);
    
        // do whatever
    }
    
    #undef FOREACH_ALLOWABLE_X
    #undef FOREACH_ALLOWABLE_Y
    #undef COMMA_SEP
    #undef LOGIC_OR_SEP_X
    #undef LOGIC_OR_SEP_Y
    #undef END_LOGIC_OR
    

    一些 C++ 纯粹主义者会对此皱眉头,但它可以完成工作。

    【讨论】:

      【解决方案6】:

      您可以使用类似is_of_size 的模板来检查数组的大小,然后在其中一个大小不匹配时使用它来禁用模板,例如:

      #include <array>
      #include <type_traits>
      
      // Forward template declaration without definition.
      template <class T, T N, T... Sizes>
      struct is_one_of;
      
      // Specialization when there is a single value: Ends of the recursion,
      // the size was not found, so we inherit from std::false_type.
      template <class T, T N>
      struct is_one_of<T, N>: public std::false_type {};
      
      // Generic case definition: We inherit from std::integral_constant<bool, X>, where X
      // is true if N == Size or if N is in Sizes... (via recursion).
      template <class T, T N, T Size, T... Sizes>
      struct is_one_of<T, N, Size, Sizes... >: 
          public std::integral_constant<
              bool, N == Size || is_one_of<T, N, Sizes... >::value> {};
      
      // Alias variable template, for simpler usage.
      template <class T, T N, T... Sizes>
      constexpr bool is_one_of_v = is_one_of<T, N, Sizes... >::value;
      
      template <std::size_t N1, std::size_t N2,
                std::enable_if_t<
                      (is_one_of_v<std::size_t, N1, 3, 4, 40> 
                      && is_one_of_v<std::size_t, N2, 2, 3, 122>), int> = 0>
      void PickyArrayHandler(
          const std::array<int, N1>& x,
          const std::array<int, N2>& y)
      {
      }
      

      那么你可以简单地:

      PickyArrayHandler(std::array<int, 3>{}, std::array<int, 122>{}); // OK
      PickyArrayHandler(std::array<int, 2>{}, std::array<int, 3>{}); // NOK
      

      在 C++17 中,您可以(我认为)将 is_one_of 替换为:

      template <auto N, auto... Sizes>
      struct is_one_of;
      

      ...并自动推导出std::size_t


      在 C++20 中,您可以使用概念来获得更清晰的错误消息;)

      【讨论】:

      • 不确定我是否对模板足够熟悉以遵循您的代码;但是为NOK +1 今天和永远进入我的词典。
      • @quasinormalized 我添加了一些信息 - 基本上你有一个模板递归来检查你传递的第一个大小 (N) 是否是另一个 Sizes 中的一个。我制作了一个通用模板,这就是为什么有一个T,但你可以为std::size_t 提供一个更简单的模板。
      • @quasinormalized 如果你熟悉递归,这个想法很容易理解。基本情况是Sizes... 为空(第一个特化),在这种情况下结果为假(std::false_type),否则,您检查第一个大小 Size 是否匹配 N,或者递归实例化模板其余尺寸为Sizes...
      【解决方案7】:

      static_assert 用于无效尺寸不是一个好的解决方案,因为它不能很好地与 SFINAE 配合使用;即,像 std::is_invocablethe detection idiom 这样的 TMP 工具将对实际上总是产生错误的调用返回误报。更好的是使用 SFINAE 从重载集中删除无效的大小,结果类似于以下内容:

      template<std::size_t SX, std::size_t SY,
               typename = std::enable_if_t<IsValidArrayXSize<SX>{} && IsValidArrayYSize<SY>{}>>
      void PickyArrayHandler(std::array<int, SX> const& x, std::array<int, SY> const& y) {
          // Do whatever
      }
      

      首先我们需要声明我们的有效尺寸;我看不出在这里更强的类型有什么好处,所以对于整数的编译时列表,std::integer_sequence 工作得很好并且非常轻量级:

      using SizesForArrayX = std::index_sequence<3, 4, 40>;
      using SizesForArrayY = std::index_sequence<2, 3, 122>;
      

      现在对于IsValidArraySize 特征...直接的方法是利用 C++14 的宽松-constexpr 规则并执行简单的线性搜索:

      #include <initializer_list>
      
      namespace detail {
          template<std::size_t... VSs>
          constexpr bool idx_seq_contains(std::index_sequence<VSs...>, std::size_t const s) {
              for (auto const vs : {VSs...}) {
                  if (vs == s) {
                      return true;
                  }
              }
              return false;
          }
      } // namespace detail
      
      template<std::size_t S>
      using IsValidArrayXSize
        = std::integral_constant<bool, detail::idx_seq_contains(SizesForArrayX{}, S)>;
      template<std::size_t S>
      using IsValidArrayYSize
        = std::integral_constant<bool, detail::idx_seq_contains(SizesForArrayY{}, S)>;
      

      Online Demo

      但是,如果编译时间完全是一个问题,我怀疑以下内容会更好,如果可能不太清楚:

      namespace detail {
          template<bool... Bs>
          using bool_sequence = std::integer_sequence<bool, Bs...>;
      
          template<typename, std::size_t>
          struct idx_seq_contains;
      
          template<std::size_t... VSs, std::size_t S>
          struct idx_seq_contains<std::index_sequence<VSs...>, S>
            : std::integral_constant<bool, !std::is_same<bool_sequence<(VSs == S)...>,
                                                         bool_sequence<(VSs, false)...>>{}>
          { };
      } // namespace detail
      
      template<std::size_t S>
      using IsValidArrayXSize = detail::idx_seq_contains<SizesForArrayX, S>;
      template<std::size_t S>
      using IsValidArrayYSize = detail::idx_seq_contains<SizesForArrayY, S>;
      

      Online Demo

      无论选择哪种实现方式,以这种方式使用 SFINAE 都会产生非常好的错误消息——例如对于PickyArrayHandler(std::array&lt;int, 5&gt;{}, std::array&lt;int, 3&gt;{});,当前的 Clang 7.0 ToT 会产生以下结果,告诉您 哪个 数组的大小无效:

      error: no matching function for call to 'PickyArrayHandler'
          PickyArrayHandler(std::array<int, 5>{}, std::array<int, 3>{});
          ^~~~~~~~~~~~~~~~~
      
      note: candidate template ignored: requirement 'IsValidArrayXSize<5UL>{}' was not satisfied [with SX = 5, SY = 3]
          void PickyArrayHandler(std::array<int, SX> const& x, std::array<int, SY> const& y) {
               ^
      

      【讨论】:

        猜你喜欢
        • 2021-06-02
        • 2021-06-08
        • 1970-01-01
        • 2020-05-20
        • 2011-05-20
        • 2013-02-24
        • 2011-06-18
        • 2017-02-17
        • 1970-01-01
        相关资源
        最近更新 更多