initializer_list和移动语义

124

我可以将元素从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++标准库中普通容器具有值语义,所以我宁愿谨慎一些并进行询问。


1
核心语言定义了 initializer_list<T> 引用的对象是const的,比如 initializer_list<int> 引用的是 int 对象。但我认为这是一个缺陷 - 它原本是打算让编译器在只读内存中静态分配列表。 - Johannes Schaub - litb
9个回答

121
不,那样做的效果并不如预期;你仍然会得到副本。我感到非常惊讶,因为我认为initializer_list存在的目的是保持临时数组,直到它们被移动。
initializer_list的begin和end返回const T *,所以你代码中move的结果是T const &&- 一个不可变的右值引用。这样的表达式不能有意义地移动。它将绑定到类型为T const &的函数参数,因为rvalue确实绑定到const lvalue引用,并且您仍将看到复制语义。
可能的原因是编译器可以选择将initializer_list成为静态初始化的常量,但似乎更清晰的做法是在编译器自行决定时将其类型设置为initializer_list或const initializer_list,以便用户不知道是否应该从begin和end期望const或可变的结果。但这只是我的直觉,可能我错了有充分的理由。
更新:我已经为initializer_list支持仅移动类型写了an ISO proposal。这只是一个初稿,还没有在任何地方实现,但你可以看到更多关于这个问题的分析。

11
如果不太清楚的话,这句话的意思是,使用 std::move 是安全的,只是可能没有提高效率(除了 T const&& 的移动构造函数)。 - Luc Danton
1
@David:说得好,但即使需要一个非引用重载,拥有一个std::initializer_list &&重载仍然很有用。我想这可能会比当前糟糕的情况更加混乱。 - Potatoswatter
1
@JBJansen 这是无法绕过的。我不确定这段代码在initializer_list方面的目的,但作为用户,您没有足够的权限来移动它。安全的代码不会这样做。 - Potatoswatter
2
@Potatoswatter,虽然晚了点,但是这个提案的状态如何?它有没有可能被纳入C++20标准中? - WhiZTiM
1
这个提案有任何进展吗?我也很惊讶初始化列表会强制复制。 - Michaël
显示剩余8条评论

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

你不能按照你的意愿移动一个const对象。而且std::initializer_list只提供对其元素的const访问。因此,it的类型是const T *。
你尝试调用std::move(*it)只会导致一个左值。即:一份副本。
std::initializer_list引用静态内存。这就是该类的作用。你不能从静态内存中移动,因为移动意味着改变它。你只能从中复制。

一个const xvalue仍然是xvalue,如果需要,initializer_list引用堆栈。(如果内容不是常量,它仍然是线程安全的。) - Potatoswatter
6
@Potatoswatter说:常量对象是无法移动的。initializer_list对象本身可能是一个右值引用,但是它所指向的内容(实际数值数组)是const的,因为这些内容可能是静态值。你不能从initializer_list的内容中进行移动操作。 - Nicol Bolas
看我的回答和它的讨论。他移动了解引用的迭代器,生成了一个constxvalue。 move可能是没有意义的,但这是合法的,甚至可以声明一个接受该值的参数。如果移动特定类型恰好是无操作,那么它甚至可能正常工作。 - Potatoswatter
1
@Potatoswatter:C++11标准在很多语言方面都花了很多力气,以确保非临时对象没有被移动,除非你使用 std::move。这确保了您可以通过检查确定何时发生移动操作,因为它会影响源和目标(您不希望对命名对象隐式发生)。因此,如果您在移动操作发生的地方使用std::move(如果您有一个const xvalue,则不会发生任何实际移动),则代码是具有误导性的。我认为在const 对象上调用 std::move 是一个错误。 - Nicol Bolas
1
也许吧,但我仍然更愿意少一些违反规则的异常,而不是可能会误导代码。无论如何,这正是为什么我回答“否”,即使它是合法的,结果仍然是一个xvalue,即使它只绑定为const lvalue。老实说,在具有受管理指针的垃圾收集类中,我已经与const &&有过短暂的恋情,其中所有相关内容都是可变的,并且移动会移动指针管理,但不会影响包含的值。总是有棘手的边缘情况 :v)。 - Potatoswatter
显示剩余3条评论

