web-dev-qa-db-ja.com

initializer_listおよび移動セマンティクス

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++標準ライブラリの通常のコンテナーのような値のセマンティクスがないため、申し訳ありませんが尋ねるよりも安全です。

83
fredoverflow

いいえ、意図したとおりに機能しません。あなたはまだコピーを取得します。 move 'dになるまで一時的な配列を保持するためにinitializer_listが存在すると思っていたので、これにはかなり驚いています。

beginendinitializer_listに対してconst T *を返すため、コード内のmoveの結果はT const && —不変の右辺値参照です。このような式は意味のあるものから移動することはできません。右辺値はconst左辺値参照にバインドするため、タイプT const &の関数パラメーターにバインドされますが、コピーセマンティクスが表示されます。

おそらくこの理由は、コンパイラがinitializer_listを静的に初期化された定数にすることを選択できるためですが、コンパイラのタイプでinitializer_listまたはconst initializer_listを作成する方がクリーンだと思われますそのため、ユーザーはconstbeginからendまたは可変の結果を期待するかどうかを知りません。しかし、それは私の直感です。おそらく、私が間違っているという正当な理由があります。

更新:私が書いた ISO提案initializer_listの移動のみの型のサポート。これは最初のドラフトにすぎず、まだどこにも実装されていませんが、問題をさらに分析するために見ることができます。

78
Potatoswatter
_bar(std::move(*it));   // kosher?
_

意図したとおりではありません。 constオブジェクトを移動することはできません。 _std::initializer_list_は、その要素へのconstアクセスのみを提供します。したがって、itのタイプは_const T *_です。

std::move(*it)を呼び出そうとすると、l値のみが返されます。 IE:コピー。

_std::initializer_list_参照静的メモリ。それがクラスの目的です。移動は変更を意味するため、静的メモリからmoveにすることはできません。そこからのみコピーできます。

17
Nicol Bolas

回避策の合理的な出発点を提供することは有益だと思いました。

インラインのコメント。

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

list.begin()の型は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>引数を宣言する代わりに、aninitializer_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;
}

1つの質問には答えが必要です。初期化子リストの要素が真の値(例ではxvalues)でなければならない場合、言語は対応するテンポラリのライフタイムがそれらが使用されるポイントまで延長することを保証しますか?率直に言って、標準の関連するセクション8.5はこの問題にまったく対処していないと思います。ただし、1.9:10を読むと、関連するfull-expressionがすべての場合に初期化子リストの使用を含むように見えるため、右辺値参照がぶら下がる危険性はないと思います。

2

要素を移動する意図を示すタグとして機能するラッパークラスを使用する、はるかに単純な実装があります。これはコンパイル時のコストです。

ラッパークラスは、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
bumfo

既に回答済み のように、現在の標準では許可されていないようです。初期化リストを取得する代わりに、関数を可変引数として定義することにより、同様のことを実現する別の回避策があります。

#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