自動非静的データ メンバー初期化子のケース

この記事では、C++ の自動非静的データ メンバー初期化子について説明します。すべてのコード スニペットは、Matt Godbolt と CE チームのおかげで、Compiler Explorer でテストできます。この機能を有効にするための clang パッチは、5 年前に Faisal Vali によって作成されました。 、しかし、clang トランク (~ 7.0) の上に大雑把にリベースしました。

実際、この記事の主な動機は、この機能を人々の手に渡して、それが機能し、標準への優れた追加になることを証明することです.

Compiler Explorer で提案された機能をテストする能力を持つことは、機能とそのコーナー ケースをよりよく理解するための優れた方法です。 コード スニペットを試してみることをお勧めします .

しかし、まず第一に。

自動非静的データ メンバー初期化子 (NSDMI) とは?

データ メンバー初期化子

C++ では、メンバー変数のデフォルト値を導入できます。これは、コンストラクター メンバー初期化子リストで、または集約初期化を使用して、明示的に初期化しない場合に変数を開始するために使用されます。


int main() {
 struct S {
 int a = 42;
 };
 S s;
 return s.a;
}

これは データ メンバー初期化子 と呼ばれます .初期化子は、メンバーが明示的に初期化されていない場合にのみ評価されます。たとえば、次の例では main 0 を返します;


int ret = 0;
int main () {
 struct {
 int x = ++ret;
 } x = {0};
 return ret;
}

静的データ メンバー初期化子

同様に、静的メンバーは初期化子を持つことができますが、規則は少し異なります.まず、静的データ メンバー初期化子は常に評価され、クラス外の定義に取って代わります.

s::foo を定義しようとするため、次のコードは失敗します 2 回:


struct s {
 static const int foo = 42;
};
int s::foo = 42;

リテラル値を表す静的データ メンバーのみが、データ メンバー初期化子を持つことができます。そうでない場合、その静的メンバーにはリンケージが必要であり (必要に応じて、実行時にアドレス指定可能である必要があります)、プログラム全体でのみ定義されるためです。そうしないと、ODR 違反が発生します。 あえぎ .

自動静的データ メンバー初期化子

データ メンバー初期化子を持つ静的データ メンバー auto で宣言できます。


struct s {
 static const auto foo = 42;
};
この場合、foo タイプ int であると推定されます auto を使用した変数の宣言とまったく同じように機能します :右側の式が評価され、その型によって変数の型 (この場合は静的データ メンバー) が決まります。

自動非静的データ メンバー初期化子

これらすべての要素を使用して、NSDMI が何であるかを確認できます。これは、型が推定される初期化子を持つクラスまたは構造体のデータ メンバーにすぎません。


struct s {
 auto foo = 42;
};

ただし、これはコンパイルされません。標準では禁止されています。

自動 NSDM のケース

つまり、自動非静的データ メンバー初期化子 実際には、C++17 でも今後の C++20 でも問題ではありません。最後に提案されたのは 2008 年で、それ以来あまり議論されていません - このブログ投稿では、これに対処しようとしています!

では、上記のコードは有効でしょうか?私は間違いなくそう思います.議論は本当に…なぜですか?

常に自動?そうではありません。

それは不十分な議論のように聞こえるかもしれませんが、データ メンバーは auto で宣言できない唯一のエンティティです。 .auto あらゆる種類のコンテキストであらゆる種類の変数を宣言できますが、これは.そして、その種の例外は期待を裏切ります。ユーザーはそれらを自然に使用しようとするかもしれませんが、なぜ機能しないのか不思議に思うかもしれません。その場合、適切な説明を考え出す必要があります。

自動車の表現力

自動 NSDMI を使用する理由は、auto を使用する場合と同じです。 他のコンテキストで。現時点で最強のショーケースはタイプ控除だと思います


#include <vector>
struct s {
 auto v1 = std::vector{3, 1, 4, 1, 5};
 std::vector<int> v2 = std::vector{3, 1, 4, 1, 5};
};

make_uniquemake_shared すべての make_ とともに、良い候補にもなります 関数


#include <memory>
struct s {
 auto ptr = std::make_shared<Foo>();
 std::shared_ptr<Foo> ptr2 = std::make_shared<Foo>();
};

