【问题标题】:initializer_list and move semanticsinitializer_list 和移动语义
【发布时间】:2012-01-01 20:20:28
【问题描述】:

我可以将元素移出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&lt;T&gt; 需要特别注意编译器,并且不像 C++ 标准库的普通容器那样具有值语义,所以我宁愿安全而不是抱歉和问。

【问题讨论】:

  • 核心语言定义initializer_list&lt;T&gt;引用的对象是-const。比如,initializer_list&lt;int&gt; 指的是int 对象。但我认为这是一个缺陷——编译器可以在只读内存中静态分配一个列表。

标签: c++ templates c++11 move-semantics initializer-list


【解决方案1】:

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

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

可能的原因是编译器可以选择将initializer_list 设为静态初始化常量,但由编译器自行决定将其类型设为initializer_listconst initializer_list 似乎更简洁,所以用户不知道是期待const 还是来自beginend 的可变结果。但这只是我的直觉,可能有一个很好的理由我错了。

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

【讨论】:

  • 如果不清楚,它仍然意味着使用std::move 是安全的,即使没有效率。 (除非 T const&amp;&amp; 移动构造函数。)
  • @David:好点子,但让std::initializer_list &amp;&amp; 重载做某事仍然有用,即使还需要非引用重载。我想这会比目前已经很糟糕的情况更令人困惑。
  • @JBJansen 它不能被黑客入侵。我不知道该代码应该完成 wrt initializer_list 的确切内容,但作为用户,您没有从它移动所需的权限。安全代码不会这样做。
  • @Potatoswatter,迟到的评论,但提案的状态如何。它是否有可能进入 C++20?
  • 这个提案有进展吗?我也很惊讶初始化程序列出了强制副本。
【解决方案2】:
bar(std::move(*it));   // kosher?

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

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

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

【讨论】:

  • 一个 const xvalue 仍然是一个 xvalue,如果有必要,initializer_list 会引用堆栈。 (如果内容不是常量,它仍然是线程安全的。)
  • @Potatoswatter:你不能从一个常量对象中移动。 initializer_list 对象本身可能是一个 xvalue,但它的内容(它指向的实际值数组)是 const,因为这些内容可能是静态值。您根本无法从initializer_list 的内容中移动。
  • 查看我的回答及其讨论。他已经移动了取消引用的迭代器,产生了一个const xvalue。 move 可能没有意义,但它是合法的,甚至可以声明一个接受它的参数。如果移动特定类型恰好是空操作,它甚至可能正常工作。
  • @Potatoswatter:C++11 标准使用了大量语言来确保非临时对象实际上不会被移动,除非您使用std::move。这确保您可以通过检查来判断移动操作何时发生,因为它会影响源和目标(您不希望它隐式地发生在命名对象中)。因此,如果您在移动操作发生的地方使用std::move(如果您有const xvalue,则不会发生实际移动),那么代码会产生误导.我认为 std::moveconst 对象上可调用是一个错误。
  • 也许吧,但我仍然会针对可能误导代码的规则采取更少的例外。无论如何,这正是我回答“不”的原因,即使它是合法的,结果是一个 xvalue,即使它只会绑定为 const 左值。老实说,我已经在一个带有托管指针的垃圾收集类中对const &amp;&amp; 进行了简短的调情,其中所有相关的内容都是可变的,并且移动会移动指针管理,但不会影响包含的值。总是有棘手的边缘情况:v)。
【解决方案3】:

这不会像之前说的那样工作,因为list.begin() 的类型是const T *,而且你无法从常量对象中移动。语言设计者可能这样做是为了允许初始化列表包含例如字符串常量,从这些常量中移动是不合适的。

