5. Häufige Fallstricke bei der Verwendung von Arrays.
5.1 Fallstricke: Vertrauen in typsichere Verknüpfungen.
OK, Ihnen wurde gesagt oder Sie haben selbst herausgefunden, dass Globale (Namespace-Bereichsvariablen, auf die außerhalb der Übersetzungseinheit zugegriffen werden kann) Evil ™ sind. Aber wussten Sie, wie wirklich Evil ™ sie sind? Betrachten Sie das folgende Programm, das aus zwei Dateien [main.cpp] und [numbers.cpp] besteht:
// [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};
In Windows 7 wird dies gut mit MinGW g ++ 4.4.1 und Visual C ++ 10.0 kompiliert und verknüpft.
Da die Typen nicht übereinstimmen, stürzt das Programm ab, wenn Sie es ausführen.
In der formalen Erklärung: Das Programm hat Undefined Behaviour (UB), und anstatt abzustürzen, kann es einfach hängen bleiben oder vielleicht nichts tun, oder es kann bedrohliche E-Mails an die Präsidenten der USA, Russlands, Indiens, senden. China und die Schweiz, und lassen Sie Nasendämonen aus Ihrer Nase fliegen.
Erklärung in main.cpp
der Praxis: Das Array wird als Zeiger behandelt, der an derselben Adresse wie das Array platziert ist. Für eine ausführbare 32-Bit-Datei bedeutet dies, dass der erste
int
Wert im Array als Zeiger behandelt wird. Dh inmain.cpp
der
numbers
Variablen enthält oder scheint zu enthalten , (int*)1
. Dies führt dazu, dass das Programm ganz unten im Adressraum auf den Speicher zugreift, der herkömmlicherweise reserviert ist und Traps verursacht. Ergebnis: Sie bekommen einen Absturz.
Die Compiler haben das Recht, diesen Fehler nicht zu diagnostizieren, da C ++ 11 §3.5 / 10 über die Anforderung kompatibler Typen für die Deklarationen sagt:
[N3290 §3.5 / 10]
Ein Verstoß gegen diese Regel zur erfordert keine Diagnose.
Im selben Absatz wird die zulässige Variation beschrieben:
… Deklarationen für ein Array-Objekt können Array-Typen angeben, die sich durch das Vorhandensein oder Fehlen einer Haupt-Array-Bindung unterscheiden (8.3.4).
Diese zulässige Variation beinhaltet nicht die Deklaration eines Namens als Array in einer Übersetzungseinheit und als Zeiger in einer anderen Übersetzungseinheit.
5.2 Fallstricke: Vorzeitige Optimierung ( memset
& Freunde).
Noch nicht geschrieben
5.3 Fallstricke: Verwenden der C-Sprache, um die Anzahl der Elemente zu ermitteln.
Mit tiefer C-Erfahrung ist es natürlich zu schreiben…
#define N_ITEMS( array ) (sizeof( array )/sizeof( array[0] ))
Da ein array
Zeiger bei Bedarf auf das erste Element verweist, kann der Ausdruck sizeof(a)/sizeof(a[0])
auch als geschrieben werden
sizeof(a)/sizeof(*a)
. Es bedeutet dasselbe, und egal wie es geschrieben ist, es ist das C-Idiom zum Finden der Zahlenelemente des Arrays.
Hauptfalle: Das C-Idiom ist nicht typsicher. Zum Beispiel der Code…
#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 );
}
übergibt einen Zeiger auf N_ITEMS
und führt daher höchstwahrscheinlich zu einem falschen Ergebnis. Kompiliert als 32-Bit-ausführbare Datei in Windows 7 erzeugt es…
7 Elemente, Aufrufanzeige ...
1 Elemente.
- Der Compiler schreibt
int const a[7]
auf just umint const a[]
.
- Der Compiler schreibt neu
int const a[]
in int const* a
.
N_ITEMS
wird daher mit einem Zeiger aufgerufen.
- Für eine 32-Bit-ausführbare Datei
sizeof(array)
(Größe eines Zeigers) ist dann 4.
sizeof(*array)
ist äquivalent zu sizeof(int)
, was für eine 32-Bit-ausführbare Datei auch 4 ist.
Um diesen Fehler zur Laufzeit zu erkennen, können Sie Folgendes tun:
#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 Elemente, Aufrufanzeige ...
Behauptung fehlgeschlagen: ("N_ITEMS erfordert ein tatsächliches Array als Argument", Typ-ID (a)! = Typ-ID (& * a)), Datei runtime_detect ion.cpp, Zeile 16
Diese Anwendung hat die Runtime aufgefordert, sie auf ungewöhnliche Weise zu beenden.
Bitte wenden Sie sich an das Support-Team der Anwendung, um weitere Informationen zu erhalten.
Die Laufzeitfehlererkennung ist besser als keine Erkennung, verschwendet jedoch ein wenig Prozessorzeit und möglicherweise viel mehr Programmierzeit. Besser mit Erkennung zur Kompilierungszeit! Und wenn Sie glücklich sind, Arrays lokaler Typen mit C ++ 98 nicht zu unterstützen, können Sie dies tun:
#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 )
Beim Kompilieren dieser Definition, die mit g ++ in das erste vollständige Programm eingesetzt wurde, erhielt ich…
M: \ count> g ++ compile_time_detection.cpp
compile_time_detection.cpp: In der Funktion 'void display (const int *)':
compile_time_detection.cpp: 14: Fehler: Keine übereinstimmende Funktion für den Aufruf von 'n_items (const int * &)'
M: \ count> _
So funktioniert es: Das Array wird unter Bezugnahme auf übergebenn_items
und verfällt daher nicht in einen Zeiger auf das erste Element. Die Funktion kann nur die Anzahl der vom Typ angegebenen Elemente zurückgeben.
Mit C ++ 11 können Sie dies auch für Arrays vom lokalen Typ verwenden, und es ist die typsichere
C ++ - Sprache zum Ermitteln der Anzahl der Elemente eines Arrays.
5.4 Fallstricke in C ++ 11 und C ++ 14: Verwenden einer constexpr
Arraygrößenfunktion.
Mit C ++ 11 und höher ist es natürlich, aber wie Sie sehen werden, gefährlich, die C ++ 03-Funktion zu ersetzen
typedef ptrdiff_t Size;
template< class Type, Size n >
Size n_items( Type (&)[n] ) { return n; }
mit
using Size = ptrdiff_t;
template< class Type, Size n >
constexpr auto n_items( Type (&)[n] ) -> Size { return n; }
wobei die wesentliche Änderung die Verwendung von ist constexpr
, wodurch diese Funktion eine Kompilierungszeitkonstante erzeugen kann .
Im Gegensatz zur C ++ 03-Funktion kann eine solche Kompilierungszeitkonstante beispielsweise verwendet werden, um ein Array mit derselben Größe wie ein anderes zu deklarieren:
// 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.
}
Betrachten Sie diesen Code jedoch anhand der folgenden constexpr
Version:
// 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 );
}
Die Falle: Ab Juli 2015 werden die oben genannten Kompilierungen mit MinGW-64 5.1.0 mit -pedantic-errors
und beim Testen mit den Online-Compilern unter gcc.godbolt.org/ auch mit Clang 3.0 und Clang 3.2, jedoch nicht mit Clang 3.3, 3.4 kompiliert
. 1, 3.5.0, 3.5.1, 3.6 (rc1) oder 3.7 (experimentell). Und wichtig für die Windows-Plattform ist, dass sie nicht mit Visual C ++ 2015 kompiliert werden kann. Der Grund ist eine C ++ 11 / C ++ 14-Anweisung zur Verwendung von Referenzen inconstexpr
Ausdrücken:
C ++ 11 C ++ 14 $ 5.19 / 2 neun
th dash
Ein bedingter Ausdruck e
ist ein konstanter Kernausdruck, es sei denn, die Auswertung von e
nach den Regeln der abstrakten Maschine (1.9) würde einen der folgenden Ausdrücke
auswerten : ⋮
- Ein ID-Ausdruck , der auf eine Variable oder ein Datenelement des Referenztyps verweist, es sei denn, die Referenz hat eine vorhergehende Initialisierung und beides
- es wird mit einem konstanten Ausdruck oder initialisiert
- es ist ein nicht statisches Datenelement eines Objekts, dessen Lebensdauer mit der Auswertung von e begann;
Man kann immer ausführlicher schreiben
// 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
}
… Aber dies schlägt fehl, wenn Collection
es sich nicht um ein Raw-Array handelt.
Um mit Sammlungen umgehen zu können, die keine Arrays sein können, benötigt man die Überladbarkeit einer
n_items
Funktion, aber für die Kompilierungszeit benötigt man eine Kompilierungszeitdarstellung der Arraygröße. Und das klassische C ++ 03 - Lösung, die feine auch in C ++ gewohnt 11 und C ++ 14, ist die Funktion Bericht das Ergebnis nicht als Wert zu lassen , sondern über seine Funktionsergebnis Typ . Zum Beispiel so:
// 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 );
}
Informationen zur Auswahl des Rückgabetyps für static_n_items
: Dieser Code wird nicht verwendet, std::integral_constant
da std::integral_constant
das Ergebnis direkt als constexpr
Wert dargestellt wird, wodurch das ursprüngliche Problem wieder eingeführt wird. Anstelle einer Size_carrier
Klasse kann man die Funktion direkt einen Verweis auf ein Array zurückgeben lassen. Allerdings ist nicht jeder mit dieser Syntax vertraut.
Über die Benennung: Teil dieser Lösung zum constexpr
Informationen Problem, dass aufgrund der Referenz ungültig ist, besteht darin, die Auswahl der Kompilierungszeitkonstante explizit festzulegen.
Hoffentlich wird das constexpr
Problem, das an Ihrem Problem beteiligt war, mit C ++ 17 behoben, aber bis dahin bietet ein Makro wie das STATIC_N_ITEMS
oben beschriebene Portabilität, z. B. für die Clang- und Visual C ++ - Compiler, wobei der Typ beibehalten wird Sicherheit.
Verwandte Themen: Makros berücksichtigen keine Bereiche. Um Namenskollisionen zu vermeiden, empfiehlt es sich, ein Namenspräfix zu verwenden, z MYLIB_STATIC_N_ITEMS
.