Initializer_list 和 move 语义

我可以从 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++标准程式库容器那样的值语义,所以我宁愿安全一点,也不要抱歉地问。

25328 次浏览

不,那样不行,你还是会得到副本的。我对此感到非常惊讶,因为我以为 initializer_list的存在是为了保持一个临时数组,直到它们被 move化。

beginend对于 initializer_list返回 const T *,因此代码中 move的结果是 T const &&ーー一个不可变的 rvalue 引用。这样的表达不能被有意义地从。它将绑定到类型为 T const &的函数参数,因为 rvalue 确实绑定到 const lvalue 引用,而且您仍将看到复制语义。

这可能是因为编译器可以选择使 initializer_list成为一个静态初始化的常量,但是由编译器自行决定使它的类型 initializer_list或者 const initializer_list似乎更简单,所以用户不知道是否期望从 beginend得到一个 const或者可变的结果。但那只是我的直觉,也许我错了是有原因的。

更新: 我为 initializer_list编写了 国际标准化组织的建议,支持仅移动类型。这只是第一个草案,还没有在任何地方实现,但是您可以看到它,以便对问题进行更多的分析。

bar(std::move(*it));   // kosher?

不是你想的那样。不能移动 const对象。而 std::initializer_list只提供对其元素的 const访问。所以 it的类型是 const T *

您尝试调用 std::move(*it)将只会导致一个 l 值。

std::initializer_list引用 静电干扰内存。这就是上课的目的。你不能从静态记忆 让开,因为运动意味着改变它。你只能从中复制。

Consider the in<T> idiom described on CppTruth. 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.

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.

但是,如果你知道初始化器列表包含 rvalue 表达式(或者你想强迫用户写这些表达式) ,那么有一个技巧可以让它工作(我受到了 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>智能指针的向量,它只定义了 move 语义(因此这些对象本身永远不能存储在初始化器列表中) ; 然而下面的初始化器列表编译没有问题。

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

有一个问题需要回答: 如果初始化器列表的元素应该是真正的 prvalue (在示例中它们是 xvalue) ,那么该语言是否确保相应临时变量的生命周期延长到使用它们的时间点?坦率地说,我认为标准的相关章节8.5根本没有解决这个问题。然而,阅读1.9:10,似乎在所有情况下相关的 full-expression都包含了初始化器列表的使用,因此我认为不存在悬空右值引用的危险。

它似乎不允许在目前的标准作为 已经回答了。下面是另一个实现类似功能的解决方案,它将函数定义为可变的,而不是采用初始化器列表。

#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 不同,变量模板可以适当地处理 r 值引用。

在这个示例代码中,我使用了一组小的 helper 函数来将可变参数转换为向量,以使其与原始代码相似。当然,您可以直接使用可变模板编写一个递归函数。

我认为提供一个合理的起点来解决这个问题可能是有益的。

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

我有一个更简单的实现,它使用一个包装器类作为标记来标记移动元素的意图。这是编译时成本。

The wrapper class is designed to be used in the way std::move is used, just replace std::move with move_wrapper, but this requires C++17. For older specs, you can use an additional builder method.

You'll need to write builder methods/constructors that accept wrapper classes inside initializer_list and move the elements accordingly.

如果需要复制而不是移动某些元素,请在将其传递给 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)
}

可以不使用 std::initializer_list<T>,而是将参数声明为数组 rvalue 引用:

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的示例

这是 const_cast很好用的情况之一

Sum::Sum(std::initializer_list<Valuable>&& l)
{
for (auto& a : l)
{
auto&& arg = std::move(const_cast<Valuable&>(a));
Add(std::move(arg));
}
}