4
这个不会像陈述的那样起作用,因为list.begin()有类型const T *,而你没有办法从一个常量对象中移动。语言设计师可能是这样做的,以便允许初始化列表包含例如字符串常量,从中移动将是不合适的。
然而,如果您处于这样一种情况,在其中您知道初始化列表包含rvalue表达式(或者您想强制用户编写这些表达式),那么有一个技巧可以使其起作用(我受到了Sumant的答案的启发,但解决方案比那个简单得多)。你需要存储在初始化器列表中的元素不是T值,而是封装了T&&的值。即使这些值本身是const限定的,它们仍然可以检索可修改的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
};

现在,不再声明一个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后,似乎相关的完整表达式在所有情况下都包括使用初始化列表,因此我认为不存在悬空rvalue引用的危险。

字符串常量?像“Hello world”这样的?如果你从它们移动,你只需复制一个指针(或绑定一个引用)。 - dyp
1
“有一个问题需要回答。”{..}内的初始化程序绑定到rref_capture函数参数中的引用。这并不延长它们的生命周期,它们仍然会在创建它们的完整表达式结束时被销毁。 - dyp
根据T.C.在另一个答案中的评论:如果您有多个构造函数重载,请使用某些转换特性(例如std::decay_t)将std::initializer_list<rref_capture<T>>包装起来,以阻止不必要的推导。 - Kuba hasn't forgotten Monica

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

问题是是否可以移动initializer_list,而不是是否有人有解决方法。此外,initializer_list的主要卖点是它仅在元素类型上进行模板化,而不是元素数量,因此不需要接收方也进行模板化 - 这完全失去了这一点。 - underscore_d
2
@underscore_d,你说得完全正确。我认为分享与问题相关的知识本身就是一件好事。在这种情况下,也许它帮助了提问者,也许没有——他没有回应。然而,通常情况下,提问者和其他人都欢迎与问题相关的额外材料。 - Richard Hodges
当然,对于那些想要像initializer_list一样的东西但不受使其有用的所有限制约束的读者来说,这确实可能会有所帮助。 :) - underscore_d
@underscore_d 我忽略了哪些约束条件? - Richard Hodges
我的意思只是,通过编译器的魔法,initializer_list 避免了在元素数量上为模板函数进行模板化。这是基于数组和/或变参函数的替代方案所固有的要求,因此限制了后者可用的范围。据我所知,这正是拥有 initializer_list 的主要原理之一,因此值得一提。 - underscore_d

1

不使用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


遗憾的是,这似乎破坏了A{a, b, c}语法,需要改为使用A({a, b, c})才能正常工作。 - axerologementy

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,而不是是否有人有解决方法。此外,initializer_list的主要卖点是它仅在元素类型上进行模板化,而不是元素数量,因此不需要接收方也进行模板化 - 这完全失去了这一点。 - underscore_d

0

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

这个包装类的设计方式和使用方法类似于 std::move,只需将 std::move 替换为 move_wrapper 即可,但这需要 C++17。对于旧版本的规范,您可以使用额外的构建器方法。

您需要编写接受包装类的构建器方法/构造函数,并相应地移动元素。

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

0

这是其中一个情况,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));
    }
}

-1
考虑在 cpptruths 上描述的 in<T> 习惯用法。其思路是在运行时确定左值或右值,然后调用移动或复制构造函数。in<T> 即使初始化列表提供的标准接口是 const 引用,也可以检测到 rvalue/lvalue。

4
编译器已经知道值的类别,为什么你还想在运行时确定它呢? - fredoverflow
1
请阅读博客,并在您不同意或有更好的替代方案时给我留言。即使编译器知道值的类别,initializer_list也不能保留它,因为它只有const迭代器。因此,在构造initializer_list并传递它时,您需要“捕获”值的类别,以便函数可以随心所欲地使用它。 - Sumant
6
没有跟随链接,这个答案基本上是无用的。SO的答案应该在不跟随链接的情况下有用。 - Yakk - Adam Nevraumont
1
@Sumant [从另一个相同的帖子中复制我的评论] 那个庞大的混乱实际上是否提供了任何可衡量的性能或内存使用方面的好处,如果有的话,这些好处足够大以充分抵消它看起来多么糟糕以及需要大约一个小时才能弄清楚它试图做什么?我有点怀疑。 - underscore_d

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接