initializer_list和移动语义

initializer_list and move semantics

提问人:fredoverflow 提问时间:11/19/2011 更新时间:7/10/2022 访问量:28333

问:

我是否可以将元素移出?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?
    }
}

由于需要特殊的编译器注意并且没有像 C++ 标准库的普通容器那样的值语义,我宁愿安全也不愿抱歉和询问。std::intializer_list<T>

C++ 模板 C++11 移动语义初始 值设定项列表

评论

1赞 Johannes Schaub - litb 11/19/2011
核心语言定义 an 引用的对象是非常量的。喜欢,指对象。但我认为这是一个缺陷 - 编译器可以在只读内存中静态分配列表。initializer_list<T>initializer_list<int>int

答:

121赞 Potatoswatter 11/19/2011 #1

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

begin和 for return ,所以在代码中的结果是 — 一个不可变的右值引用。这样的表达方式不能有意义地移动。它将绑定到类型的函数参数,因为 rvalues 确实绑定到 const 左值引用,并且您仍然会看到复制语义。endinitializer_listconst T *moveT const &&T const &

这样做的原因可能是编译器可以选择将静态初始化的常量设为常量,但似乎将其类型设为类型或由编译器自行决定会更干净,因此用户不知道是否期望 和 的结果或可变结果。但这只是我的直觉,也许我错了是有充分理由的。initializer_listinitializer_listconst initializer_listconstbeginend

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

评论

