可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
Am I allowed to move elements out of a 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?
}
}
Since std::intializer_list<T>
requires special compiler attention and does not have value semantics like normal containers of the C++ standard library, I\'d rather be safe than sorry and ask.
回答1:
No, that won\'t work as intended; you will still get copies. I\'m pretty surprised by this, as I\'d thought that initializer_list
existed to keep an array of temporaries until they were move
\'d.
begin
and end
for initializer_list
return const T *
, so the result of move
in your code is T const &&
— an immutable rvalue reference. Such an expression can\'t meaningfully be moved from. It will bind to an function parameter of type T const &
because rvalues do bind to const lvalue references, and you will still see copy semantics.
Probably the reason for this is so the compiler can elect to make the initializer_list
a statically-initialized constant, but it seems it would be cleaner to make its type initializer_list
or const initializer_list
at the compiler\'s discretion, so the user doesn\'t know whether to expect a const
or mutable result from begin
and end
. But that\'s just my gut feeling, probably there\'s a good reason I\'m wrong.
Update: I\'ve written an ISO proposal for initializer_list
support of move-only types. It\'s only a first draft, and it\'s not implemented anywhere yet, but you can see it for more analysis of the problem.
回答2:
bar(std::move(*it)); // kosher?
Not in the way that you intend. You cannot move a const
object. And std::initializer_list
only provides const
access to its elements. So the type of it
is const T *
.
Your attempt to call std::move(*it)
will only result in an l-value. IE: a copy.
std::initializer_list
references static memory. That\'s what the class is for. You cannot move from static memory, because movement implies changing it. You can only copy from it.
回答3:
This won\'t work as stated, because list.begin()
has type const T *
, and there is no way you can move from a constant object. The language designers probably made that so in order to allow initializer lists to contain for instance string constants, from which it would be inappropriate to move.
However, if you are in a situation where you know that the initializer list contains rvalue expressions (or you want to force the user to write those) then there is a trick that will make it work (I was inspired by the answer by Sumant for this, but the solution is way simpler than that one). You need the elements stored in the initialiser list to be not T
values, but values that encapsulate T&&
. Then even if those values themselves are const
qualified, they can still retrieve a modifiable rvalue.
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
};
Now instead of declaring an initializer_list<T>
argument, you declare aninitializer_list<rref_capture<T> >
argument. Here is a concrete example, involving a vector of std::unique_ptr<int>
smart pointers, for which only move semantics is defined (so these objects themselves can never be stored in an initializer list); yet the initializer list below compiles without problem.
#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;
}
One question does need an answer: if the elements of the initializer list should be true prvalues (in the example they are xvalues), does the language ensure that the lifetime of the corresponding temporaries extends to the point where they are used? Frankly, I don\'t think the relevant section 8.5 of the standard addresses this issue at all. However, reading 1.9:10, it would seem that the relevant full-expression in all cases encompasses the use of the initializer list, so I think there is no danger of dangling rvalue references.
回答4:
I thought it might be instructive to offer a reasonable starting point for a workaround.
Comments inline.
#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));
}
回答5:
It seems not allowed in the current standard as already answered. Here is another workaround to achieve something similar, by defining the function as variadic instead of taking an initializer list.
#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;
}
Variadic templates can handle r-value references appropriately, unlike initializer_list.
In this example code, I used a set of small helper functions to convert the variadic arguments into a vector, to make it similar to the original code. But of course you can write a recursive function with variadic templates directly instead.
回答6:
Consider the in<T>
idiom described on cpptruths. The idea is to determine lvalue/rvalue at run-time and then call move or copy-construction. in<T>
will detect rvalue/lvalue even though the standard interface provided by initializer_list is const reference.