5.配列を使用する際の一般的な落とし穴。
5.1落とし穴:信頼できない型安全でないリンク。
OK、グローバル(翻訳単位の外部からアクセスできる名前空間スコープ変数)はEvil™であると言われました。しかし、それらがどれほど真にEvil™であるかを知っていましたか?2つのファイル[main.cpp]と[numbers.cpp]で構成される以下のプログラムを考えてみます。
// [main.cpp]
#include <iostream>
extern int* numbers;
int main()
{
using namespace std;
for( int i = 0; i < 42; ++i )
{
cout << (i > 0? ", " : "") << numbers[i];
}
cout << endl;
}
// [numbers.cpp]
int numbers[42] = {1, 2, 3, 4, 5, 6, 7, 8, 9};
Windows 7では、これはMinGW g ++ 4.4.1およびVisual C ++ 10.0の両方でコンパイルおよびリンクします。
タイプが一致しないため、実行するとプログラムがクラッシュします。
正式な説明:プログラムには未定義の動作(UB)があり、クラッシュする代わりにハングするか、おそらく何もしないか、または脅迫メールをアメリカ、ロシア、インドの大統領に送信することができます。中国とスイス、そして鼻のデーモンを鼻から飛ばします。
実際の説明:main.cpp
配列では、配列と同じアドレスに配置されたポインターとして扱われます。32ビット実行可能ファイルのint
場合、これは配列の最初の値がポインターとして扱われることを意味し
ます。すなわち、中に変数が含まれ、あるいは含まれているように見えます。これにより、プログラムはアドレス空間の最下部にあるメモリにアクセスします。これは、従来は予約されており、トラップを引き起こしていました。結果:クラッシュします。main.cpp
numbers
(int*)1
C ++ 11§3.5/ 10は、宣言に互換性のある型の要件について述べているため、コンパイラーはこのエラーを診断しない権利を完全に有しています。
[N3290§3.5/ 10]
タイプアイデンティティに関するこのルールの違反は、診断を必要としません。
同じ段落で、許可されるバリエーションについて詳しく説明しています。
…配列オブジェクトの宣言では、主要な配列境界(8.3.4)の有無によって異なる配列型を指定できます。
この許可されたバリエーションには、名前を1つの変換単位の配列として、および別の変換単位のポインターとして宣言することは含まれません。
5.2落とし穴:時期尚早の最適化(memset
とその仲間)。
まだ書かれていない
5.3落とし穴:Cイディオムを使用して要素の数を取得する。
深いCの経験があれば、書くのは当然です…
#define N_ITEMS( array ) (sizeof( array )/sizeof( array[0] ))
array
必要に応じて最初の要素へのポインタへの減衰があるので、式sizeof(a)/sizeof(a[0])
はと書くこともできます
sizeof(a)/sizeof(*a)
。それは同じことを意味し、どのように記述されていても、配列の数値要素を見つけるためのCイディオムです。
主な落とし穴:Cイディオムはタイプセーフではありません。たとえば、コード…
#include <stdio.h>
#define N_ITEMS( array ) (sizeof( array )/sizeof( *array ))
void display( int const a[7] )
{
int const n = N_ITEMS( a ); // Oops.
printf( "%d elements.\n", n );
}
int main()
{
int const moohaha[] = {1, 2, 3, 4, 5, 6, 7};
printf( "%d elements, calling display...\n", N_ITEMS( moohaha ) );
display( moohaha );
}
へのポインタを渡すためN_ITEMS
、おそらく誤った結果が生成されます。Windows 7で32ビットの実行可能ファイルとしてコンパイルされると、…が生成されます。
7つの要素、displayを呼び出す...
1つの要素。
- コンパイラは書き換えて
int const a[7]
ばかりにint const a[]
。
- コンパイラは書き換えて
int const a[]
までint const* a
。
N_ITEMS
したがって、ポインタで呼び出されます。
- 32ビットの実行可能ファイルの場合
sizeof(array)
(ポインタのサイズ)は4です。
sizeof(*array)
sizeof(int)
32ビット実行可能ファイルの場合も4 と同じです。
実行時にこのエラーを検出するために、次のことができます...
#include <assert.h>
#include <typeinfo>
#define N_ITEMS( array ) ( \
assert(( \
"N_ITEMS requires an actual array as argument", \
typeid( array ) != typeid( &*array ) \
)), \
sizeof( array )/sizeof( *array ) \
)
7つの要素、displayを呼び出しています...
アサーションに失敗しました:( "N_ITEMSには引数として実際の配列が必要です"、typeid(a)!= typeid(&* a))、ファイルruntime_detect ion.cpp、16行目
このアプリケーションは、ランタイムに異常な方法で終了するように要求しました。
詳細については、アプリケーションのサポートチームにお問い合わせください。
実行時エラーの検出は、検出しないよりも優れていますが、プロセッサー時間を少しだけ浪費し、おそらくプログラマーの時間をはるかに浪費します。コンパイル時の検出でより良い!C ++ 98でローカルタイプの配列をサポートしないことに満足している場合は、次のようにできます。
#include <stddef.h>
typedef ptrdiff_t Size;
template< class Type, Size n >
Size n_items( Type (&)[n] ) { return n; }
#define N_ITEMS( array ) n_items( array )
この定義を最初の完全なプログラムに置き換えて、g ++でコンパイルすると、…
M:\ count> g ++ compile_time_detection.cpp
compile_time_detection.cpp:関数 'void display(const int *)':
compile_time_detection.cpp:14:error: 'n_items(const int *&)'の呼び出しに対応する関数がありません
M:\ count> _
仕組み:配列はへの参照によって渡されるためn_items
、最初の要素へのポインターに減衰せず、関数は型で指定された要素の数を返すだけです。
C ++ 11では、これをローカルタイプの配列にも使用できます。これは、配列の要素数を見つけるためのタイプセーフな
C ++イディオムです。
5.4 C ++ 11&C ++ 14の落とし穴:constexpr
配列サイズ関数の使用。
C ++ 11以降では当然のことですが、危険だとわかるように、C ++ 03関数を置き換える
typedef ptrdiff_t Size;
template< class Type, Size n >
Size n_items( Type (&)[n] ) { return n; }
と
using Size = ptrdiff_t;
template< class Type, Size n >
constexpr auto n_items( Type (&)[n] ) -> Size { return n; }
ここで重要な変更はの使用ですconstexpr
。これにより、この関数でコンパイル時定数を生成できます。
たとえば、C ++ 03関数とは異なり、このようなコンパイル時定数を使用して、同じサイズの配列を別の配列と宣言できます。
// Example 1
void foo()
{
int const x[] = {3, 1, 4, 1, 5, 9, 2, 6, 5, 4};
constexpr Size n = n_items( x );
int y[n] = {};
// Using y here.
}
しかし、constexpr
バージョンを使用してこのコードを検討してください:
// Example 2
template< class Collection >
void foo( Collection const& c )
{
constexpr int n = n_items( c ); // Not in C++14!
// Use c here
}
auto main() -> int
{
int x[42];
foo( x );
}
落とし穴:2015年7月の時点で、上記はMinGW-64 5.1.0を
-pedantic-errors
使用してコンパイルし、gcc.godbolt.org /のオンラインコンパイラを使用してテストします。また、clang 3.0およびclang 3.2を使用しますが、clang 3.3、3.4は使用しません。 1、3.5.0、3.5.1、3.6(rc1)または3.7(実験的)。Windowsプラットフォームにとって重要であり、Visual C ++ 2015でコンパイルされません。理由は、constexpr
式での参照の使用に関するC ++ 11 / C ++ 14ステートメントです。
C ++ 11 C ++ 14 $ 5.19 / 2 9
番目のダッシュ
条件式で e
あるコア定数式の評価ない限り、e
以下の式のいずれかを評価するであろう、抽象機械(1.9)の規則に従って、:
⋮
- 参照に先行する初期化と以下のいずれかがない限り、参照タイプの変数またはデータメンバーを参照
するid式
- 定数式で初期化されている、または
- 有効期間がeの評価内で始まったオブジェクトの非静的データメンバーです。
いつでももっと冗長に書くことができます
// Example 3 -- limited
using Size = ptrdiff_t;
template< class Collection >
void foo( Collection const& c )
{
constexpr Size n = std::extent< decltype( c ) >::value;
// Use c here
}
…がCollection
生の配列でない場合、これは失敗します。
配列ではない可能性のあるコレクションを処理するには、n_items
関数のオーバーロード
機能が必要ですが、コンパイル時は、配列サイズのコンパイル時の表現が必要です。また、C ++ 11およびC ++ 14でも正常に機能する従来のC ++ 03ソリューションは、関数に結果を値としてではなく、関数の結果の型を介して報告させることです。たとえば、次のようになります。
// Example 4 - OK (not ideal, but portable and safe)
#include <array>
#include <stddef.h>
using Size = ptrdiff_t;
template< Size n >
struct Size_carrier
{
char sizer[n];
};
template< class Type, Size n >
auto static_n_items( Type (&)[n] )
-> Size_carrier<n>;
// No implementation, is used only at compile time.
template< class Type, size_t n > // size_t for g++
auto static_n_items( std::array<Type, n> const& )
-> Size_carrier<n>;
// No implementation, is used only at compile time.
#define STATIC_N_ITEMS( c ) \
static_cast<Size>( sizeof( static_n_items( c ).sizer ) )
template< class Collection >
void foo( Collection const& c )
{
constexpr Size n = STATIC_N_ITEMS( c );
// Use c here
(void) c;
}
auto main() -> int
{
int x[42];
std::array<int, 43> y;
foo( x );
foo( y );
}
戻り値の型の選択についてstatic_n_items
このコードは使用しませんstd::integral_constant
でので、std::integral_constant
その結果を直接として表現されconstexpr
、元の問題を再導入、値。Size_carrier
クラスの代わりに、関数が配列への参照を直接返すようにすることができます。ただし、誰もがその構文に精通しているわけではありません。
命名について:constexpr
-invalid-due-to-reference問題に対するこのソリューションの一部は、コンパイル時定数の選択を明示的にすることです。
うまくいけば、oops-there-was-a-reference-involved-in-your-のconstexpr
問題はC ++ 17で修正されますが、それまでは、STATIC_N_ITEMS
上記のようなマクロによって移植性が得られます。安全性。
関連:マクロはスコープを尊重しないため、名前の衝突を回避するために、名前の接頭辞を使用することをお勧めしますMYLIB_STATIC_N_ITEMS
。