リテラルも良い候補になりますが、using namespace が必要です ヘッダーで行うことは避けてください。どちらがリテラルの問題であり、クラス スコープで名前空間を使用できないことです。


#include <chrono>
using namespace std::chrono_literals;
struct doomsday_clock {
 auto to_midnight = 2min;
};

既に動作しています

N2713 - Allow auto for non static data members - 2008 に記載されているように、auto で表現できるほとんどすべて decltype で表現できます


struct s {
 decltype(42) foo = 42;
};

実際、私たちはマクロを考案することができます (家でこれを試さないでください)


#define AUTO(var, expr) decltype(expr) var = (expr)
struct s {
 AUTO(foo, 42);
};

そして、あまり便利ではない構文で機能するのであれば、人々の生活を楽にしてみませんか?

Lambda データ メンバー

decltype では実現できないことが 1 つあります。 ただし、データメンバーとしてのラムダ。実際、各ラムダ式は一意の型であるため、decltype([]{}) foo = []{}; 動作しません。そのため、データ メンバーとしてのラムダを実現できません。もちろん、std::function などの何らかの型消去に頼らない限り、 .

メンバー関数の代わりにラムダを使用することにはあまり価値がないと思います.キャプチャ グループを持つラムダを除いて、キャプチャ グループ内の単一の呼び出し可能オブジェクトに固有の変数を格納できるため、気にするデータ メンバーが少なくなります.

たとえば、次の例では、構築時にグローバル変数をキャプチャします (繰り返しますが、自宅でこれを試さないでください!)。

/*
 prints 10 9 8 7 6 5 4 3 2 1
*/
#include <vector>
#include <iostream>
#include <range/v3/view/reverse.hpp>

int counter = 0;
struct object {
 auto id = [counter = ++counter] { return counter;};
};

int main() {
 std::vector<object> v(10);
 for(auto & obj : v | ranges::view::reverse) {
 std::cout << obj.id() << ' ';
 }
}

では、自動 NSDMI が標準にないのはなぜですか?

N2713 がそれらを追加することを提案したにもかかわらず、いくつかの懸念があったため削除され、少し忘れられていたようです.

クラスを解析するとき、コンパイラは最初に宣言 (関数シグネチャ、変数定義、ネストされたクラスなど) を解析し、次にインライン定義、メソッドのデフォルト パラメーター、およびデータ メンバー初期化子を解析します。

これにより、まだ宣言されていないメンバーに応じた式でメンバーを初期化できます。


struct s {
 int a = b();
 int b();
};

ただし、自動メンバーを導入すると、物事はそれほど単純ではありません。次の有効なコードを取得してください


struct s{
 auto a = b();
 int b() {
 return 42;
 };
} foo;

ここで、何が起こるか

<オール> <リ>

コンパイラはメンバー a を作成します auto の タイプ、この段階で変数 a 名前はありますが、実際に使用できるタイプはありません。

<リ>

コンパイラは関数 b を作成します int 型;

<リ>

コンパイラは a の初期化子を解析します と a int になります ただし、b() は呼び出されません。

<リ>

コンパイラは b の定義を解析します

<リ>

コンパイラは foo を構築し、b() を呼び出します a を初期化する

場合によっては、コンパイラがデータ メンバーの型を推測するときに、クラスがまだ完成していないため、プログラムの形式が正しくないことがあります。


struct s {
 auto a = sizeof(s);
 auto b = 0;
};

ここ:

