【问题标题】:Why static-sized array type cannot be a container type?为什么静态大小的数组类型不能是容器类型?
【发布时间】:2017-11-24 13:11:19
【问题描述】:

我有一个静态大小数组的别名,使用起来很简单:

using triplet_t = std::uint8_t[3];

//           vvvvvvvvvvvvvvvvvv <--- easier than std::uint8_t(&triplet)[3]
void f(const triplet_t &triplet) { /* whatever */ }

triplet_t t{}; // As good as std::uint8_t t[3]{};

t[0] = '0';
t[1] = '1';
t[2] = '2';
for (auto &v : t) std::cout << v << ' ';
std::cout << '\n';

// So far so good...
triplet_t t3[3]{};
for (auto &r : t3)
    for(auto &v : r)
        v = 42;

我什至可以在容器中使用别名:

std::vector<triplet_t> vt;

或者我以前是这么想的,因为一旦你使用vt,它就会失败:

vt.push_back({});

GCC 8.0.0 201711

error: parenthesized initializer in array new [-fpermissive]
{ ::new((void *)__p) _Up(std::forward<_Args>(__args)...); }
  ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

error: request for member '~unsigned char [3]' in '* __p', which is of non-class type 'unsigned char [3]'
destroy(_Up* __p) { __p->~_Up(); }
                    ~~~~~~^~~

问题似乎在于,在展开所有模板技巧后,将调用placement-new 来转发所有带括号提供的参数,显然这不是初始化静态大小数组的方法。

此外,不知何故,容器将triplet_t 视为一个对象,因此要求使用析构函数,再次编译失败。没有别名,问题显然是一样的:

std::vector<std::uint8_t[3]> vt;
vt.push_back({});          // Boom!
vt.push_back({255, 0, 0}); // Ouch!

但使用具有相同内存布局的struct 没有问题:

struct rgb { std::uint8_t r, g, b; };
std::vector<rgb> vt;
vt.push_back({});          // Nice!
vt.push_back({255, 0, 0}); // Cool!

我想知道为什么会发生这种情况,有没有办法在容器中使用静态大小的数组作为包含类型?

【问题讨论】:

  • “有没有办法在容器中使用静态大小的数组作为包含类型?”应该可以使用std::array
  • C 数组不可复制。
  • 你和using triplet_t = std::array&lt;std::uint8_t,3&gt;得到同样的结果吗?
  • 结构包装器可能会有所帮助。 struct chocolate { std::uint8_t[3] nougat; }.
  • @Jarod42 你的评论让我想到了为什么人们会问关于 C/C++ 的问题

标签: c++ stl containers


【解决方案1】:

阅读std::vector documentaion,可以发现T必须满足CopyAssignable和CopyConstructible的要求。

这意味着(简化):对于vt 两个T 类型的实例,表达式t = v 必须是合法的。显然,如果T 是一个原生数组,则情况并非如此(不能将 C 数组分配给另一个数组),std::vector&lt;T&gt; 的某些函数格式不正确。

解决方案是将triplet_t 定义为:

using triplet_t = std::array<std::uint8_t, 3>;

void f(const triplet_t &triplet) { /* whatever */ }

triplet_t t{};

t[0] = '0';
t[1] = '1';
t[2] = '2';
for (auto &v : t) std::cout << v << ' ';
std::cout << '\n';

// So far so good...
triplet_t t3[3]{};
for (auto &r : t3)
    for(auto &v : r)
        v = 42;

std::vector<triplet_t> vt;

vt.push_back({});

【讨论】:

  • 其实push_back并不要求类型是CopyAssignable,而是CopyInsertable或者MoveInsertable
  • @Jodocus 一旦调用了UB,在鼻恶魔的宏伟计划中一切都是一样的;)但我同意你的答案更好。我会赞成的;)
  • 其实不是UB。如果您更仔细地阅读您发布的链接,因为 C++11,vector 不需要类型为CopyAssignable。严格的规则放宽了,这意味着类型要求取决于实际执行的操作,而不是容器。
  • @Jodocus 我以为是 cppreference 简化了事情,但你是对的。根据[vector],这些是要求,实施必须提供诊断。这不是UB ^^。
【解决方案2】:

根据documentationpush_back 要求值类型为CopyInsertableMoveInsertable。来看看definition

类型 T 是 CopyInsertable 到容器 X 中,如果 T 是 MoveInsertable,则其 value_type 与 T 相同到 X 中,并且,给定 [...] 以下表达式是格式良好的:

std::allocator_traits<A>::construct(m, p, v);

所以在 C 数组的情况下,对于标准分配器,有一个表达式 like

::new((void *)p) int[3](std::forward<int[3]>(v))

其中 v 是数组类型。根据specification of new,这是格式错误的:

  • 如果 type 是数组类型,则初始化对象数组。
    • 如果没有初始化,每个元素都是默认初始化的
    • 如果初始化程序是一对空括号,则每个元素都进行值初始化。
    • 如果初始化程序是用大括号括起来的参数列表,则数组是聚合初始化的。

没有允许非空括号的数组类型语法。

论证与MoveInsertable 类别非常相似。 总而言之,建议的解决方案是使用(已经提到的)std::array,它本身不是数组类型,因此可以通过标准分配器的construct 函数采用的语法正确初始化。

最后一点:严格的别名规则只允许使用unsigned charsigned charchar 进行任何类型的转换。虽然几乎可以肯定 std::uint8_t 只是您实现中其中一个的别名 typedef,但标准中并不能保证这一点。

【讨论】:

    猜你喜欢
    • 2010-10-25
    • 1970-01-01
    • 2014-09-27
    • 2012-05-27
    • 2021-11-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多