Initializer-list-构造一个不可复制(但可移动)对象的向量

Posted

技术标签:

【中文标题】Initializer-list-构造一个不可复制(但可移动)对象的向量【英文标题】:Initializer-list-constructing a vector of noncopyable (but movable) objects 【发布时间】:2011-11-06 01:51:38 【问题描述】:

可以将不可复制但可移动类型的push_back 右值放入该类型的向量中:

#include <vector>

struct S

    S(int);
    S(S&&);
;

int main()

    std::vector<S> v;
    v.push_back(S(1));
    v.push_back(S(2));
    v.push_back(S(3));

但是,当我尝试使用相同的右值初始化向量列表时,我收到有关需要复制构造函数的错误:

#include <vector>

struct S

    S(int);
    S(S&&);
;

int main()

    std::vector<S> v = S(1), S(2), S(3);

我在使用 GCC 4.7 时遇到以下错误:

In file included from include/c++/4.7.0/vector:63:0,
                 from test.cpp:1:
include/c++/4.7.0/bits/stl_construct.h: In instantiation of 'void std::_Construct(_T1*, _Args&& ...) [with _T1 = S, _Args = const S&]':
include/c++/4.7.0/bits/stl_uninitialized.h:77:3:   required from 'static _ForwardIterator std::__uninitialized_copy<_TrivialValueTypes>::__uninit_copy(_InputIterator, _InputIterator, _ForwardIterator) [with _InputIterator = const S*, _ForwardIterator = S*, bool _TrivialValueTypes = false]'
include/c++/4.7.0/bits/stl_uninitialized.h:119:41:   required from '_ForwardIterator std::uninitialized_copy(_InputIterator, _InputIterator, _ForwardIterator) [with _InputIterator = const S*, _ForwardIterator = S*]'
include/c++/4.7.0/bits/stl_uninitialized.h:260:63:   required from '_ForwardIterator std::__uninitialized_copy_a(_InputIterator, _InputIterator, _ForwardIterator, std::allocator<_Tp>&) [with _InputIterator = const S*, _ForwardIterator = S*, _Tp = S]'
include/c++/4.7.0/bits/stl_vector.h:1185:4:   required from 'void std::vector<_Tp, _Alloc>::_M_range_initialize(_ForwardIterator, _ForwardIterator, std::forward_iterator_tag) [with _ForwardIterator = const S*, _Tp = S, _Alloc = std::allocator<S>]'
include/c++/4.7.0/bits/stl_vector.h:362:2:   required from 'std::vector<_Tp, _Alloc>::vector(std::initializer_list<_Tp>, const allocator_type&) [with _Tp = S, _Alloc = std::allocator<S>, std::vector<_Tp, _Alloc>::allocator_type = std::allocator<S>]'
test.cpp:11:41:   required from here
include/c++/4.7.0/bits/stl_construct.h:77:7: error: no matching function for call to 'S::S(const S&)'
include/c++/4.7.0/bits/stl_construct.h:77:7: note: candidates are:
test.cpp:6:5: note: S::S(S&&)
test.cpp:6:5: note:   no known conversion for argument 1 from 'const S' to 'S&&'
test.cpp:5:5: note: S::S(int)
test.cpp:5:5: note:   no known conversion for argument 1 from 'const S' to 'int'

这应该被允许吗?我认为它没有被允许的技术障碍,但我目前手头没有标准......

【问题讨论】:

