initializer_list并移动语义


97

我可以将元素移出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 ++标准库的普通容器那样的值语义,因此,我宁愿安全而不愿后悔。


核心语言定义了对象由称为initializer_list<T> -const。喜欢,initializer_list<int>是指int物体。但是我认为这是一个缺陷-旨在使编译器可以在只读内存中静态分配列表。
Johannes Schaub-litb

Answers:


89

不,这不会按预期工作;您仍然会得到副本。我对此感到非常惊讶,因为我以为initializer_list存在可以保留一系列临时人员,直到被临时裁定为止move

beginend对于initializer_list回报const T *,所以结果move在你的代码T const &&-一个不变的右值引用。这样的表达式不能被有意义地删除。它将绑定到类型的函数参数,T const &因为rvalues确实绑定到const lvalue引用,并且您仍然会看到复制语义。

可能的原因是这样,编译器可以选择创建initializer_list一个静态初始化的常量,但似乎可以更轻松地创建其类型initializer_listconst initializer_list由编译器自行决定,因此用户不知道是否期望a const或可变begin和的结果end。但这只是我的直觉,可能是我错了的充分原因。

更新:我已经写了一份ISO建议,initializer_list支持仅移动类型。这只是一个初稿,尚未在任何地方实施,但是您可以查看它以进一步分析问题。


11
万一不清楚,那仍然意味着使用std::move是安全的,即使效率不高。(禁止T const&&移动构造函数。)
Luc Danton

我认为您不能const std::initializer_list<T>或者std::initializer_list<T>不会以经常引起意外的方式提出整个论点。考虑到其中的每个参数initializer_list都可以是const或不是,并且在调用方的上下文中是已知的,但是编译器必须在被调用方的上下文中仅生成代码的一个版本(即,在内部foo它对参数不了解任何内容)来电者传入)
大卫·罗德里格斯(DavidRodríguez

1
@David:好点,但是std::initializer_list &&即使也需要非引用重载,让重载做一些事情仍然有用。我想这将比目前的情况更加混乱,而目前的情况已经很糟了。
Potatoswatter

1
@JBJansen不能被黑客入侵。我没有确切看到该代码应完成wrt initializer_list的内容,但是作为用户,您没有从它移出所需的权限。安全代码不会这样做。
Potatoswatter 2014年

1
@Potatoswatter,最新评论,但提案的状态如何。它有可能进入C ++ 20吗?
WhiZTiM'7

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

并非您打算的那样。您不能移动const对象。并且std::initializer_list仅提供const对其元素的访问。因此,类型itconst T *

您的呼叫尝试std::move(*it)只会产生一个L值。IE:副本。

std::initializer_list引用静态内存。这就是课程的目的。您不能从静态内存中移出,因为移动意味着要对其进行更改。您只能从中复制。


const xvalue仍然是xvalue,并initializer_list在必要时引用堆栈。(如果内容不是恒定的,则它仍然是线程安全的。)
Potatoswatter 2011年

5
@Potatoswatter:您不能从恒定的物体上移动。所述initializer_list对象本身可以是x值,但它的内容(值的实际阵列它指向)是const,因为这些内容可以是静态值。您根本无法脱离的内容initializer_list
Nicol Bolas

请参阅我的答案及其讨论。他已经移动了取消引用的迭代器,从而产生了constxvalue。move可能是没有意义的,但是声明一个仅接受该参数的参数是合法的,甚至是可能的。如果移动特定类型碰巧是空手,那么它甚至可以正常工作。
Potatoswatter

1
@Potatoswatter:C ++ 11标准花费了大量语言,以确保除非使用,否则不会临时移动非临时对象std::move。这样可以确保您可以从检查中得知何时发生移动操作,因为它会影响源和目标(您不希望隐式发生在命名对象上)。因此,如果您std::move发生移动操作的地方使用(如果您具有constxvalue,则不会发生实际的移动),那么代码将产生误导。我认为std::move可以在const对象上调用是错误的。
Nicol Bolas

1
也许可以,但是在误导性代码的可能性方面,我仍将规则的例外情况排除在外。无论如何,即使这是合法的,这也正是我回答“否”的原因,即使将其仅绑定为const lvalue,结果还是一个xvalue。老实说,我已经const &&在一个带有托管指针的垃圾回收类中进行了简短的调情,其中所有相关的东西都是可变的,移动移动了指针管理,但不影响所包含的值。总是有一些棘手的情况:v)。
Potatoswatter

2

这将无法正常运行,因为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;
}

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


字符串常量?喜欢"Hello world"吗?如果从它们移开,则只需复制一个指针(或绑定一个引用)。
dyp

1
“一个问题确实需要答案”。内部的初始化{..}程序绑定到的功能参数中的引用rref_capture。这不会延长它们的寿命,它们在创建它们的完整表达式结束时仍然被销毁。
dyp

根据TC在另一个答案中的评论:如果构造函数有多个重载,请包装std::initializer_list<rref_capture<T>>您选择的某种转换特性-例如,std::decay_t-阻止不必要的推论。
恢复莫妮卡

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

1
@underscore_d你绝对正确。我认为,共享与问题相关的知识本身就是一件好事。在这种情况下,可能对OP有所帮助,但可能没有帮助-他没有回应。但是,OP和其他人常常欢迎与该问题有关的其他材料。
理查德·霍奇斯

当然,对于想要类似initializer_list但不受所有限制使其有用的读者来说,这确实可能有所帮助。:)
underscore_d

@underscore_d我忽略了哪些限制?
理查德·霍奇斯

我的意思是initializer_list(通过编译器魔术)不必在元素数量上模板化函数,这是基于数组和/或可变参数函数的替代方法固有的要求,从而限制了后者可用的情况范围。据我了解,这恰恰是拥有的主要理由之一initializer_list,因此似乎值得一提。
underscore_d

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

可变参数模板可以适当地处理r值引用,这与initializer_list不同。

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


问题是是否initializer_list可以将其移开,而不是是否有人有解决方法。除此之外,它的主要卖点initializer_list是它仅在元素类型上进行模板化,而不是在元素数量上进行模板化,因此不需要将接收者也进行模板化-这样就完全失去了这一点。
underscore_d

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

0

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


-1

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


4
当编译器已经知道时,为什么要在运行时确定值类别?
fredoverflow

1
如果您不同意或有更好的选择,请阅读博客,并给我留下评论。即使编译器知道值类别,initializer_list也不会保留它,因为它只有const迭代器。因此,在构造initializer_list并将其传递时,您需要“捕获”值类别,以便函数可以随意使用它。
Sumant

5
如果不遵循链接,此答案基本上是没有用的,因此,如果不遵循链接,SO答案应该是有用的。
Yakk-Adam Nevraumont

1
@Sumant [从其他地方的同一篇文章中复制我的评论]巨大的混乱是否真的为性能或内存使用量提供了任何可衡量的好处,如果是的话,是否有足够多的此类好处可以充分抵消它的外观及其可怕性,需要大约一个小时来弄清楚它要做什么?我对此表示怀疑。
underscore_d
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.