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

如果我通过 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: : only _ ptr < _ Tp,_ Dp > : : only _ ptr (const std: : only _ ptr < _ Tp,_ Dp > &)[ with _ Tp = int; _ Dp = std: : default _ delete; std: : only _ ptr < _ Tp,_ Dp > = std: : only _ ptr ]’

GCC 试图从初始化器列表中复制指针是否正确?

21686 次浏览

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

具体来说,我们有:

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

编辑: 既然@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))};
}

The iterators returned by std::make_move_iterator will move the pointed-to element when being dereferenced.


原答案: 我们要使用一个小帮手类型:

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

Sadly, the straight-forward code here won't work:

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

Since the standard, for whatever reason, doesn't define a converting copy constructor like this:

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

由 brace-init-list ({...})创建的 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());

正如已经指出的,不可能使用初始化器列表初始化仅移动类型的向量。@ 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;
}
}

正如在其他答案中提到的,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()的技巧,您可以使用一个 helper 函数:

#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所做的那样。

试图给我们其他人一个简单明了的答案。

You can't. It's broken.

Fortunately, array initializers aren't broken.

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]为这一点辩护,提供给成员的纯粹主义祭司。


编程语言应该提高生产力。 在这种情况下,模板元编程没有做到这一点 Want 是一种确保分配的内存不会泄漏的方法 静态初始化到堆中,从而使其不可能 来证明我没有泄露记忆。

这是一个日常使用的案例。而且它不应该是困难的。使它远程复杂只会导致捷径沿着道路。

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

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

我已经为此目的 made a small library

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
}

Unlike the move_iterator approach, this doesn't necessarily move each element. nullptr is emplaced directly into the vector, without constructing an intermediate std::unique_ptr.

这使得它甚至可以与非 可移动的类型一起工作:

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