5. Häufige Fallstricke bei der Verwendung von Arrays.
5.1 Fallstrick: Vertrauen in typunsichere Verknüpfungen.
OK, man hat Ihnen gesagt oder Sie haben es selbst herausgefunden, dass Globals (Namespace Bereichsvariablen, auf die außerhalb der Übersetzungseinheit zugegriffen werden kann) Evil™ sind. Aber wussten Sie, wie böse™ sie wirklich sind? Betrachten Sie das Programm, das aus zwei Dateien besteht [main.cpp] und [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};
Unter Windows 7 kompiliert und verknüpft es sich gut mit MinGW g++ 4.4.1 und Visual C++ 10.0.
Da die Typen nicht übereinstimmen, stürzt das Programm ab, wenn Sie es ausführen.
Die formale Erklärung: Das Programm hat Undefined Behavior (UB), und anstatt abstürzt, kann es sich daher einfach aufhängen, oder vielleicht nichts tun, oder es Droh-E-Mails an die Präsidenten der USA, Russlands, Indiens und der Schweiz schicken, China und der Schweiz schicken und Nasal Daemons aus Ihrer Nase fliegen lassen.
Erklärung aus der Praxis: in main.cpp
das Array wird als Zeiger behandelt und in auf dieselbe Adresse wie das Array. Für eine 32-Bit ausführbare Datei bedeutet dies, dass die erste int
Wert im Array, wird als Zeiger behandelt. D.h., in main.cpp
die numbers
Variable enthält oder zu enthalten scheint, (int*)1
. Dies bewirkt, dass das Programm auf den Speicher ganz unten im Adressraum zugreifen, der reserviert ist und zu Traps führt. Ergebnis: Sie erhalten einen Absturz.
Es ist das gute Recht der Compiler, diesen Fehler nicht zu diagnostizieren, denn C++11 §3.5/10 sagt über die Anforderung kompatibler Typen für die Deklarationen,
[N3290 §3.5/10]
Ein Verstoß gegen diese Regel zur Typenidentität erfordert keine Diagnose.
Im selben Absatz werden die zulässigen Abweichungen aufgeführt:
Deklarationen für ein Array-Objekt können Array-Typen angeben, die sich durch das Vorhandensein oder Nichtvorhandensein einer Haupt-Array-Bindung unterscheiden (8.3.4).
Diese erlaubte Variante beinhaltet nicht die Deklaration eines Namens als Array in einer Übersetzungseinheit und als Zeiger in einer anderen Übersetzungseinheit.
5.2 Fallstrick: Vorzeitige Optimierung ( memset
& Freunde).
Noch nicht geschrieben
5.3 Fallstrick: Die Verwendung des C-Idioms, um die Anzahl der Elemente zu ermitteln.
Mit umfassender C-Erfahrung ist es natürlich,
#define N_ITEMS( array ) (sizeof( array )/sizeof( array[0] ))
Da ein array
zerfällt zum Zeiger auf das erste Element, wenn es benötigt wird, die Ausdruck sizeof(a)/sizeof(a[0])
kann auch geschrieben werden als sizeof(a)/sizeof(*a)
. Es bedeutet dasselbe, und egal, wie es geschrieben wird geschrieben wird, ist es das C Idiom um die Anzahl der Elemente eines Arrays zu ermitteln.
Hauptfallstrick: 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 einer falschen Ergebnis. Kompiliert als ausführbare 32-Bit-Datei unter Windows 7 erzeugt es
7 Elemente, die Anzeige aufrufen...
1 Elemente.
- Der Compiler schreibt neu
int const a[7]
um einfach int const a[]
.
- Der Compiler schreibt neu
int const a[]
zu int const* a
.
N_ITEMS
wird daher mit einem Zeiger aufgerufen.
- Für eine ausführbare 32-Bit-Datei
sizeof(array)
(Größe eines Zeigers) ist dann 4.
sizeof(*array)
ist gleichbedeutend mit sizeof(int)
was für eine ausführbare 32-Bit-Datei ebenfalls 4 ist.
Um diesen Fehler während der Laufzeit zu erkennen, können Sie
#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, die Anzeige aufrufen...
Assertion fehlgeschlagen: ("N_ITEMS erfordert ein aktuelles Array als Argument", typeid( a ) != typeid( &*a ) ), Datei runtime_detect ion.cpp, Zeile 16
Diese Anwendung hat die Laufzeitumgebung aufgefordert, sie auf eine ungewöhnliche Weise zu beenden.
Für weitere Informationen wenden Sie sich bitte an das Support-Team der Anwendung.
Die Erkennung von Laufzeitfehlern ist besser als keine Erkennung, aber sie verschwendet ein wenig Prozessorzeit und vielleicht viel mehr Zeit des Programmierers. Besser mit Erkennung zur Kompilierzeit! Und wenn Sie froh sind, dass Sie mit C++98 keine Arrays lokaler Typen unterstützen, dann können Sie das 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 )
Kompilieren dieser Definition in das erste vollständige Programm ersetzt, mit g++, erhalte 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 passende Funktion für den Aufruf von 'n_items(const int*&)'
M:\count > _
So funktioniert es: Das Array wird übergeben durch Verweis zu n_items
und das tut sie auch nicht auf den Zeiger auf das erste Element abklingen, und die Funktion kann einfach den Anzahl von Elementen zurückgeben, die durch den Typ angegeben ist.
Mit C++11 können Sie dies auch für Arrays vom lokalen Typ verwenden, und es ist die typsichere C++ Idiom um die Anzahl der Elemente eines Arrays zu ermitteln.
5.4 C++11 & C++14 Fallstrick: Die Verwendung einer constexpr
Array-Größenfunktion.
Mit C++11 und später 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 in der Verwendung von constexpr
die es ermöglicht diese Funktion eine Kompilierzeitkonstante .
Im Gegensatz zur C++03-Funktion ist eine solche Kompilierzeitkonstante zum Beispiel verwendet werden, um ein Array mit der gleichen 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.
}
Aber betrachten Sie diesen Code unter Verwendung der 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 );
}
Der Fallstrick: ab Juli 2015 kompiliert das oben genannte mit MinGW-64 5.1.0 mit -pedantic-errors
, und, Tests mit den Online-Compilern unter gcc.godbolt.org/ auch mit clang 3.0 und clang 3.2, aber nicht mit clang 3.3, 3.4.1, 3.5.0, 3.5.1, 3.6 (rc1) oder 3.7 (experimentell). Und wichtig für die Windows-Plattform, es kompiliert nicht mit Visual C++ 2015. Der Grund dafür ist eine C++11/C++14-Anweisung über die Verwendung von Referenzen in constexpr
Ausdrücke:
C++11 C++14 $5.19/2 neun th Bindestrich
A bedingter-Ausdruck e
ist eine Kernkonstante es sei denn, die Bewertung von e
würde nach den Regeln der abstrakten Maschine (1.9) einen der folgenden Ausdrücke auswerten folgenden Ausdrücke auswerten:
- eine id-Ausdruck die sich auf eine Variable oder ein Datenelement vom Referenztyp verweist, es sei denn, die Referenz hat eine vorangehende Initialisierung und entweder
- es wird mit einem konstanten Ausdruck initialisiert oder
- es ist ein nicht statisches Datenelement eines Objekts, dessen Lebensdauer innerhalb von der Auswertung von e begann;
Man kann immer die ausführlichere
// 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 das schlägt fehl, wenn Collection
ist kein rohes Array.
Um mit Sammlungen umzugehen, die keine Arrays sein können, braucht man die Überladbarkeit einer n_items
Funktion, aber auch für die Verwendung zur Kompilierzeit benötigt man eine Kompilierzeit Darstellung der Array-Größe. Die klassische C++03-Lösung, die auch in C++11 und C++14 gut funktioniert auch in C++11 und C++14 funktioniert, ist, dass die Funktion ihr Ergebnis nicht als Wert sondern über ihr 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 );
}
Über die Wahl der Rückgabeart für static_n_items
: Dieser Code verwendet nicht std::integral_constant
denn mit std::integral_constant
wird das Ergebnis dargestellt direkt als constexpr
Wert, wodurch das ursprüngliche Problem wieder auftritt. Stattdessen einer Size_carrier
Klasse kann man die Funktion direkt ein Referenz auf ein Array zurückgeben. Allerdings ist nicht jeder mit dieser Syntax vertraut.
Zur Namensgebung: Teil dieser Lösung für das Problem der constexpr
-ungültige-due-to-reference Problem ist es, die Wahl der Kompilierzeitkonstante explizit zu machen.
Hoffentlich ist die "Hoppla, da war ein Verweis im Spiel"-Geschichte constexpr
Problem wird behoben mit C++17 behoben werden, aber bis dahin kann ein Makro wie das STATIC_N_ITEMS
ergibt die Übertragbarkeit, z. B. zu den Compilern von Clang und Visual C++, wobei die Typsicherheit erhalten bleibt.
Zum Thema: Makros respektieren keine Geltungsbereiche; um Namenskollisionen zu vermeiden, kann es also eine eine gute Idee sein, ein Namenspräfix zu verwenden, z. B. MYLIB_STATIC_N_ITEMS
.