我可以列出初始化一个仅移动类型的向量吗?

Can I list-initialize a vector of move-only type?

本文关键字:类型 移动 向量 一个 初始化 我可以      更新时间:2023-10-16

如果我通过GCC 4.7快照传递以下代码,它会尝试将unique_ptr s复制到向量中。

#include <vector>
#include <memory>
int main() {
    using move_only = std::unique_ptr<int>;
    std::vector<move_only> v { move_only(), move_only(), move_only() };
}

显然不能工作,因为std::unique_ptr是不可复制的:

std::unique_ptr<_Tp, _Dp>::unique_ptr(const std::unique_ptr<_Tp, _Dp>&) [with _Tp = int;_Dp = std::default_delete;std:: unique_ptr<_Tp _Dp> = std:: unique_ptr]

GCC在尝试从初始化列表中复制指针是正确的吗?

编辑:既然@Johannes似乎不想发布最佳解决方案作为答案,我就这么做了。

#include <iterator>
#include <vector>
#include <memory>
int main(){
  using move_only = std::unique_ptr<int>;
  move_only init[] = { move_only(), move_only(), move_only() };
  std::vector<move_only> v{std::make_move_iterator(std::begin(init)),
      std::make_move_iterator(std::end(init))};
}

std::make_move_iterator返回的迭代器将在解引用时移动指向元素。


原始答:我们将在这里使用一个小helper类型:

#include <utility>
#include <type_traits>
template<class T>
struct rref_wrapper
{ // CAUTION - very volatile, use with care
  explicit rref_wrapper(T&& v)
    : _val(std::move(v)) {}
  explicit operator T() const{
    return T{ std::move(_val) };
  }
private:
  T&& _val;
};
// only usable on temporaries
template<class T>
typename std::enable_if<
  !std::is_lvalue_reference<T>::value,
  rref_wrapper<T>
>::type rref(T&& v){
  return rref_wrapper<T>(std::move(v));
}
// lvalue reference can go away
template<class T>
void rref(T&) = delete;

遗憾的是,这里的直接代码不起作用:

std::vector<move_only> v{ rref(move_only()), rref(move_only()), rref(move_only()) };

由于某种原因,标准没有定义这样的转换复制构造函数:

// in class initializer_list
template<class U>
initializer_list(initializer_list<U> const& other);

由括号初始化列表({...})创建的initializer_list<rref_wrapper<move_only>>不会转换为vector<move_only>所使用的initializer_list<move_only>。这里我们需要两步初始化:

std::initializer_list<rref_wrapper<move_only>> il{ rref(move_only()),
                                                   rref(move_only()),
                                                   rref(move_only()) };
std::vector<move_only> v(il.begin(), il.end());

18.9中<initializer_list>的概要相当清楚地表明,初始化列表的元素总是通过const-reference传递。不幸的是,在当前版本的语言中,似乎没有任何方法可以在初始化列表元素中使用move-semantic。

具体来说,我们有:

typedef const E& reference;
typedef const E& const_reference;
typedef const E* iterator;
typedef const E* const_iterator;
const E* begin() const noexcept; // first element
const E* end() const noexcept; // one past the last element

如其他答案所述,std::initializer_list的行为是按值保存对象,不允许移出,所以这是不可能的。这里有一个可能的解决方案,使用函数调用,其中初始化式作为可变参数给出:

#include <vector>
#include <memory>
struct Foo
{
    std::unique_ptr<int> u;
    int x;
    Foo(int x = 0): x(x) {}
};
template<typename V>        // recursion-ender
void multi_emplace(std::vector<V> &vec) {}
template<typename V, typename T1, typename... Types>
void multi_emplace(std::vector<V> &vec, T1&& t1, Types&&... args)
{
    vec.emplace_back( std::move(t1) );
    multi_emplace(vec, args...);
}
int main()
{
    std::vector<Foo> foos;
    multi_emplace(foos, 1, 2, 3, 4, 5);
    multi_emplace(foos, Foo{}, Foo{});
}

不幸的是,multi_emplace(foos, {});失败了,因为它不能推断出{}的类型,所以对于默认构造的对象,您必须重复类名。(或使用vector::resize)

更新c++ 20 :使用Johannes Schaub的std::make_move_iterator()技巧和c++ 20的std::to_array(),您可以使用类似于make_tuple()等的辅助函数,这里称为make_vector():

#include <array>
#include <memory>
#include <vector>
struct X {};
template<class T, std::size_t N>
auto make_vector( std::array<T,N>&& a )
    -> std::vector<T>
{
    return { std::make_move_iterator(std::begin(a)),
             std::make_move_iterator(std::end(a)) };
}
template<class... T>
auto make_vector( T&& ... t )
{
    return make_vector( std::to_array({ std::forward<T>(t)... }) );
}
int main()
{
    using UX = std::unique_ptr<X>;
    const auto a  = std::to_array({ UX{}, UX{}, UX{} });     // Ok
    const auto v0 = make_vector( UX{}, UX{}, UX{} );         // Ok
    //const auto v2 = std::vector< UX >{ UX{}, UX{}, UX{} }; // !! Error !!
}

观看现场直播Godbolt


旧c++的类似答案:

使用Johannes Schaub的std::make_move_iterator()std::experimental::make_array()的技巧,您可以使用辅助函数:

