111

我可以将元素移出 astd::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++ 标准库的普通容器那样的值语义,我宁愿安全也不愿道歉和问。

4

8 回答 8

108

不,这不会按预期工作;你仍然会得到副本。我对此感到非常惊讶,因为我认为它的initializer_list存在是为了保留一系列临时人员,直到他们被move淘汰。

beginend对于initializer_listreturn ,您的代码中const T *的结果是— 一个不可变的右值引用。这样的表达不能有意义地移开。它将绑定到类型的函数参数,因为右值确实绑定到 const 左值引用,并且您仍然会看到复制语义。moveT const &&T const &

可能的原因是编译器可以选择制作initializer_list一个静态初始化的常量,但似乎制作它的类型initializer_listconst initializer_list由编译器自行决定会更干净,所以用户不知道是期望 aconst还是可变的结果来自beginend。但这只是我的直觉,可能有一个很好的理由我错了。

更新:我写了一个支持移动类型的 ISO 提案。initializer_list这只是初稿,还没有在任何地方实施,但您可以查看它以对问题进行更多分析。

于 2011-11-19T09:38:59.053 回答
23
bar(std::move(*it));   // kosher?

不是你想要的方式。您不能移动const对象。并且std::initializer_list只提供const对其元素的访问。所以 的类型itconst T *

您尝试调用std::move(*it)只会产生左值。IE:副本。

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

于 2011-11-19T16:29:19.897 回答
3

这不会像所说的那样工作,因为list.begin()它具有 type 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;
}

一个问题确实需要回答:如果初始化列表的元素应该是真正的纯右值(在示例中它们是 xvalues),那么该语言是否确保相应临时对象的生命周期延长到使用它们的点?坦率地说,我认为标准的相关第 8.5 节根本没有解决这个问题。但是,阅读 1.9:10,似乎所有情况下的相关完整表达式都包含初始化列表的使用,所以我认为没有悬挂右值引用的危险。

于 2014-07-07T11:32:50.293 回答
2

我认为为解决方法提供一个合理的起点可能是有益的。

内联评论。

#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));
}
于 2017-06-01T12:36:28.427 回答
1

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

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

于 2020-06-02T11:42:06.973 回答
0

正如已经回答的那样,目前的标准似乎不允许。这是实现类似功能的另一种解决方法,通过将函数定义为可变参数而不是采用初始化列表。

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

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

于 2017-03-26T04:47:18.290 回答
0

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

包装类被设计成按使用方式std::move使用,只需替换std::movemove_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)
}
于 2019-07-10T17:07:34.783 回答
-1

考虑cpptruthsin<T>上描述的成语。这个想法是在运行时确定左值/右值,然后调用移动或复制构造。即使 initializer_list 提供的标准接口是 const 引用,也会检测 rvalue/lvalue。in<T>

于 2013-09-18T18:27:21.547 回答