<オール>
  • コンパイラはメンバ a を作成します auto の タイプ、この段階で変数 a 名前はありますが、実際に使用できるタイプはありません。
  • コンパイラはメンバ b を作成します auto の タイプ
  • コンパイラは a の初期化子を解析します そのタイプを決定するため
  • この段階では、a または b のサイズは不明です。クラスは「不完全」で、sizeof です。 式の形式が正しくありません:error: invalid application of 'sizeof' to an incomplete type 's' .
  • そのため、auto-nsdmi 内で実行できないことがいくつかあります:sizeof の呼び出し *this を参照 (decltype でも)、クラスのインスタンスの構築など。これはすべて意味があり、decltype で同じ問題が発生します。 .または単に実行することで

    
    struct s {
     s nope;
    };
    

    もう 1 つの落とし穴は、auto です。 データ メンバーは、後に宣言された別のデータ メンバーに依存することはできません:

    
    struct s {
     auto a = b;
     auto b = 0;
    };
    int main() {
     return s{}.a;
    }
    

    ここ:

    <オール>
  • コンパイラはメンバ a を作成します auto の タイプ、この段階で変数 a 名前はありますが、実際に使用できるタイプはありません。
  • コンパイラはメンバ b を作成します auto の タイプ、この段階で変数 b 名前はありますが、実際に使用できるタイプはありません。
  • コンパイラは a の初期化子を解析します そのタイプを決定するために。 b の型 は不明であるため、プログラムの形式が正しくありません。
  • これもまた、ほとんどの C++ 開発者にとって自然に感じるはずです。悲しいかな、これらの癖は機能がワーキング ドラフトで作成されないのに十分でした。

    バイナリ互換性

    struct S { auto x = 0; }; の変更 struct S { auto x = 0.0 ; }; へ abi の互換性を壊します。これは確かに少し紛らわしいかもしれませんが、auto で機能します。 戻り型には同じ問題があります。一般に、C++ でバイナリ安定インターフェイスを公開することは、避けるべき複雑な作業です。この提案された機能によって問題が大幅に悪化することはありません。何らかの理由でバイナリ互換性が気になる場合は、auto の使用を避けてください。 エクスポートされたインターフェイスで。また、データ メンバー初期化子の使用を避けることもできます

    論文は来ますか?

    それは私が計画していることではありません。もう一度議論を始めたかっただけです!元の論文は古すぎてまだ関連性がありません.

    当時の著者は次のように述べています:

    最近、comp.lang.c++.moderated で、decltype を使用して醜いコードを使用するだけで同じ効果が得られることが指摘されました。そのため、著者は auto に対する反対意見が和らいだと考えています。

    それ以来、規格の文言は大幅に変更されました。今日の標準で自動 NSDMI を正確に妨げているものを見つけるのに時間がかかったので、いくつかの文言を見てみましょう。

    dcl.spec.auto auto または decltype(auto) を使用して宣言された変数の型は、その初期化子から推測されます。この使用は、変数の初期化宣言 ([dcl.init]) で許可されています。 auto または decltype(auto) は、decl-specifier-seq 内の decl-specifiers の 1 つとして表示され、decl-specifier-seq の後には 1 つ以上の宣言子が続き、それぞれの宣言子の後には空でない初期化子が続きます。 .

    その最初の段落は auto foo = ... になります 有効で、簡単に見つけることができました。ただし、データ メンバーの除外については何も述べていません (静的データ メンバーを明示的に許可することもありません)。

    基本 変数は、非静的データ メンバーまたはオブジェクト以外の参照の宣言によって導入されます。変数の名前があれば、参照またはオブジェクトを示します。

    variable の規範的な定義を確認することを考える前に、私はかなり長い間立ち往生していました 、非静的データ メンバーを選び出します。いいですね。

    したがって、自動 NSDMI を標準に追加するには、以下を追加するだけで済みます:

    dcl.spec.auto auto または decltype(auto) を使用して宣言された変数またはデータ メンバーの型は、その初期化子から推定されます。この使用は、変数の初期化宣言 ([dcl.init]) で許可されています。

    しかし、委員会は、自動 NSDMI と後期クラス解析が相互作用する方法を正確に指定したい場合もあります。これは、ブログ投稿で説明するのは簡単ですが、言葉遣いを書くのははるかに困難です.

    謝辞

    • Matt Godbolt とコンパイラ エクスプローラ チームは、この実験的なブランチをコンパイラ エクスプローラに配置するのを手伝ってくれました。
    • 最初の clang サポートを作成した Faisal Vali。
    • この記事を書く動機を与えてくれた Alexandr Timofeev。

    参考文献

    • N2713 - 非静的データ メンバーの auto を許可 - 2008
    • N2712 - 非静的データ メンバー初期化子
    • C++ ワーキング ドラフト