使用零大小数组的 std::swap() 编译错误?

Posted

技术标签:

【中文标题】使用零大小数组的 std::swap() 编译错误?【英文标题】:Compile error for std::swap() with zero-size arrays? 【发布时间】:2014-06-10 21:47:22 【问题描述】:

在数组长度变为 0 的情况下,我试图避免为我的类模板添加显式特化。事实证明 std::swap() 无法处理它:

#include <algorithm>

int main() 
    int a[0], b[0];

    std::swap(a, b);   // g++-4.8 compile error

我认为应该有一些 SFINAE 来防止这样的错误,不是吗?显然,在这种情况下,什么都不做是正确的。

如果标准强制std::swap() 引发编译器错误,我可以手动添加一个编译时if 来检查非类型模板参数std::size_t N 是否为0?

编辑

确实,std::array&lt;T, 0&gt; 是一个专门的模板,可以避免声明一个大小为零的数组。来自gcc-4.8.2/libstdc++-v3/include/std/array

template<typename _Tp, std::size_t _Nm>
    struct __array_traits
    
      typedef _Tp _Type[_Nm];

      static constexpr _Tp&
      _S_ref(const _Type& __t, std::size_t __n) noexcept
       return const_cast<_Tp&>(__t[__n]); 
    ;

 template<typename _Tp>
   struct __array_traits<_Tp, 0>
   
     struct _Type  ;

     static constexpr _Tp&
     _S_ref(const _Type&, std::size_t) noexcept
      return *static_cast<_Tp*>(nullptr); 
   ;

【问题讨论】:

在标准 C 和 C++ 中,不允许使用零大小的数组。 它能处理其他大小的数组吗?我希望它不适用于数组期间,因为您无法分配给数组。 @40two:它似乎适用于除std::swap() 调用之外的所有内容,不过...... @Stefan 在 gcc 中您会收到警告:ISO C++ 禁止零大小数组。从那里发生的事情是未定义的行为。 @Stefan:这不是灰色地带。这是明确和明确禁止的。将模板特化为不使用 0 大小的数组。 【参考方案1】:

这是在 C++ 中定义零大小数组的未定义行为。

C++ 标准 n3337 § 8.3.4/1

如果常量表达式 (5.19) 存在,它应该是一个整数 常量表达式,其值应大于零。

但是使用 array new 动态创建零大小的数组是有效的:new[]:

C++ 标准 n3337 § 5.3.4/6

noptr-new-declarator 中的每个常量表达式都应该是 积分常数表达式 (5.19) 并严格计算为 正值。 noptr-new-declarator 中的表达式应为 整数类型、无作用域枚举类型或一个类类型 单个非显式转换函数到整数或无范围 枚举类型存在 (12.3)。如果表达式是类类型, 通过调用该转换函数来转换表达式,并且 转换的结果被用来代替原来的 表达。

C++ 标准 n3337 § 5.3.4/7

当 noptr-new-declarator 中表达式的值为零时调用分配函数来分配一个没有 如果该表达式的值小于零等 分配对象的大小将超过 实现定义的限制,或者如果 new-initializer 是一个花括号- 初始化子句数超过 要初始化的元素数量,没有获得存储,并且 new-expression 通过抛出一个类型的异常来终止 将匹配 std::bad_array_new_length 类型的处理程序 (15.3) (18.6.2.2)。

【讨论】:

是的,new T[0] 是允许的。见 5.3.4/7。【参考方案2】:

免责声明: 已经说明 C++ 不允许零长度数组,因此创建/交换它们是未定义的行为。 gcc 支持零长度数组作为扩展。 所以后面的一切可能只适用于 gcc,不适用于其他编译器。


编译错误没有说明零长度数组。如果您启用-pedantic,则会出现关于它们的警告,但它们不会被完全拒绝。相反,编译器抱怨分配无效。原因很有趣。

std::swap 对数组类型有重载。但是,由于零长度数组不被视为有效的数组类型,因此当您传入零长度数组时,不会选择此重载。这可以用下面的代码来演示:

template<typename T, std::size_t N>
void foo(T const (&)[N])

    std::cout << __PRETTY_FUNCTION__ << std::endl;


template<typename T>
void foo(T const&)

    std::cout << __PRETTY_FUNCTION__ << std::endl;

将非零长度数组传递给foo,输出为

void foo(const T (&)[N]) [with T = int; long unsigned int N = 10ul]

现在将一个长度为零的数组传递给foo,输出会发生变化

void foo(const T&) [with T = int [0]]

Live demo


现在,回到错误的原因。非数组的std::swap 实现会将一个参数移动/复制到局部变量,然后将第二个参数移动/复制到第一个参数,最后将局部变量移动/复制到第二个参数。正是这一系列的移动/复制初始化和分配出错了。

T temp = move(arg1);
arg2 = move(arg2);
arg1 = move(temp);

T=int[0]时上述语句均无效,故报错。


解决此问题的最简单方法是使用std::array。它对零长度数组有特殊的支持,并且交换它们将正常工作。

否则,如果您想继续依赖不可移植的 gcc 扩展,我将为 swap 创建一个包装器,该包装器具有接受零长度数组的重载。在所有其他情况下,包装器将调用 std::swap

template<typename T>
void my_swap(T& arg1, T& arg2)

    using std::swap;
    swap(arg1, arg2);


template<typename T, std::size_t N>
void my_swap(T (&arg1)[N], T (&arg2)[N])

    using std::swap;
    swap(arg1, arg2);


template<typename T>
void my_swap(T (&)[0], T (&)[0])

    // do nothing

Live demo

【讨论】:

感谢您的分析和出色的解决方法!现在我的课程在没有代码重复的情况下工作正常(请参阅我的帖子中的编辑,了解std::arraygcc-4.8 的代码重复版本)。我认为零大小的数组最终会进入标准,因为相反是愚蠢的。 @Stefan 我不建议解决这个问题。你应该使用std::array 除了我的类是我自己版本的std::array,所以我不想依赖工厂版本。另外,如果gcc 允许零大小的数组,它自己的std::swap() 应该知道如何处理它们。在他们修复错误之前,您的解决方法很棒;)

以上是关于使用零大小数组的 std::swap() 编译错误?的主要内容,如果未能解决你的问题,请参考以下文章

在 Visual Studio 中调用 std::swap 时的 std::bad_function_call

ISO C++ 禁止可变大小数组(编译错误)

插入数组列表时,索引(从零开始)必须大于或等于零且小于参数列表的大小

使用宏来专门化std :: swap

如何修复“ValueError:零大小数组到没有标识的归约操作 fmin”

将 std::iter_swap 用于 std::optional