5. Insidie comuni quando si usano le matrici.
5.1 Insidie: fidarsi dei collegamenti non sicuri di tipo.
OK, ti è stato detto, o hai scoperto te stesso, che i globali (variabili dell'ambito dello spazio dei nomi a cui è possibile accedere all'esterno dell'unità di traduzione) sono Evil ™. Ma sapevi quanto sono veramente cattivi? Considera il seguente programma, costituito da due file [main.cpp] e [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};
In Windows 7 questo compila e collega bene sia con MinGW g ++ 4.4.1 che con Visual C ++ 10.0.
Poiché i tipi non corrispondono, il programma si arresta in modo anomalo quando lo si esegue.
Spiegazione formale: il programma ha Undefined Behaviour (UB), e invece di bloccarsi può quindi semplicemente bloccarsi, o forse non fare nulla, oppure può inviare e-mail minacciose ai presidenti degli Stati Uniti, Russia, India, La Cina e la Svizzera e fai volare i demoni nasali dal tuo naso.
Spiegazione pratica: main.cpp
nell'array viene trattato come un puntatore, posizionato allo stesso indirizzo dell'array. Per eseguibile a 32 bit ciò significa che il primo
int
valore nell'array viene trattato come un puntatore. Vale a dire, nel main.cpp
la
numbers
variabile contiene, o sembra contenere, (int*)1
. Questo fa sì che il programma acceda alla memoria in fondo allo spazio degli indirizzi, che è convenzionalmente riservato e causa trap. Risultato: si ottiene un arresto anomalo.
I compilatori hanno pienamente i loro diritti di non diagnosticare questo errore, poiché C ++ 11 §3.5 / 10 dice, sul requisito dei tipi compatibili per le dichiarazioni,
[N3290 §3.5 / 10]
Una violazione di questa regola sull'identità del tipo non richiede una diagnostica.
Lo stesso paragrafo descrive in dettaglio la variazione consentita:
… Le dichiarazioni per un oggetto array possono specificare tipi di array che differiscono per la presenza o l'assenza di un limite dell'array principale (8.3.4).
Questa variazione consentita non include la dichiarazione di un nome come matrice in un'unità di traduzione e come puntatore in un'altra unità di traduzione.
5.2 Trabocchetto: ottimizzazione prematura (memset
e amici).
Non ancora scritto
5.3 Trappola: usare il linguaggio C per ottenere il numero di elementi.
Con una profonda esperienza in C è naturale scrivere ...
#define N_ITEMS( array ) (sizeof( array )/sizeof( array[0] ))
Poiché un array
decadimento punta al primo elemento dove necessario, l'espressione sizeof(a)/sizeof(a[0])
può anche essere scritta come
sizeof(a)/sizeof(*a)
. Significa lo stesso, e non importa come sia scritto, è il linguaggio C per trovare gli elementi numerici dell'array.
Trabocchetto principale: il linguaggio C non è dilemma. Ad esempio, il codice ...
#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 );
}
passa un puntatore a N_ITEMS
, e quindi molto probabilmente produce un risultato sbagliato. Compilato come eseguibile a 32 bit in Windows 7, produce ...
7 elementi, chiamata display ...
1 elementi.
- Il compilatore riscrive
int const a[7]
solo int const a[]
.
- Il compilatore riscrive
int const a[]
inint const* a
.
N_ITEMS
viene quindi invocato con un puntatore.
- Per un eseguibile a 32 bit
sizeof(array)
(dimensione di un puntatore) è quindi 4.
sizeof(*array)
è equivalente a sizeof(int)
, che per un eseguibile a 32 bit è anche 4.
Per rilevare questo errore in fase di esecuzione puoi fare ...
#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 elementi, chiamata display ...
Asserzione non riuscita: ("N_ITEMS richiede un array effettivo come argomento", typeid (a)! = Typeid (& * a)), file runtime_detect ion.cpp, riga 16
Questa applicazione ha richiesto a Runtime di terminarlo in modo insolito.
Per ulteriori informazioni, contattare il team di supporto dell'applicazione.
Il rilevamento degli errori di runtime è meglio di nessun rilevamento, ma spreca un po 'di tempo del processore e forse molto più tempo del programmatore. Meglio con il rilevamento in fase di compilazione! E se sei felice di non supportare array di tipi locali con C ++ 98, puoi farlo:
#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 )
Compilando questa definizione sostituita nel primo programma completo, con g ++, ho ottenuto ...
M: \ count> g ++ compile_time_detection.cpp
compile_time_detection.cpp: nella funzione 'void display (const int *)':
compile_time_detection.cpp: 14: errore: nessuna funzione corrispondente per la chiamata a 'n_items (const int * &)'
M: \ count> _
Come funziona: la matrice viene passata con riferimento allan_items
, e quindi non decade al puntatore al primo elemento, e la funzione può semplicemente restituire il numero di elementi specificati dal tipo.
Con C ++ 11 puoi usarlo anche per array di tipo locale, ed è il linguaggio sicuro C ++ di tipo
per trovare il numero di elementi di un array.
5.4 Trabocchetto C ++ 11 e C ++ 14: utilizzo di una constexpr
funzione di dimensione dell'array.
Con C ++ 11 e versioni successive è naturale, ma come vedrai pericoloso !, sostituire la funzione C ++ 03
typedef ptrdiff_t Size;
template< class Type, Size n >
Size n_items( Type (&)[n] ) { return n; }
con
using Size = ptrdiff_t;
template< class Type, Size n >
constexpr auto n_items( Type (&)[n] ) -> Size { return n; }
dove il cambiamento significativo è l'uso di constexpr
, che consente a questa funzione di produrre una costante di tempo di compilazione .
Ad esempio, contrariamente alla funzione C ++ 03, una tale costante di tempo di compilazione può essere utilizzata per dichiarare un array della stessa dimensione di un altro:
// 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.
}
Ma considera questo codice usando la constexpr
versione:
// 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 );
}
La trappola: a partire da luglio 2015 il precedente si compila con MinGW-64 5.1.0 con
-pedantic-errors
e, testando con i compilatori online su gcc.godbolt.org/ , anche con clang 3.0 e clang 3.2, ma non con clang 3.3, 3.4. 1, 3.5.0, 3.5.1, 3.6 (rc1) o 3.7 (sperimentale). E importante per la piattaforma Windows, non si compila con Visual C ++ 2015. Il motivo è un'istruzione C ++ 11 / C ++ 14 sull'uso dei riferimenti nelle constexpr
espressioni:
C ++ 11 C ++ 14 $ 5.19 / 2 nove
° trattino
Un condizionale espressione e
è un'espressione costante nucleo a meno che la valutazione e
, secondo le regole della macchina astratta (1.9), sarebbe valutare una delle seguenti espressioni:
⋮
- un id-espressione che si riferisce ad un elemento variabile o dati di riferimento di tipo a meno che il riferimento ha un'inizializzazione precedente e sia
- è inizializzato con un'espressione costante o
- è un membro di dati non statico di un oggetto la cui durata è iniziata nell'ambito della valutazione di e;
Si può sempre scrivere il più dettagliato
// 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
}
... ma questo non riesce quando Collection
non è un array grezzo.
Per gestire raccolte che possono essere non array, è necessaria la sovraccaricabilità di una
n_items
funzione, ma anche, per un utilizzo in fase di compilazione, è necessaria una rappresentazione in fase di compilazione della dimensione dell'array. E la classica soluzione C ++ 03, che funziona bene anche in C ++ 11 e C ++ 14, è di lasciare che la funzione riporti il suo risultato non come un valore ma tramite il suo tipo di risultato della funzione . Ad esempio in questo modo:
// 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 );
}
Informazioni sulla scelta del tipo restituito per static_n_items
: questo codice non viene utilizzato std::integral_constant
perché con std::integral_constant
il risultato viene rappresentato direttamente come constexpr
valore, reintroducendo il problema originale. Invece di una Size_carrier
classe si può lasciare che la funzione restituisca direttamente un riferimento a un array. Tuttavia, non tutti hanno familiarità con questa sintassi.
Informazioni sulla denominazione: parte di questa soluzione al problema constexpr
-invalid-due-to-reference è rendere esplicita la scelta del tempo di compilazione.
Si spera che il constexpr
problema relativo al problema sia stato risolto con C ++ 17, ma fino ad allora una macro come quella STATIC_N_ITEMS
sopra produce portabilità, ad esempio per i compilatori clang e Visual C ++, mantenendo il tipo sicurezza.
Correlati: le macro non rispettano gli ambiti, quindi per evitare le collisioni di nomi può essere una buona idea usare un prefisso di nome, ad es MYLIB_STATIC_N_ITEMS
.