但是,如果您知道初始化器列表包含右值表达式(或者您想强制用户编写这些表达式),那么有一个技巧可以使它起作用(我受到答案的启发Sumant 为此,但解决方案比那个简单)。您需要存储在初始化程序列表中的元素不是T 值,而是封装T&amp;&amp; 的值。然后,即使这些值本身是 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&lt;T&gt; 参数,而是声明initializer_list&lt;rref_capture&lt;T&gt; &gt; 参数。这是一个具体的例子,涉及std::unique_ptr&lt;int&gt;智能指针的向量,只定义了移动语义(因此这些对象本身永远不能存储在初始化列表中);然而下面的初始化列表编译没有问题。

#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,似乎所有情况下相关的 full-expression 都包含初始化列表的使用,所以我认为没有悬挂右值引用的危险。

【讨论】:

  • 字符串常量?喜欢"Hello world"?如果你离开它们,你只需复制一个指针(或绑定一个引用)。
  • “一个问题确实需要一个答案” {..} 内部的初始化器绑定到rref_capture 的函数参数中的引用。这不会延长它们的生命周期,它们仍然会在创建它们的完整表达式结束时被销毁。
  • Per T.C.'s comment from another answer:如果您有多个构造函数重载,std::initializer_list&lt;rref_capture&lt;T&gt;&gt; 包装在您选择的某些转换特征中 - 例如,std::decay_t -阻止不必要的扣除。
【解决方案4】:

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

内嵌评论。

#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 你说得对。我认为分享与问题相关的知识本身就是一件好事。在这种情况下,也许它帮助了 OP,也许它没有 - 他没有回应。然而,OP 和其他人通常会欢迎与该问题相关的额外材料。
  • 当然,对于想要initializer_list 之类的东西但不受所有使其有用的限制的读者来说,它确实可能会有所帮助。 :)
  • @underscore_d 我忽略了哪些约束?
  • 我的意思是initializer_list(通过编译器魔术)避免了必须对元素数量进行模板函数,这是基于数组和/或可变参数函数的替代方案固有的要求,因此限制了后者可用的情况范围。据我了解,这正是拥有initializer_list 的主要理由之一,因此似乎值得一提。
【解决方案5】:

您可以将参数声明为数组右值引用,而不是使用 std::initializer_list&lt;T&gt;

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&lt;int&gt;的示例:https://gcc.godbolt.org/z/2uNxv6

【讨论】:

    【解决方案6】:

    在当前标准中似乎不允许already answered。这是实现类似功能的另一种解决方法,通过将函数定义为可变参数而不是采用初始化列表。

    #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,而不是是否有人有解决方法。此外,initializer_list 的主要卖点是它只根据元素类型而不是元素数量进行模板化,因此不需要收件人也被模板化 - 这完全失去了这一点。
    【解决方案7】:

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

    包装类被设计成使用std::move的方式使用,只需将std::move替换为move_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)
    }
    

    【讨论】:

      【解决方案8】:

      考虑cpptruths 中描述的in&lt;T&gt; 成语。这个想法是在运行时确定左值/右值,然后调用移动或复制构造。 in&lt;T&gt; 将检测右值/左值,即使 initializer_list 提供的标准接口是 const 引用。

      【讨论】:

      • 在编译器已经知道的情况下,为什么还要在运行时确定值类别?
      • 如果您不同意或有更好的选择,请阅读博客并给我留言。即使编译器知道值类别,initializer_list 也不会保留它,因为它只有 const 迭代器。因此,您需要在构造 initializer_list 时“捕获”值类别并传递它,以便函数可以随意使用它。
      • 这个答案不关注链接基本没用,SO答案不关注链接应该有用。
      • @Sumant [从其他地方的相同帖子中复制我的评论] 这种巨大的混乱是否真的为性能或内存使用提供了任何可衡量的好处,如果是这样,足够多的此类好处足以抵消如何它看起来很糟糕,而且它需要大约一个小时才能弄清楚它想要做什么?我有点怀疑。
      猜你喜欢
      • 2016-02-04
      • 2020-08-20
      • 1970-01-01
      • 1970-01-01
      • 2021-12-12
      • 2013-02-11
      相关资源
      最近更新 更多