我可以对一个移动语义类型的向量使用列表初始化吗?

122
如果我在GCC 4.7快照中使用以下代码,则它会尝试将unique_ptr复制到向量中。
#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是否正确?


3
Visual Studio和clang有相同的行为。 - Jean-Simon Brochu
8个回答

84

编辑:由于@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返回的迭代器在解引用时会移动指向的元素。

Original answer: 我们将在这里利用一个小助手类型:

#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());

1
啊...这是std::ref的右值引用版本,对吧?也许应该叫做std::rref - Kerrek SB
23
我猜这句话应该不会在评论中被忽略 :) move_only m[] = { move_only(), move_only(), move_only() }; std::vector<move_only> v(std::make_move_iterator(m), std::make_move_iterator(m + 3)); - Johannes Schaub - litb
1
@Johannes:有时候,简单的解决方案就是我无法想到的。虽然我得承认,我还没有尝试过那些move_iterator - Xeo
3
@ Johannes:另外,为什么那不是一个答案? :) 为什么那不是答案? :) - Xeo
1
@JohanLundberg:我认为这是一个质量问题,但我不知道它为什么不能这样做。例如,VC++的stdlib基于迭代器类别进行标签分派,并使用std::distance用于前向或更好的迭代器,而std::move_iterator则适配底层迭代器的类别。无论如何,这是一个很好且简洁的解决方案。你可以将其发布为答案,也许? - Xeo
显示剩余7条评论

55

<initializer_list>的概述在18.9中已经比较清楚地表明,初始化列表的元素总是通过const引用传递。不幸的是,在当前语言版本中似乎没有任何方法可以在初始化列表元素中使用移动语义。

具体地说,我们有:

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

4
考虑在cpptruths上描述的in<T>惯用语(http://cpptruths.blogspot.com/2013/09/21-ways-of-passing-parameters-plus-one.html)。其思想是在运行时确定左值/右值,然后调用移动或复制构造函数。即使初始化列表提供的标准接口为const引用,in<T>也可以检测到rvalue/lvalue。 - Sumant
3
对我来说,这似乎不是很符合惯用语:相反,这不是严格未定义行为吗?因为不仅迭代器本身而且底层元素本身可能是“const”,在一份良好的程序中无法去除其const属性。 - underscore_d

13

正如其他答案中所述,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)


5
可以用虚拟数组逗号运算符技巧来替换递归展开包,以节省几行代码。 - M.M

11
C++20的更新: 使用Johannes Schaub的技巧,结合C++20的std::to_array()和std::make_move_iterator(),你可以使用一个类似于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 一样。


1

这是我最喜欢的解决方案。

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;
}

1
我已经为此目的制作了一个小库

在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 直接插入到向量中,而不构造一个中间的 std::unique_ptr

这使得它即使在非可移动类型中也可以工作:

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

哇,这真的很棒。整天都在苦苦挣扎后,看到事情原来可以如此简单,真是令人耳目一新。你知道有什么原因会阻止这种行为有朝一日成为标准C++吗? - Don Hatch
@DonHatch 我不知道。对我来说,std::initializer_list 看起来设计得很糟糕。 - HolyBlackCat

0

试图为我们其他人提供简明扼要的答案。

你不能。它已经损坏了。

幸运的是,数组初始化器没有损坏。

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>`,则有无数种方法可以实现,其中许多涉及复杂的模板元编程,但所有这些都可以通过for循环避免。
幸运的是,在许多情况下,使用数组而不是std::vector可以起到与您真正想使用std::vector一样的效果。
或者,考虑编写一个自定义的`static_vector`类,它在初始化器列表中接受T*,并在其析构函数中删除它们。这也不是很理想,但您必须接受这样一个事实:`std::vector>`无法在合理的时间内或以合理的精力工作。您可以删除任何执行潜在移动的方法(移动和复制构造函数、T&operator[]()等)。或者,如果必要,使用基本的移动语义进行高级操作(但您可能不需要这样做)。
请参见[1],了解对这一观点的辩护,这是为最纯粹的祭司成员提供的。

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

这是一个日常使用案例。它不应该很难。让它变得复杂只会导致以后出现捷径。


-2
正如已经指出的那样,无法使用初始化器列表初始化移动语义类型的向量。@Johannes 最初提出的解决方案很好,但我有另一个想法……我们不创建临时数组然后将其元素移动到向量中,而是使用放置 new 在向量内存块的位置上初始化该数组怎么样?
这是我的函数,用于使用参数包初始化 unique_ptr 向量:
#include <iostream>
#include <vector>
#include <make_unique.h>  /// @see https://dev59.com/22w05IYBdhLWcg3wxkmr

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;
    }
}

那是一个糟糕的想法。Placement new 不是一把锤子,而是一种精密工具。result.data() 不是指向某个随机内存的指针,它是指向一个对象的指针。当你在它上面进行 placement new 操作时,请考虑一下那个可怜的对象会发生什么。 - R. Martinho Fernandes
1
此外,放置新的数组形式并不是真正可用的。https://dev59.com/lWoy5IYBdhLWcg3wL7H1 - R. Martinho Fernandes
@R. Martinho Fernandes:感谢您指出数组的placement-new不起作用。现在我明白为什么那是个坏主意了。 - Gart

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接