initializer_list和移动语义
我允许将元素移出std::initializer_list<T>
吗?
#include <initializer_list> #include <utility> template<typename T> void foo(std::initializer_list<T> list) { for (auto it = list.begin(); it != list.end(); ++it) { bar(std::move(*it)); // kosher? } }
由于std::intializer_list<T>
需要特殊的编译器注意,并没有像C ++标准库的普通容器那样的价值语义,所以我宁愿安全,也不要抱歉。
不,这不会按预期工作; 你仍然会得到副本。 我很惊讶,因为我认为initializer_list
存在,以保持临时arrays,直到他们的move
。
begin
和end
initializer_list
返回const T *
,所以在你的代码中move
的结果是T const &&
– 一个不可变的右值引用。 这样的expression不能有意义地从中移走。 它将绑定到types为T const &
的函数参数,因为rvalues绑定到常量左值引用,您仍将看到复制语义。
可能的原因是编译器可以select使initializer_list
成为一个静态初始化的常量,但是由编译器自行决定将其types设置为initializer_list
或const initializer_list
会更清晰,所以用户不知道是否要从begin
和end
期望一个const
或可变结果。 但是,这只是我的直觉,可能有一个很好的理由,我错了。
更新:我已经写了一个ISO提案 initializer_list
支持移动types。 这只是一个初稿,并没有在任何地方执行,但你可以看到它的问题更多的分析。
bar(std::move(*it)); // kosher?
不像你打算的那样。 你不能移动一个const
对象。 而std::initializer_list
只提供对其元素的const
访问。 所以it
的types是const T *
。
你试图调用std::move(*it)
只会导致一个l值。 IE:副本。
std::initializer_list
引用静态内存。 这是class级的目的。 你不能从静态存储器移动 ,因为运动意味着改变它。 你只能从它复制。
这不会像所述的那样工作,因为list.begin()
types是const T *
,并且你不能从一个常量对象中移动。 语言devise者可能是这样做的,以便允许初始化列表包含实例string常量,从中不适合移动。
但是,如果你的情况是你知道初始化列表包含右值expression式(或者你想强制用户写这些expression式),那么有一个技巧可以使它工作(我受到了Sumant的回答这个,但解决scheme比那个更简单)。 您需要存储在初始化程序列表中的元素不是T
值,而是包含T&&
值。 那么即使这些值本身是const
限定的,它们仍然可以检索一个可修改的右值。
template<typename T> class rref_capture { T* ptr; public: rref_capture(T&& x) : ptr(&x) {} operator T&& () const { return std::move(*ptr); } // restitute rvalue ref };
现在,不是声明一个initializer_list<T>
参数,而是声明一个initializer_list<rref_capture<T> >
参数。 这里是一个具体的例子,涉及一个std::unique_ptr<int>
智能指针的向量,对于这个指针只定义了移动语义(所以这些对象本身不能存储在初始化列表中)。 但下面的初始化器列表编译没有问题。
#include <memory> #include <initializer_list> class uptr_vec { typedef std::unique_ptr<int> uptr; // move only type std::vector<uptr> data; public: uptr_vec(uptr_vec&& v) : data(std::move(v.data)) {} uptr_vec(std::initializer_list<rref_capture<uptr> > l) : data(l.begin(),l.end()) {} uptr_vec& operator=(const uptr_vec&) = delete; int operator[] (size_t index) const { return *data[index]; } }; int main() { std::unique_ptr<int> a(new int(3)), b(new int(1)),c(new int(4)); uptr_vec v { std::move(a), std::move(b), std::move(c) }; std::cout << v[0] << "," << v[1] << "," << v[2] << std::endl; }
一个问题需要一个答案:如果初始化列表的元素应该是真正的prvalues(在这个例子中是xvalues),那么这个语言是否确保相应的临时对象的生存期延长到它们被使用的地步? 坦率地说,我认为标准的第8.5节不涉及这个问题。 然而,阅读1.9:10,似乎在所有情况下相关的完整expression式都包含使用初始化列表,所以我认为不存在悬挂右值引用的危险。
目前的标准似乎不允许已经回答 。 这是另一个解决方法,通过将函数定义为可变参数而不是采用初始值列表来实现类似的function。
#include <vector> #include <utility> // begin helper functions 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; } // end helper functions struct S { S(int) {} S(S&&) {} }; void bar(S&& s) {} template <typename T, typename... Args> void foo(Args&&... args) { std::vector<T> args_vec = make_vector<T>(std::forward<Args>(args)...); for (auto& arg : args_vec) { bar(std::move(arg)); } } int main() { foo<S>(S(1), S(2), S(3)); return 0; }
与initializer_list不同,variables模板可以适当地处理r值引用。
在这个示例代码中,我使用了一组小辅助函数将可变参数转换为向量,使其与原始代码类似。 但是,当然你可以直接用variadic模板直接编写recursion函数。
我认为为解决方法提供一个合理的起点可能是有益的。
内嵌评论。
#include <memory> #include <vector> #include <array> #include <type_traits> #include <algorithm> #include <iterator> template<class Array> struct maker; // a maker which makes a std::vector template<class T, class A> struct maker<std::vector<T, A>> { using result_type = std::vector<T, A>; template<class...Ts> auto operator()(Ts&&...ts) const -> result_type { result_type result; result.reserve(sizeof...(Ts)); using expand = int[]; void(expand { 0, (result.push_back(std::forward<Ts>(ts)),0)... }); return result; } }; // a maker which makes std::array template<class T, std::size_t N> struct maker<std::array<T, N>> { using result_type = std::array<T, N>; template<class...Ts> auto operator()(Ts&&...ts) const { return result_type { std::forward<Ts>(ts)... }; } }; // // delegation function which selects the correct maker // template<class Array, class...Ts> auto make(Ts&&...ts) { auto m = maker<Array>(); return m(std::forward<Ts>(ts)...); } // vectors and arrays of non-copyable types using vt = std::vector<std::unique_ptr<int>>; using at = std::array<std::unique_ptr<int>,2>; int main(){ // build an array, using make<> for consistency auto a = make<at>(std::make_unique<int>(10), std::make_unique<int>(20)); // build a vector, using make<> because an initializer_list requires a copyable type auto v = make<vt>(std::make_unique<int>(10), std::make_unique<int>(20)); }
考虑在cpptruths上描述的in<T>
习语。 这个想法是在运行时确定左值/右值,然后调用移动或复制构造。 即使initializer_list提供的标准接口是const引用, in<T>
也会检测右值/左值。