5. Errores comunes al usar matrices.
5.1 Peligro: Confiando en el tipo de enlace inseguro.
Bien, le han dicho, o se ha enterado usted mismo, que los globales (variables de alcance del espacio de nombres a las que se puede acceder fuera de la unidad de traducción) son Evil ™. ¿Pero sabías cuán verdaderamente malvados son? Considere el siguiente programa, que consta de dos archivos [main.cpp] y [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};
En Windows 7 esto compila y enlaza bien tanto con MinGW g ++ 4.4.1 como con Visual C ++ 10.0.
Como los tipos no coinciden, el programa se bloquea cuando lo ejecuta.
Explicación formal: el programa tiene Comportamiento Indefinido (UB), y en lugar de fallar, por lo tanto, simplemente puede colgar, o tal vez no hacer nada, o puede enviar correos electrónicos amenazantes a los presidentes de los EE. UU., Rusia, India, China y Suiza, y haz que los Demonios Nasales salgan volando de tu nariz.
Explicación en main.cpp
la práctica: en la matriz se trata como un puntero, ubicado en la misma dirección que la matriz. Para el ejecutable de 32 bits, esto significa que el primer
int
valor de la matriz se trata como un puntero. Es decir, en main.cpp
la
numbers
variable contiene, o parece contener, (int*)1
. Esto hace que el programa acceda a la memoria en la parte inferior del espacio de direcciones, que está convencionalmente reservado y causa trampa. Resultado: tienes un accidente.
Los compiladores tienen pleno derecho a no diagnosticar este error, porque C ++ 11 §3.5 / 10 dice, sobre el requisito de tipos compatibles para las declaraciones,
[N3290 §3.5 / 10]
Una violación de esta regla sobre la identidad de tipo no requiere un diagnóstico.
El mismo párrafo detalla la variación permitida:
… Las declaraciones para un objeto de matriz pueden especificar tipos de matriz que difieren por la presencia o ausencia de un límite de matriz principal (8.3.4).
Esta variación permitida no incluye declarar un nombre como una matriz en una unidad de traducción y como un puntero en otra unidad de traducción.
5.2 Peligro: Hacer optimización prematura ( memset
y amigos).
Aún no escrito
5.3 Dificultad: usar el lenguaje C para obtener el número de elementos.
Con experiencia profunda en C es natural escribir ...
#define N_ITEMS( array ) (sizeof( array )/sizeof( array[0] ))
Como un array
decaimiento del puntero al primer elemento donde sea necesario, la expresión sizeof(a)/sizeof(a[0])
también se puede escribir como
sizeof(a)/sizeof(*a)
. Significa lo mismo, y no importa cómo esté escrito, es el idioma C para encontrar los elementos numéricos de la matriz.
Principal escollo: el idioma C no es seguro. Por ejemplo, el código ...
#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 );
}
pasa un puntero a N_ITEMS
y, por lo tanto, lo más probable es que produzca un resultado incorrecto. Compilado como un ejecutable de 32 bits en Windows 7 produce ...
7 elementos, pantalla de llamada ...
1 elementos.
- El compilador reescribe
int const a[7]
a just int const a[]
.
- El compilador reescribe
int const a[]
a int const* a
.
N_ITEMS
Por lo tanto, se invoca con un puntero.
- Para un ejecutable de 32 bits
sizeof(array)
(tamaño de un puntero) es entonces 4.
sizeof(*array)
es equivalente a sizeof(int)
, que para un ejecutable de 32 bits también es 4.
Para detectar este error en tiempo de ejecución, puede hacer ...
#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 elementos, llamando a la pantalla ...
Error de aserción: ("N_ITEMS requiere una matriz real como argumento", typeid (a)! = Typeid (& * a)), archivo runtime_detect ion.cpp, línea 16
Esta aplicación ha solicitado el tiempo de ejecución para terminarlo de una manera inusual.
Póngase en contacto con el equipo de soporte de la aplicación para obtener más información.
La detección de errores en tiempo de ejecución es mejor que la ausencia de detección, pero desperdicia un poco de tiempo de procesador y quizás mucho más tiempo de programador. ¡Mejor con detección en tiempo de compilación! Y si está contento de no admitir matrices de tipos locales con C ++ 98, puede hacerlo:
#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 esta definición sustituida en el primer programa completo, con g ++, obtuve ...
M: \ count> g ++ compile_time_detection.cpp
compile_time_detection.cpp: En la función 'void display (const int *)':
compile_time_detection.cpp: 14: error: no hay función coincidente para la llamada a 'n_items (const int * &)'
M: \ cuenta> _
Cómo funciona: la matriz se pasa por referencia a n_items
, y por lo tanto no decae al puntero al primer elemento, y la función solo puede devolver el número de elementos especificados por el tipo.
Con C ++ 11 puede usar esto también para matrices de tipo local, y es el idioma seguro de
C ++ para encontrar el número de elementos de una matriz.
5.4 Peligro de C ++ 11 y C ++ 14: uso de una constexpr
función de tamaño de matriz
Con C ++ 11 y versiones posteriores es natural, ¡pero como verás peligroso !, reemplazar la función 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; }
donde el cambio significativo es el uso de constexpr
, que permite que esta función produzca una constante de tiempo de compilación .
Por ejemplo, en contraste con la función C ++ 03, dicha constante de tiempo de compilación se puede usar para declarar una matriz del mismo tamaño que otra:
// 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.
}
Pero considere este código usando la constexpr
versión:
// 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 trampa: a partir de julio de 2015, lo anterior se compila con MinGW-64 5.1.0 con
-pedantic-errors
, y, probando con los compiladores en línea en gcc.godbolt.org/ , también con clang 3.0 y clang 3.2, pero no con clang 3.3, 3.4. 1, 3.5.0, 3.5.1, 3.6 (rc1) o 3.7 (experimental). E importante para la plataforma Windows, no se compila con Visual C ++ 2015. La razón es una declaración C ++ 11 / C ++ 14 sobre el uso de referencias en constexpr
expresiones:
C ++ 11 ++ 14 C $ 5.19 / 2 nueve
º tablero
Una expresión condicional e
es una expresión constante central a menos que la evaluación de e
, siguiendo las reglas de la máquina abstracta (1.9), evalúe una de las siguientes expresiones:
⋮
- una expresión de identificación que se refiere a una variable o miembro de datos de tipo de referencia a menos que la referencia tenga una inicialización anterior y
- se inicializa con una expresión constante o
- es un miembro de datos no estático de un objeto cuya vida útil comenzó dentro de la evaluación de e;
Uno siempre puede escribir más detallado
// 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
}
... pero esto falla cuando Collection
no es una matriz en bruto.
Para manejar colecciones que pueden ser no matrices, se necesita la capacidad de sobrecarga de una
n_items
función, pero también, para el tiempo de compilación, se necesita una representación en tiempo de compilación del tamaño de la matriz. Y la solución clásica de C ++ 03, que funciona bien también en C ++ 11 y C ++ 14, es dejar que la función informe su resultado no como un valor sino a través de su tipo de resultado de función . Por ejemplo así:
// 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 );
}
Acerca de la elección del tipo de retorno para static_n_items
: este código no se usa std::integral_constant
porque con std::integral_constant
el resultado se representa directamente como un constexpr
valor, reintroduciendo el problema original. En lugar de una Size_carrier
clase, se puede dejar que la función devuelva directamente una referencia a una matriz. Sin embargo, no todos están familiarizados con esa sintaxis.
Acerca de la nomenclatura: parte de esta solución al problema constexpr
-invalido-debido a una referencia es hacer explícita la elección del tiempo de compilación constante.
Afortunadamente, oops-there-was-a-reference-implicate-in-your- constexpr
issue se solucionará con C ++ 17, pero hasta entonces una macro como la STATIC_N_ITEMS
anterior proporciona portabilidad, por ejemplo, a los compiladores clang y Visual C ++, conservando el tipo la seguridad.
Relacionado: las macros no respetan los ámbitos, por lo que para evitar colisiones de nombres puede ser una buena idea usar un prefijo de nombre, por ejemplo MYLIB_STATIC_N_ITEMS
.