我是否允许将元素移出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 ++标准库的普通容器那样的值语义,所以我宁愿安全而不是抱歉并且问。
答案 0 :(得分:75)
不,这不会按预期工作;你仍然会得到副本。我对此感到非常惊讶,因为我认为initializer_list
存在一系列临时工,直到他们move
为止。
begin
的{{1}}和end
返回initializer_list
,因此代码中const T *
的结果为move
- 不可变的右值参考。这样的表达无法有意义地被移除。它将绑定到类型T const &&
的函数参数,因为rvalues会绑定到const lvalue引用,您仍然会看到复制语义。
原因可能是编译器可以选择使T const &
成为静态初始化常量,但似乎使其类型initializer_list
或initializer_list
更清晰由编译器自行决定,因此用户不知道是否期望来自const initializer_list
和const
的{{1}}或可变结果。但这只是我的直觉,可能是我错了。
更新:我为begin
仅移动类型的支持撰写了an ISO proposal。它只是初稿,并没有在任何地方实现,但你可以看到它来更多地分析问题。
答案 1 :(得分:17)
bar(std::move(*it)); // kosher?
不是你想要的方式。您无法移动const
对象。并且std::initializer_list
仅提供对其元素的const
访问权限。因此it
的类型为const T *
。
您尝试拨打std::move(*it)
只会产生一个l值。 IE:副本。
std::initializer_list
引用静态内存。这就是班级的用途。你无法从静态内存移动,因为移动意味着改变它。你只能从中复制。
答案 2 :(得分:2)
这不符合规定,因为list.begin()
的类型为const T *
,并且您无法从常量对象移动。语言设计者可能这样做是为了允许初始化列表包含例如字符串常量,从中移动它是不合适的。
但是,如果您处于这样一种情况,即您知道初始化列表包含右值表达式(或者您希望强制用户编写这些表达式),那么有一个技巧可以使其工作(我受到了答案的启发由Sumant为此,但解决方案比那个更简单)。您需要存储在初始化列表中的元素不是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,似乎相关的完整表达式在所有情况下都包含初始化列表的使用,所以我认为没有悬挂右值引用的危险。
答案 3 :(得分:1)
我认为为解决方法提供合理的起点可能是有益的。
评论内联。
#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));
}
答案 4 :(得分:0)
目前的标准似乎不允许already answered。通过将函数定义为可变参数而不是获取初始化列表,这是另一种实现类似功能的解决方法。
#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;
}
可变参数模板可以适当地处理r值引用,与initializer_list不同。
在这个示例代码中,我使用了一组小辅助函数将可变参数转换为向量,使其类似于原始代码。但是当然你可以直接用可变参数模板编写一个递归函数。
答案 5 :(得分:0)
我有一个更简单的实现,它利用包装类来充当标记以标记移动元素的意图。这是编译时的成本。
设计包装器类的方式是使用std::move
的方式,只需将std::move
替换为move_wrapper
,但这需要C ++ 17。对于较早的规格,可以使用其他构建器方法。
您需要编写在initializer_list
内接受包装器类的构建器方法/构造器,并相应地移动元素。
如果您需要复制而不是移动某些元素,请在将其传递到initializer_list
之前构造一个副本。
该代码应自记录。
#include <iostream>
#include <vector>
#include <initializer_list>
using namespace std;
template <typename T>
struct move_wrapper {
T && t;
move_wrapper(T && t) : t(move(t)) { // since it's just a wrapper for rvalues
}
explicit move_wrapper(T & t) : t(move(t)) { // acts as std::move
}
};
struct Foo {
int x;
Foo(int x) : x(x) {
cout << "Foo(" << x << ")\n";
}
Foo(Foo const & other) : x(other.x) {
cout << "copy Foo(" << x << ")\n";
}
Foo(Foo && other) : x(other.x) {
cout << "move Foo(" << x << ")\n";
}
};
template <typename T>
struct Vec {
vector<T> v;
Vec(initializer_list<T> il) : v(il) {
}
Vec(initializer_list<move_wrapper<T>> il) {
v.reserve(il.size());
for (move_wrapper<T> const & w : il) {
v.emplace_back(move(w.t));
}
}
};
int main() {
Foo x{1}; // Foo(1)
Foo y{2}; // Foo(2)
Vec<Foo> v{Foo{3}, move_wrapper(x), Foo{y}}; // I want y to be copied
// Foo(3)
// copy Foo(2)
// move Foo(3)
// move Foo(1)
// move Foo(2)
}
答案 6 :(得分:0)
您可以将参数声明为数组右值引用,而不是使用std::initializer_list<T>
:
template <typename T>
void bar(T &&value);
template <typename T, size_t N>
void foo(T (&&list)[N] ) {
std::for_each(std::make_move_iterator(std::begin(list)),
std::make_move_iterator(std::end(list)),
&bar);
}
void baz() {
foo({std::make_unique<int>(0), std::make_unique<int>(1)});
}
请参见使用std::unique_ptr<int>
的示例:https://gcc.godbolt.org/z/2uNxv6
答案 7 :(得分:-1)
考虑cpptruths中描述的in<T>
习语。我们的想法是在运行时确定左值/右值,然后调用move或copy-construction。 in<T>
将检测rvalue /左值,即使initializer_list提供的标准接口是const引用。