11赞 Luc Danton 11/19/2011
如果不清楚,这仍然意味着使用是安全的,即使不是有效的。(禁止移动构造函数。std::moveT const&&
1赞 Potatoswatter 11/19/2011
@David:说得好,但是让重载做一些事情仍然很有用,即使还需要非引用重载。我想这会比目前的情况更令人困惑,这已经很糟糕了。std::initializer_list &&
1赞 Potatoswatter 3/10/2014
@JBJansen 它不能被黑客入侵。我不明白该代码应该initializer_list完成什么,但作为用户,您没有从中移动所需的权限。安全代码不会这样做。
2赞 WhiZTiM 7/19/2017
@Potatoswatter,迟到的评论,但提案的状态如何。它是否有可能进入 C++20?
1赞 Michaël 2/26/2021
这项建议是否取得了任何进展?我也相当惊讶初始化项列出了强制副本。
26赞 Nicol Bolas 11/20/2011 #2
bar(std::move(*it));   // kosher?

不是你想要的方式。您无法移动对象。并且仅提供对其元素的访问。所以的类型是 .conststd::initializer_listconstitconst T *

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

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

评论

0赞 Potatoswatter 11/20/2011
const xvalue 仍然是一个 xvalue,并在必要时引用堆栈。(如果内容不是恒定的,它仍然是线程安全的。initializer_list
6赞 Nicol Bolas 11/20/2011
@Potatoswatter:你不能从一个恒定的对象移动。对象本身可能是一个 xvalue,但它的内容(它指向的实际值数组)是 ,因为这些内容可能是静态值。您根本无法从 .initializer_listconstinitializer_list
0赞 Potatoswatter 11/20/2011
请参阅我的回答及其讨论。他移动了取消引用的迭代器,生成了一个 xvalue。 可能毫无意义,但这是合法的,甚至可以声明一个只接受它的参数。如果移动特定类型恰好是无操作,它甚至可能正常工作。constmove
1赞 Nicol Bolas 11/20/2011
@Potatoswatter:C++11 标准花费了大量语言来确保除非您使用 .这样可以确保从检查中判断移动操作何时发生,因为它会影响源和目标(您不希望它隐式地发生在命名对象中)。因此,如果您在不发生移动操作的地方使用(如果您有 xvalue,则不会发生实际移动),则代码具有误导性。我认为在对象上可调用是一个错误。std::movestd::moveconststd::moveconst
1赞 Potatoswatter 11/20/2011
也许吧,但我仍然会减少对误导性代码可能性的规则的例外。无论如何,这正是我回答“否”的原因,即使它是合法的,结果是 xvalue,即使它只会绑定为常量左值。老实说,我已经在带有托管指针的垃圾回收类中进行了短暂的调情,其中所有相关内容都是可变的,并且移动会移动指针管理,但不会影响包含的值。总是有棘手的边缘情况:v) 。const &&
-1赞 Sumant 9/19/2013 #3

考虑一下 cpptruths 上描述的成语。这个想法是在运行时确定左值/右值,然后调用 move 或 copy-construction。 将检测右值/左值,即使initializer_list提供的标准接口是常量参考。in<T>in<T>

评论

4赞 fredoverflow 9/19/2013
当编译器已经知道值类别时,为什么要在运行时确定它?
1赞 Sumant 9/21/2013
请阅读博客,如果您不同意或有更好的选择,请给我留言。即使编译器知道值类别,initializer_list也不会保留它,因为它只有常量迭代器。因此,在构造initializer_list并传递它时,您需要“捕获”值类别,以便函数可以随心所欲地使用它。
6赞 Yakk - Adam Nevraumont 7/19/2016
这个答案基本上没有用,不点击链接,SO答案应该有用,不点击链接。
1赞 underscore_d 8/10/2016
@Sumant [从其他地方的相同帖子中复制我的评论] 这种笨拙的混乱是否真的为性能或内存使用提供了任何可衡量的好处,如果是这样,那么足够多的此类好处足以充分抵消它看起来有多糟糕,以及需要大约一个小时才能弄清楚它试图做什么的事实?我有点怀疑。
4赞 Marc van Leeuwen 7/7/2014 #4

这不会像所说的那样工作,因为 has type ,并且您无法从常量对象中移动。语言设计者这样做可能是为了让初始值设定项列表包含字符串常量,例如,从中移动是不合适的。list.begin()const T *

但是,如果您知道初始值设定项列表包含右值表达式(或者您想强制用户编写这些表达式),那么有一个技巧可以使它起作用(我受到 Sumant 对此的答案的启发,但解决方案比那个简单得多)。您需要存储在初始值设定器列表中的元素不是值,而是封装 .然后,即使这些值本身是限定的,它们仍然可以检索可修改的右值。TT&&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,似乎所有情况下的相关完整表达式都包含初始值设定项列表的使用,因此我认为不存在悬空右值引用的危险。

评论

0赞 dyp 7/7/2014
字符串常量?喜欢?如果从它们移动,只需复制指针(或绑定引用)。"Hello world"
1赞 dyp 7/7/2014
“一个问题确实需要答案”里面的初始值设定项绑定到 函数参数中的引用。这不会延长它们的生存期,它们仍然在创建它们的完整表达式结束时被销毁。{..}rref_capture
0赞 Kuba hasn't forgotten Monica 10/27/2016
根据 T.C. 对另一个答案的评论:如果您有多个构造函数重载,请将 std::initializer_list<rref_capture<T>> 包装在你选择的某个转换特征中 - 例如,std::d ecay_t - 以阻止不需要的推导。
0赞 Hiroshi Ichikawa 3/26/2017 #5

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

#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 值引用。

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

评论

0赞 underscore_d 7/2/2017
问题是是否可以移动,而不是是否有人有变通办法。此外,它的主要卖点是它只对元素类型进行模板化,而不是对元素数量进行模板化,因此不需要收件人也被模板化——这完全失去了这一点。initializer_listinitializer_list
2赞 Richard Hodges 6/1/2017 #6

我认为为解决方法提供一个合理的起点可能会很有启发性。

内联注释。

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

评论

0赞 underscore_d 7/2/2017
问题是是否可以移动,而不是是否有人有变通办法。此外,它的主要卖点是它只对元素类型进行模板化,而不是对元素数量进行模板化,因此不需要收件人也被模板化——这完全失去了这一点。initializer_listinitializer_list
2赞 Richard Hodges 7/3/2017
@underscore_d你是绝对正确的。我认为,分享与该问题相关的知识本身是一件好事。在这种情况下,也许它帮助了 OP,也许它没有——他没有回应。然而,OP和其他人通常欢迎与该问题有关的额外材料。
0赞 underscore_d 7/3/2017
当然,对于那些想要类似的东西但不受所有限制的读者来说,它确实可能会有所帮助。:)initializer_list
0赞 Richard Hodges 7/3/2017
@underscore_d我忽略了哪些限制?
0赞 underscore_d 7/3/2017
我的意思是(通过编译器魔术)避免了必须根据元素数量对函数进行模板化,这是基于数组和/或可变函数的替代方案固有的,从而限制了后者可用的情况范围。根据我的理解,这恰恰是拥有 的主要理由之一,所以似乎值得一提。initializer_listinitializer_list
0赞 bumfo 7/11/2019 #7

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

包装类被设计为以使用的方式使用,只需替换为 ,但这需要 C++17。对于较旧的规范,您可以使用其他构建器方法。std::movestd::movemove_wrapper

您需要编写内部接受包装类的构建器方法/构造函数,并相应地移动元素。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)
}
1赞 Jorge Bellon 6/2/2020 #8

您可以将参数声明为数组右值引用,而不是使用 。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)});
}

请参阅示例:https://gcc.godbolt.org/z/2uNxv6std::unique_ptr<int>

评论

0赞 axerologementy 5/9/2022
可悲的是,这似乎破坏了 A{a, b, c} 语法,需要 A({a, b, c}) 才能工作。