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


96

から要素を移動することはでき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ます。しかし、それは欠点だと思います-コンパイラが静的にリストを読み取り専用メモリに割り当てることができることが意図されています。
ヨハネスシャウブ-litb '19年

回答:


89

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

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

おそらくこれの理由は、コンパイラーがinitializer_list静的に初期化された定数を作成することを選択できるようにするためですが、その型を作成するinitializer_listconst initializer_list、コンパイラーの裁量で作成するほうがきれいなようで、ユーザーはa constとmutableのどちらを期待するかわかりませんbeginおよびからの結果end。しかし、それはただ私の直感です。おそらく私が間違っているのには十分な理由があります。

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


11
明確ではない場合でもstd::move、生産的ではないにしても、安全に使用できることを意味します。(T const&&移動コンストラクターを除外します。)
Luc Danton

私はあなたが全体の議論をすることができるとは思いませんか、const std::initializer_list<T>またはstd::initializer_list<T>頻繁に驚きを引き起こさない方法でだけです。の各引数はinitializer_listどちらでもかまいconstませんが、呼び出し元のコンテキストでは既知ですが、コンパイラは呼び出し先のコンテキストでコードの1つのバージョンのみを生成する必要があります(つまり、内部fooでは引数について何も知りません)。呼び出し側が)で通過していること
デイヴィッド・ロドリゲス- dribeas

1
@David:良い点ですが、std::initializer_list &&参照以外のオーバーロードも必要な場合でも、オーバーロードで何かを実行すると便利です。すでに悪い現在の状況よりもさらに混乱するでしょう。
Potatoswatter

1
@JBJansenハッキングすることはできません。そのコードがwrt initializer_listを実行すると想定されていることは正確にはわかりませんが、ユーザーとして、そこから移動するために必要な権限がありません。安全なコードはそうしません。
Potatoswatter 2014年

1
@Potatoswatter、遅いコメントですが、提案の状況はどうですか。C ++ 20になる可能性はありますか?
WhiZTiM 2017

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

5
@Potatoswatter:定数オブジェクトから移動することはできません。initializer_listオブジェクト自体ははxValueかもしれないが、コンテンツ(それが指している値の実際の配列)であるだconstものコンテンツが静的な値とすることができるので、。のコンテンツから移動することはできませんinitializer_list
Nicol Bolas

私の答えとその議論を見てください。逆参照されたイテレータを移動して、constxvalueを生成しました。move意味がないかもしれませんが、それは合法であり、それだけを受け入れるパラメータを宣言することさえ可能です。特定のタイプを移動しても何も起こらない場合は、正しく機能することもあります。
Potatoswatter

1
@Potatoswatter:C ++ 11標準では、を使用しない限り非一時的なオブジェクトが実際に移動されないようにするために、多くの言語が使用されますstd::move。これにより、移動操作がいつ発生したかを検査から確実に知ることができます。これは、移動操作が移動元と移動先の両方に影響するためです(名前付きオブジェクトに対して暗黙的に行われることは望ましくありません)。そのためstd::move、移動操作発生しない場所で使用すると(そして、constxvalue がある場合は実際の移動は発生しません)、コードは誤解を招きます。オブジェクトstd::moveで呼び出せるのは間違いだと思いconstます。
Nicol Bolas、2011年

1
たぶん、でも、誤解を招くコードの可能性についてのルールの例外は少なくします。とにかく、それが合法であるにもかかわらず「いいえ」と答えたのはまさにそのためであり、const左辺値としてのみバインドされる場合でも、結果はx値です。正直に言うと、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;
}

1つの質問には答えが必要です。イニシャライザリストの要素が真のprvaluesである必要がある場合(例ではxvalues)、言語は対応する一時オブジェクトのライフタイムがそれらが使用されるポイントまで延長されることを保証しますか?率直に言って、規格の関連セクション8.5がこの問題にまったく対処していないと思います。ただし、1.9:10を読むと、すべての場合に関連する全式がイニシャライザリストの使用を包含しているように見えるため、右辺値参照をぶら下げる危険はないと思います。


文字列定数?のように"Hello world"?それらから移動する場合は、ポインタをコピーする(または参照をバインドする)だけです。
dyp 2014

1
「1つの質問には答えが必要です」内部のイニシャライザ{..}は、の関数パラメータの参照にバインドされていますrref_capture。これは寿命を延ばすわけではなく、作成された完全な表現の最後にまだ破棄されます。
dyp 2014

別の回答からのTCのコメントによると:コンストラクターに複数のオーバーロードがある場合は、選択した変換特性をラップしてstd::initializer_list<rref_capture<T>>(たとえば)、std::decay_t不要な演繹をブロックします。
2016

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 2017

1
@underscore_dあなたは絶対的に正しいです。私は、質問に関連する知識を共有すること自体が良いことだと思います。この場合、おそらくそれはOPを助け、おそらく役に立たなかった-彼は応答しなかった。しかしながら、多くの場合、OPや他の人は、質問に関連する追加の資料を歓迎します。
Richard Hodges 2017

確かに、それは、何かを望んでいるinitializer_listが、それを有用にするすべての制約を受けていない読者にとって確かに役立つかもしれません。:)
underscore_d

@underscore_d私が見落とした制約はどれですか?
Richard Hodges 2017

つまり、initializer_list(コンパイラーマジックを介して)要素の数に基づいて関数のテンプレートを作成する必要がなくなるということです。これは、配列や可変関数に基づく代替手段によって本質的に必要とされるもので、後者が使用できるケースの範囲を制限します。私の理解では、これはまさにを持つinitializer_listための主要な理論的根拠の1つなので、言及する価値があるように思われました。
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;
}

Variadicテンプレートは、initializer_listとは異なり、r値参照を適切に処理できます。

このコード例では、小さなヘルパー関数のセットを使用して、可変個引数をベクトルに変換し、元のコードと同様にしました。しかし、もちろん、代わりに可変長テンプレートを使用して再帰関数を直接記述することもできます。


問題は、initializer_list誰かが回避策を持っているかどうかではなく、から移動できるかどうかでした。さらに、の主なセールスポイントはinitializer_list、要素の数ではなく要素タイプのみでテンプレート化されるため、受信者もテンプレート化する必要がなく、これにより完全に失われます。
underscore_d 2017

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を呼び出すことです。in<T>initializer_listによって提供される標準インターフェースがconst参照であっても、rvalue / lvalueを検出します。


4
コンパイラーがすでにそれを知っているのに、なぜ実行時に値カテゴリーを決定したいのですか?
fredoverflow 2013

1
あなたが同意しないか、より良い代替案がある場合は、ブログを読んでコメントを残してください。コンパイラが値のカテゴリを知っている場合でも、initializer_listはconstイテレータしか持っていないため、それを保持しません。したがって、initializer_listを作成して渡すときに、値カテゴリを「キャプチャ」して、関数がそれを自由に使用できるようにする必要があります。
Sumant

5
この回答は基本的にリンクをたどらないと役に立たないので、SOの回答はリンクをたどらないと便利です。
Yakk-Adam Nevraumont

1
@Sumant [他の場所にある同じ投稿から私のコメントをコピーする]その巨大な混乱は、実際にパフォーマンスまたはメモリ使用量に測定可能な利点を提供しますか。それが何をしようとしているのかを理解するのに約1時間かかりますか?ちょっと疑問です。
underscore_d
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.