std::initializer_list の修正

C++11 導入 06 .これは、事前に定義された一連の要素でコンテナ型を初期化する場合に使用される小さなクラスです。単純な古い C 配列と同じように、非常に便利な構文を使用できます。

ただし、いくつかの問題があります。この投稿では、それらとその修正方法について説明します。

この投稿では、例として次のクラスを使用します。

class my_vector
{
public:
 // initializes with count elements each having the given value
 my_vector(std::size_t count, int value);

 // initializes from a pointer range
 my_vector(const int* begin, const int* end);

 …
];

ここで関連するのはコンストラクターのみです。これは 11 の簡易バージョンです。 2 つの主要なコンストラクターを提供します。1 つは指定されたサイズで初期化し、もう 1 つはポインター範囲で初期化します。

指定したサイズのベクトルを作成したい場合は、次のように使用します:

my_vector vec(5, -1); // -1 -1 -1 -1 -1

配列の内容を取得したい場合は、次のように使用します:

template <std::size_t N>
my_vector copy(int (&array)[N})
{
 return my_vector(array, array + N);
}

簡単です。

しかし、要素 29 を含むベクトルが必要な場合はどうでしょうか。 、 3847 ?一時ストレージとして配列を使用する必要があります:

int array[] = {1, 2, 3};
my_vector vec(array, array + 3);

それはあまり良くないので、51 新しいコンストラクタを追加するだけです:

my_vector(std::initializer_list<int> ilist);

そして、次のように使用できます:

// all are equivalent:
my_vector vec1(std::initializer_list<int>{1, 2, 3});
my_vector vec2({1, 2, 3}); // omit the type name
my_vector vec3{1, 2, 3}; // omit the parenthesis
my_vector vec4 = {1, 2, 3};

これにより、配列の初期化 60 と同じ構文が可能になります。 2 つのランダム アクセス反復子によって定義された範囲を提供するだけなので、コンストラクターは 2 つのポインター コンストラクターと同じように実装できます。

では、74 の問題は何でしょうか? ?

いくつかあります:

問題 1):均一な初期化

まず、部屋にいるゾウに話しかけましょう:

C++11 には別の機能も追加されました - 一様な初期化です。一様な初期化自体も非常に優れています。これにより、単一の構文ですべてを初期化でき、ほとんどの厄介な解析と縮小変換を回避できます。

しかし、C++ では、2 つの無関係な機能が互いに強化し合い、その組み合わせがその部分の合計よりも大きくなり、機能が互いに強化し、多くの可能性を開く場合があります。そして、均一な初期化と 89 .

問題は、新しい統一された初期化構文が同じであることです。 93 のものとして !どちらも 104 を使用 と 114 特に、これは two と競合します。 上記の 4 つの初期化子リスト構文のうち、つまり 126137 .

スニペットを変更して、要素が 2 つだけになるようにしましょう:

my_vector vec1(std::initializer_list<int>{1, 2});
my_vector vec2({1, 2});
my_vector vec3{1, 2};
my_vector vec4 = {1, 2};

149 の構文 統一された初期化構文でコンストラクターを呼び出すのと同じです - 2 つの整数を取るコンストラクターがあるのは偶然です:カウント + 値 1.だから、これを呼び出して、1 つの 154 または、イニシャライザリストコンストラクタを呼び出して、ベクトルを 164 で初期化しますか? と 176 ?

しかし、188 にも同様のあいまいさがあります。 .イニシャライザ リスト コンストラクタを呼び出しますか、それとも一様な初期化を使用して一時的な 197 を作成しますか count + value コンストラクターからコピーして、それをコピーしますか?

答えは:202 がある場合 コンストラクターであり、何らかの方法で 215 に変換できるいくつかの要素でブレース構文を使用します 、それはイニシャライザ リスト コンストラクタを使用します。要素から 229 への変換の場合 縮小されているため、初期化子リスト コンストラクターを引き続き使用しますが、コンパイルに失敗します。

この動作は、悪名高い統一初期化の落とし穴を作成するために使用できます:

my_vector a(1, 2); // 2
my_vector b{1, 2}; // 1 2

