自動初期化

おそらく C++11 で最も頻繁に使用される機能は auto です。 auto のおかげで、コンパイラは初期化子から変数の型を決定します。しかし、安全性が重要なソフトウェアに何の意味があるのでしょうか?

自動車の事実

auto による自動型推論は非常に便利です。第 1 に、特に困難なテンプレート式で、不要な入力を大幅に削減できます。第二に、プログラマーとは対照的に、コンパイラーは決して間違っていません。

次のリストで、明示的な型と推定型を比較します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <vector>

int myAdd(int a,int b){ return a+b; }

int main(){

 // define an int-value
 int i= 5; // explicit
 auto i1= 5; // auto
 
 // define a reference to an int
 int& b= i; // explicit
 auto& b1= i; // auto
 
 // define a pointer to a function
 int (*add)(int,int)= myAdd; // explicit
 auto add1= myAdd; // auto
 
 // iterate through a vector
 std::vector<int> vec;
 for (std::vector<int>::iterator it= vec.begin(); it != vec.end(); ++it){} 
 for (auto it1= vec.begin(); it1 != vec.end(); ++it1) {}

}

コンパイラは、変数の型を取得するために、テンプレート引数推定の規則を使用します。したがって、外側の const または volatile 修飾子と参照は削除されます。次の例は、定数と参照に対するこの動作を示しています。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
int main(){
 
 int i= 2011;
 const int i2= 2014;
 const int& i3= i2;
 
 auto a2= i2; // int
 auto a3= i3; // int
 
}

しかし、初期化に const int 型または const int&型の変数を使用したにもかかわらず、a2 または a3 が int 型であることをどのように確認できますか?時々私はそれを間違って推測します。答えは簡単です。コンパイラは真実を知っています。唯一の宣言されたクラス テンプレート GetType は、私を大いに助けてくれます。

template <typename T>
class GetType; 

宣言された唯一のクラス テンプレートを使用すると、コンパイラはすぐに文句を言います。定義がありません。それは私が必要とする特性です。コンパイラは、インスタンス化できないクラス テンプレートの型を正確に教えてくれます。まずは拡張ソースコードへ。次のソース コードを無効にして、宣言された唯一のクラス テンプレートをインスタンス化しようとしました。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#include <get_type.hpp>

int main(){
 
 int i= 2011;
 const int i2= 2014;
 // GetType<decltype(i2)> myType;
 const int& i3= i2;
 // GetType<decltype(i3)> myType;
 
 auto a2= i2; 
 // GetType<decltype(a2)> myType;
 auto a3= i3;
 // GetType<decltype(a3)> myType;
 
}

行 7、9、12、および 14 の GetType 呼び出しでは、指定子 decltype を使用しています。これにより、宣言された変数の正確な型が得られます。残りは大変な作業です。各 GetType 式について、順にコメントしました。 g++ コンパイラのエラー メッセージの詳細は非常に興味深いものです。

エラーメッセージのキー表現には赤い線があります。感動?しかし、もう一度。セーフティ クリティカルなソフトウェアのポイントは何ですか?

初期化してください!

auto は、初期化子からその型を決定します。それは単に意味します。初期化子がないと、型がないため、変数がありません。ポジティブに言えば。コンパイラは、各型の初期化を処理します。これは、めったに言及されない auto の優れた副作用です。

変数の初期化を忘れたのか、言語の理解が間違っていたために変数を作成できなかったのかは関係ありません。結果はまったく同じで、未定義の動作です。 auto を使用すると、これらの厄介なエラーを克服できます。正直に言ってください。変数の初期化に関するすべての規則を知っていますか?はいの場合、おめでとうございます。そうでない場合は、記事のデフォルトの初期化と、この記事で参照されているすべての記事をお読みください。なぜ彼らが次のステートメントを使用したのか、私にはわかりません。この定式化は、善よりも害を引き起こします。ローカル変数はデフォルトで初期化されません。

デフォルトの初期化の 2 番目のプログラムを変更して、未定義の動作をより明確にしました。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// init.cpp

#include <iostream>
#include <string>
 
struct T1 {};
 
struct T2{
 int mem; // Not ok: indeterminate value
 public:
 T2() {} 
};
 
int n; // ok: initialized to 0
 
int main(){
 
 std::cout << std::endl;
 
 int n; // Not ok: indeterminate value
 std::string s; // ok: Invocation of the default constructor; initialized to "" 
 T1 t1; // ok: Invocation of the default constructor 
 T2 t2; // ok: Invocation of the default constructor
 
 std::cout << "::n " << ::n << std::endl;
 std::cout << "n: " << n << std::endl;
 std::cout << "s: " << s << std::endl;
 std::cout << "T2().mem: " << T2().mem << std::endl;
 
 std::cout << std::endl;
 
}