看起来用可移动对象填充向量的唯一方法是恢复旧样式:std::vector v; v.emplace_back(S(1)); v.emplace_back(S(2)); v.emplace_back(S(3));` (或更短/模糊的v.emplace_back(1); etc ...)。伤心,不是吗? 如果模板类型是MoveInsertable,可以使用push_back。 @JensÅkerblom:我知道;事实上,这就是我的第一个代码 sn-p 显示的内容。不过,有时您想使用内联语法。 类似问题:***.com/questions/8468774/… 【参考方案1】:

也许 8.5.4.5 中的这个条款解释了它(我的重点):

std::initializer_list 类型的对象由 初始化器列表,就好像实现分配了一个 N 数组 E 类型的元素,其中 N 是 初始化列表。 该数组的每个元素都是复制初始化的 与初始化列表的相应元素,以及 构造 std::initializer_list 对象以引用该数组。

因此,如果对象是可复制的,则只能从列表中初始化。


更新:正如 Johannes 所指出的,复制初始化可以通过复制和移动构造函数来实现,因此仅凭这一点还不足以回答这个问题。然而,这里是 18.9 中描述的 initializer_list 类规范的摘录:

  template<class _E>
    class initializer_list
    
    public:
      typedef _E            value_type;
      typedef const _E&     reference;
      typedef const _E&     const_reference;
      typedef size_t        size_type;
      typedef const _E*     iterator;
      typedef const _E*     const_iterator;

注意没有非常量类型定义!

我刚刚尝试创建一个 IL 构造函数,该构造函数将通过 std::make_move_iterator 遍历初始值设定项列表,但由于 const T &amp; 无法转换为 T&amp;&amp; 而失败。

所以答案是:你不能离开 IL,因为标准是这样说的。

【讨论】:

@sehe:这是来自 N3290 FDIS 的引述。我认为这是规范的! :-) 我明白了。这似乎是标准中错过的优化机会,那么......也许它会在下一个版本中修复:) 这个答案让人困惑多于帮助。复制初始化并不意味着“只复制不移动”。复制初始化很可能涉及移动。它只是意味着你初始化就像你已经完成了T t = e;e 可以是一些右值移动到 t @Johannes:我从来没有说过 - 我没有说复制初始化后调用了哪种构造。您有改进的建议吗? @Johannes:那么为什么移动不起作用呢?它只是 libstdc++ 实现,还是标准中禁止它的其他内容?【参考方案2】:

看来这可能是编译器问题。 This works in g++ 4.5.1 (click for IdeOne online demo)

结论:从某种意义上说,旧的 g++ 实现没有正确标记错误;初始化器列表不支持移动它们的元素(元素在过程中被隐式复制)。感谢 Kerrek SB 提供标准中的quoting the helpful phrase。


旧程序(为了理解cmets:)

编辑发现至少g++ 4.6.1+似乎有你对这段代码的抱怨。

编辑 在阅读std::initializer_list&lt;T&gt; 的源代码后,我开始觉得库不支持此功能(看起来是故意的)。标准是否真的允许初始化列表转发它的元素的xvalue-ness......如果他们停在那里我不会感到惊讶(完美的转发仍然是我认为在 C++0x 中不容易支持,并且并非所有初始化器参数都需要具有相同的(可扣除的)类型。

有没有更标准的人来帮忙? http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2008/n2640.pdf

#include <vector>

struct S

    S(int) ;
    S(S&&) ;
;

int main()

    std::vector<S> v = S(1), S(2), S(3);
    std::vector<S> w = std::move(S(1)), std::move(S(2)), std::move(S(3));

    std::vector<S> or_even_just = 1, 2, 3;

【讨论】:

这根本没有帮助(我得到相同的错误),这并不奇怪 - 所有 std::move 所做的就是将左值变成右值。由于 S(1)、S(2) 和 S(3) 已经是右值,因此将它们包装在 std::move 中绝对没有任何作用。 @HighCommander4: 不,std::move 将它们变成rvalue references。不是一回事。让我在本地检查一下。 同时,前往here 获取一些有用的提示/解释 很确定这是一个编译器问题:ideone.com/bYhq9 表明它在 g++ 4.5.1 上运行良好,无论有无 std::move。我建议使用隐式构造 b.t.w. 那是因为 GCC 4.5 没有正确实现移动/复制构造函数的自动生成 - 即使存在用户定义的移动构造函数,它(错误地)为 S 生成一个复制构造函数,并且然后使用它来复制初始化列表对象。给 S 添加一个私有拷贝构造函数,你会看到 GCC 4.5 现在给出了同样的错误。 已编辑结果的答案。谢谢大家指正;关于我每天使用的编译器,我学到了一两件事【参考方案3】:

initializer_list 仅提供 const 引用和 const 迭代器。向量无法从那里移动。

template<class E> 
class initializer_list 
public:
    typedef E value_type;

    typedef const E& reference;
    typedef const E& const_reference;

    typedef size_t size_type;

    typedef const E* iterator;
    typedef const E* const_iterator;

【讨论】:

该语言目前不禁止丢弃 const 然后从对象中移出。但我认为这是一种语言缺陷。【参考方案4】:

看来Kerrek SB's answer 的答案是否定的。但是您可以通过使用可变参数模板的小型辅助函数来实现类似的功能:

#include <vector>
#include <utility>

template <typename T>
void add_to_vector(std::vector<T>* vec) 

template <typename T, typename... Args>
void add_to_vector(std::vector<T>* vec, T&& car, Args&&... cdr) 
  vec->push_back(std::forward<T>(car));
  add_to_vector(vec, std::forward<Args>(cdr)...);


template <typename T, typename... Args>
std::vector<T> make_vector(Args&&... args) 
  std::vector<T> result;
  add_to_vector(&result, std::forward<Args>(args)...);
  return result;


struct S 
  S(int) 
  S(S&&) 
;

int main() 
  std::vector<S> v = make_vector<S>(S(1), S(2), S(3));
  return 0;

【讨论】:

以上是关于Initializer-list-构造一个不可复制(但可移动)对象的向量的主要内容,如果未能解决你的问题,请参考以下文章

通过定义默认私有构造函数使类不可继承

我认为数组是不可复制的

在 C++ 中禁止复制构造函数的最可靠方法是啥?

用指向对象的指针初始化不可复制的第三方基类

访问控制对已删除的构造函数是不是重要?

内部接口 *比内部受保护的构造函数更不可访问?