したがって、すべてを均一な初期化に切り替えるだけで動作が変わります!これは、233 がある場合、均一な初期化がもはや均一ではないことを意味します。 代わりに括弧を使用する必要があります。

しかし、問題はこれで終わりではありません。

問題 2) ブレース付きイニシャライザには型がありません

コア言語は 246 用に修正されていますが、 、式 255 がありません 263 .テンプレート関数がある場合:

template <typename T>
void do_sth(T t);

そして、初期化子リストでそれを呼び出したい:

do_sth({1, 2, 3, 4, 5});

エラーが発生します。これにより、一般的な make 関数がコンパイルされないため、より複雑になります。

auto ptr = std::make_unique<my_vector>({1, 2, 3, 4, 5});

それをサポートしたい場合は、追加のオーバーロードを作成するなど、さらに多くの作業を行う必要があります:

template <typename T, typename ... Args>
foo make_foo(std::initializer_list<T> ilist, Args&&... args);

std::optional のインプレース コンストラクターのように、これを行う必要がある標準ライブラリ全体の多くのケースがあります。

そして、ブレース付きイニシャライザの自動推定のルールを説明しないでください!

問題 3):279 アクセスは 281 を返します

291 がある場合 コンストラクターは要素をコピーする必要がありますが、301 しか取得できないため移動できません elements.これは、312 を使用できないことを意味します 可動要素の場合、一時要素を渡したとしても、可能な限り効率的ではありません。

均一な初期化の問題の修正

重要な更新:ここで紹介するソリューションには、残念ながら問題があります。イニシャライザ リストによって作成された一時配列は、リスト自体が存続する間だけ存続します。そのため、ここで行うように、それらをメンバーとして格納することに十分注意する必要があります。

すべての問題は、間接的なレイヤーを追加することで解決できます。この問題も同様です。

329 の主な問題 おそらく均一な初期化に関する癖です。しかし、これは簡単に解決できます。間接的なレイヤーを追加します。独自の 338 を定義する :

#include <initializer_list>

template <typename T>
class initializer_list
{
public:
 initializer_list(std::initializer_list<T> ilist)
 : ilist_(ilist) {}

 const T* begin() const noexcept
 {
 return ilist_.begin();
 }

 const T* end() const noexcept
 {
 return ilist_.end();
 }

 std::size_t size() const noexcept
 {
 return ilist_.size();
 }

private:
 std::initializer_list<T> ilist_;
};

これは 349 の単なるラッパーです .しかし、355 を変更すると この型を使用するようにイニシャライザ リスト コンストラクタを使用すると、問題が解決します:

my_vector a(5, 0);
my_vector b{5, 0};
my_vector c({5, 0});
my_vector d{ {5, 0} }; // need space there, otherwise jekyll expands it...

367 通常どおり count + value コンストラクターを呼び出しますが、 377 また、それを呼び出します!これは、380 を取るコンストラクターがないためです。 したがって、通常のルールが適用されます。391 402 を意味する可能性があるため、実際にはコンパイル エラーです。 または 410 .Only 425 438 を使用します 446 の余分な中括弧があるため、コンストラクタ あいまいさを解決するには、優先順位が重要です。

これで、一様な初期化に関して貪欲ではないイニシャライザ リストができました。二重中括弧を含む構文が見苦しいと言う場合でも、問題ありません。これはまだ有効です:

my_vector e = {5, 0};

コンテナを要素で初期化するときに使用したい構文は、配列の構文と同じです。

残念ながら、その構文は使用できません。

テンプレート控除の修正

新しい 453460 の型を変更していません ただし、ジェネリック関数ではまだ適切に機能しません。また、リテラルの型を変更できないため、実際にできることはありません。

ユーザー定義のリテラルを作成することはできますが、波括弧付きの初期化子のバージョンはありません。最近、基本的に 470 を許可するという議論を見ました。 、しかしそれ以上は進みませんでした。

C++17 のクラス テンプレートの引数推定がまだないため、484 一般的な make 関数か、ライブラリの実装者の余分な作業のいずれかが残されています。

make 関数は次のようになります:

namespace detail
{
 template <typename T, typename ... Args>
 T get_list_t(int, std::initializer_list<T>);

 struct error
 {
 template <typename ... Args>
 error(Args&&...) {}
 };