まず、25 行目のスコープ解決 operator::に進みます。::はグローバル スコープを指定します。この場合、14 行目の変数 n です。不思議なことに、25 行目の自動変数 n の値は 0 です。n の値は未定義であるため、プログラムの動作は未定義です。これは、クラス T2 の変数 mem には当てはまりません。 mem は未定義の値を返します。

ここで、auto を使用してプログラムを書き直します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// initAuto.cpp

#include <iostream>
#include <string>
 
struct T1 {};
 
struct T2{
 int mem= 0; // auto mem= 0 is an error
 public:
 T2() {}
};
 
auto n= 0;
 
int main(){
 
 std::cout << std::endl;
 
 using namespace std::string_literals;
 
 auto n= 0;
 auto s=""s; 
 auto t1= T1(); 
 auto t2= T2();
 
 std::cout << "::n " << ::n << std::endl;
 std::cout << "n: " << n << std::endl;
 std::cout << "s: " << s << std::endl;
 std::cout << "T2().mem: " << T2().mem << std::endl;
 
 std::cout << std::endl;
 
}

ソース コードの 2 行は特に興味深いものです。まず、9 行目です。現在の標準では、クラスの非定数メンバーを auto で初期化することは禁止されています。したがって、明示的な型を使用する必要があります。これは、直感に反して、私の観点からです。この問題に関する C++ 標準化委員会の議論は、記事 3897.pdf にあります。次に、23 行目です。C++14 は C++ 文字列リテラルを取得します。 C 文字列リテラル ("") を使用してそれらを作成し、接尾辞 s (""s) を追加します。便宜上、20 行目:using namespace std::string_literals をインポートしました。

プログラムの出力はそれほどスリル満点ではありません。完全を期すためだけに。 T2().mem の値は 0 です。

リファクタリング

ちょうど今、この投稿を締めくくりたいと思います。自動車の新しいユースケースが頭に浮かびます。 auto は、コードのリファクタリングを非常にうまくサポートします。第 1 に、情報の型がない場合、コードを再構築するのは非常に簡単です。次に、コンパイラは適切な型を自動的に処理します。どういう意味ですか?コードスニペットの形で答えを出します。最初は auto なしのコードです。

int a= 5;
int b= 10;
int sum= a * b * 3;
int res= sum + 10; 

type in の変数 b を double 10.5 に置き換えると、依存するすべての型を調整する必要があります。それは面倒で危険です。適切なタイプを使用し、ナローイングやその他のインテリジェントな現象に対処する必要があります C++ で。

int a2= 5;
double b2= 10.5;
double sum2= a2 * b2 * 3;
double res2= sum2 * 10.5;

この危険は自動車の場合には存在しません。すべてが自動的に行われます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// refactAuto.cpp

#include <typeinfo>
#include <iostream>

int main(){
 
 std::cout << std::endl;

 auto a= 5;
 auto b= 10;
 auto sum= a * b * 3;
 auto res= sum + 10; 
 std::cout << "typeid(res).name(): " << typeid(res).name() << std::endl;
 
 auto a2= 5;
 auto b2= 10.5;
 auto sum2= a2 * b2 * 3;
 auto res2= sum2 * 10; 
 std::cout << "typeid(res2).name(): " << typeid(res2).name() << std::endl;
 
 auto a3= 5;
 auto b3= 10;
 auto sum3= a3 * b3 * 3.1f;
 auto res3= sum3 * 10; 
 std::cout << "typeid(res3).name(): " << typeid(res3).name() << std::endl;
 
 std::cout << std::endl;
 
}

コード スニペットの小さなバリエーションによって、常に res、res2、または res3 の正しいタイプが決まります。それがコンパイラの仕事です。 17 行目の変数 b2 は double 型であるため、res2 も型です。 24 行目の変数 sum3 は、float リテラル 3.1f との乗算により float 型になり、したがって最終結果 res3 にもなります。コンパイラから型を取得するには、ヘッダーの typeinfo で定義されている typeid 演算子を使用します。

ここでは、黄色地に黒色の結果が得られます。

感動?私もです。

次は?

中括弧 {} による初期化は、auto と多くの共通点があります。同様に頻繁に使用され、コードを読みやすくし、コードをより安全にします。どのように?次の投稿でお見せします。