#include <memory>
#include <type_traits>
#include <vector>
#include <experimental/array>
struct X {};
template<class T, std::size_t N>
auto make_vector( std::array<T,N>&& a )
    -> std::vector<T>
{
    return { std::make_move_iterator(std::begin(a)), std::make_move_iterator(std::end(a)) };
}
template<class... T>
auto make_vector( T&& ... t )
    -> std::vector<typename std::common_type<T...>::type>
{
    return make_vector( std::experimental::make_array( std::forward<T>(t)... ) );
}
int main()
{
    using UX = std::unique_ptr<X>;
    const auto a  = std::experimental::make_array( UX{}, UX{}, UX{} ); // Ok
    const auto v0 = make_vector( UX{}, UX{}, UX{} );                   // Ok
    //const auto v1 = std::vector< UX >{ UX{}, UX{}, UX{} };           // !! Error !!
}

Coliru上观看。

也许有人可以利用std::make_array()的诡计来允许make_vector()直接做它的事情,但我没有看到如何(更准确地说,我尝试了我认为应该工作的,失败了,然后继续前进)。在任何情况下,编译器都应该能够将数组内联到向量转换,就像Clang在GodBolt上对O2所做的那样。

这是我最喜欢的解决方案

c++ 17版本

#include <vector>
#include <memory>
template <typename T, typename ...Args>
std::vector<T> BuildVectorFromMoveOnlyObjects(Args&&... args) {
  std::vector<T> container;
  container.reserve(sizeof...(Args));
  ((container.emplace_back(std::forward<Args>(args))), ...);
  return container;
}

int main() {
  auto vec = BuildVectorFromMoveOnlyObjects<std::unique_ptr<int>>(
      std::make_unique<int>(10),
      std::make_unique<int>(50));
}

更丑的c++ 11版本

template <typename T, typename ...Args>
std::vector<T> BuildVectorFromMoveOnlyObjects(Args&&... args) {
  std::vector<T> container;
    using expander = int[];
    (void)expander{0, (void(container.emplace_back(std::forward<Args>(args))), 0)... };
  return container;
}

我为此制作了一个小库。

run on gcc.godbolt.org

#include <better_braces.hpp>
#include <iostream>
#include <memory>
#include <vector>
int main()
{
    std::vector<std::unique_ptr<int>> foo = init{nullptr, std::make_unique<int>(42)};
    std::cout << foo.at(0) << 'n'; // 0
    std::cout << foo.at(1) << " -> " << *foo.at(1) << 'n'; // 0x602000000010 -> 42
}

move_iterator方法不同,这并不需要移动每个元素。直接将nullptr放置到vector中,不构造中间的std::unique_ptr

这允许它在非可移动类型下工作:

std::vector<std::atomic_int> bar = init{1, 2, 3};

试图给我们其余的人一个简单扼要的答案。

你不能。它坏了。

幸运的是,数组初始化式没有被破坏。

static std::unique_ptr<SerializerBase> X::x_serializers[] = { 
    std::unique_ptr<SerializerBase>{
        new Serializer<X,int>("m_int",&X::m_int)
    },
    std::unique_ptr<SerializerBase>{
        new Serializer<X,double>("m_double",&X::m_double)
    },
  nullptr, // lol. template solutions from hell possible here too.
};

如果你想用这个数组来初始化一个std::vector<std::unique_ptr<T>>,有无数种方法可以做到这一点,其中许多都涉及到令人不快的模板元编程,所有这些都可以通过for循环来避免。

幸运的是,在很多情况下,使用数组而不是std::vector可以工作,而这些情况实际上是您更愿意使用std::vector的。

或者,考虑编写一个custom::static_vector<T>类,它在初始化列表中接受T*,并在析构函数中删除它们。也不高兴,但你需要接受这样一个事实,即std::vector<std::unique_ptr<T>>不会在合理的时间或合理的努力下工作。您可以删除任何执行潜在移动的方法(移动和复制构造函数,T&operator[]() &c)。如果你需要的话,也可以更花哨一点,实现基本的move语义(但你可能不会这么做)。

参见[1]对此的辩护,这是为纯粹派祭司提供的。


[1]编程语言应该提高生产力。在这种情况下,模板元编程没有做到这一点。我所有的我想要的是一种方法,以确保我不泄漏分配的内存静态初始化到堆中,因此不可能使用valgrind来验证我没有泄漏内存。

这是一个日常用例。这应该不难。把它弄得过于复杂只会让你走捷径。

前面已经指出,不能用初始化列表初始化只移动类型的向量。@Johannes最初提出的解决方案很好,但我有另一个想法…如果我们不创建一个临时数组,然后将元素从那里移动到向量中,而是使用放置new来初始化这个数组,该数组已经取代了向量的内存块,该怎么办?

下面是我使用参数包初始化一个unique_ptr的向量的函数:

#include <iostream>
#include <vector>
#include <make_unique.h>  /// @see http://stackoverflow.com/questions/7038357/make-unique-and-perfect-forwarding
template <typename T, typename... Items>
inline std::vector<std::unique_ptr<T>> make_vector_of_unique(Items&&... items) {
    typedef std::unique_ptr<T> value_type;
    // Allocate memory for all items
    std::vector<value_type> result(sizeof...(Items));
    // Initialize the array in place of allocated memory
    new (result.data()) value_type[sizeof...(Items)] {
        make_unique<typename std::remove_reference<Items>::type>(std::forward<Items>(items))...
    };
    return result;
}
int main(int, char**)
{
    auto testVector = make_vector_of_unique<int>(1,2,3);
    for (auto const &item : testVector) {
        std::cout << *item << std::endl;
    }
}