 template <typename ... Args>
 error get_list_t(short, error);
}

template <typename ... Args>
auto make_list(Args&&... args)
{
 using value_type = decltype(detail::get_list_t(0, {std::forward<Args>(args)...}));
 static_assert(!std::is_same<value_type, detail::error>::value,
 "make_list() called without common type");
 return initializer_list<value_type>{std::forward<Args>(args)...};
}

490 関数自体は、リストの値の型を決定し、500 を使用してそれを返します。 513 のコンストラクタ .

ここでのスマートな部分は、値の型を決定することです。私はそれを 525 に活用しました それ自体。最初の 539 542 で呼び出されたときのオーバーロード 559 の引数を推測します 567 を返します .572 を推測できない場合 (タイプが競合するため)、2 番目のオーバーロードが選択されます - 586 を変換する必要があるため、優先度が低くなります。 リテラル 596 601 へ 、一般的なトリックです。2 番目のタイプは 618 です。 、できる 型の任意のセットから作成され、それを返します。

これで 621 できます 選択した関数の戻り値の型と 638 649 ではないこと

移動セマンティクスの許可

654 はまだ使えません すべての要素が右辺値であるリストを簡単にサポートできますが、これは設計上同種のコンテナーであり、左辺値参照 の両方を格納することはできません。 右辺値参照なので、混在させることはできません。

それを抽象化するために、間接的な第 2 層が必要です。

669 を作ってみましょう 671 にラッパーを格納する 、内部的にすべて 680 へのポインタを格納します 、しかし、右辺値が与えられたかどうかを覚えているので、 694 を呼び出すことができます または 703 コード内のその情報に応じて:

template <typename T>
class wrapper
{
public:
 wrapper(const T& value)
 : ptr_(&value), move_(false) {}

 wrapper(T&& value)
 : ptr_(&value), move_(true) {}

 const T& get() const
 {
 return *ptr_;
 }

 T&& get_rvalue() const
 {
 assert(move_);
 // const_cast safe, we know it was not declared const
 return std::move(*const_cast<T*>(ptr_));
 }

 bool is_rvalue() const
 {
 return move_;
 }

private:
 const T* ptr_;
 bool move_;
};

次のように使用します:

template <typename T>
void assign(T& val, const wrapper<T>& ref)
{
 if (ref.is_rvalue())
 val = ref.get_rvalue();
 else
 val = ref.get();
}

template <typename T>
void create(void* mem, const wrapper<T>& ref)
{
 if (ref.is_rvalue())
 ::new(mem) T(ref.get_rvalue());
 else
 ::new(mem) T(ref.get());
}

次に 718 を変更します 722 を格納するための実装 737 の代わりに 直接、745 を変更します 各引数をラッパーでラップするようにします。

754 を使用する場合よりも、オーバーヘッドがまったくないか、さらに少なくなります。 直接、移動セマンティクスも許可します。

移動セマンティクスの許可 - テイク 2

767 770 を使用 うまく機能しますが、コンパイラは、現在の要素が左辺値または右辺値であるかどうかをチェックする条件を削除できません。たとえその情報がコンパイル時にわかっていてもです。

そして 783 でも (およびインライン化) 要素の数がコンパイル時にわかっていても、ループを展開できません。

幸いなことに、C++11 には、任意の数のオブジェクトを関数に渡す機能も追加されました:可変個引数テンプレート. 真に汎用的なイニシャライザ リストが必要な場合は、可変個引数テンプレートと 797 を使用します。 または 804 816 と同じ構文を使用することもできます。 均一な初期化のおかげです。

確かに、実装は単純な 821 ではありません ただし、パック展開で実行できる場合があります。ただし、コンパイラはすべてを完全に最適化できます。

結論

830 均一な初期化、テンプレート引数、移動セマンティクスではうまく機能しません。

845 を単純にラッピングすることで、これらの問題をすべて解決できますが、 、各 856 をラップ 汎用の make 関数を提供しますが、これはまだ完全ではありません。

ただし、可変数の引数を受け入れるコンストラクターを作成すると、同じ構文が可能になり、これらの問題を完全に回避できます。 コンストラクター、可変数の引数を持つコンストラクターを作成することを検討してください。