5. Armadilhas comuns ao usar matrizes.
5.1 Armadilha: Confiando em links não seguros do tipo.
OK, você foi informado ou descobriu que globals (variáveis de escopo de namespace que podem ser acessadas fora da unidade de tradução) são Evil ™. Mas você sabia como eles são realmente maus? Considere o programa abaixo, composto por dois arquivos [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};
No Windows 7, isso compila e vincula bem ao MinGW g ++ 4.4.1 e ao Visual C ++ 10.0.
Como os tipos não correspondem, o programa falha quando você o executa.
Explicação formal: o programa tem comportamento indefinido (UB) e, em vez de travar, pode simplesmente travar, ou talvez não fazer nada, ou enviar e-mails ameaçadores aos presidentes dos EUA, Rússia, Índia, China e Suíça, e faça com que os Daemons Nasais voem pelo nariz.
Explicação na prática: na main.cpp
matriz é tratada como um ponteiro, colocado no mesmo endereço da matriz. Para executável de 32 bits, isso significa que o primeiro
int
valor na matriz é tratado como um ponteiro. Isto é, main.cpp
na
numbers
variável contém, ou parece conter, (int*)1
. Isso faz com que o programa acesse a memória na parte inferior do espaço de endereço, que é convencionalmente reservado e causa trapping. Resultado: você recebe um acidente.
Os compiladores têm pleno direito de não diagnosticar esse erro, porque C ++ 11 §3.5 / 10 diz sobre o requisito de tipos compatíveis para as declarações,
[N3290 §3.5 / 10]
A violação desta regra sobre identidade de tipo não requer diagnóstico.
O mesmo parágrafo detalha a variação permitida:
… As declarações para um objeto de matriz podem especificar tipos de matriz que diferem pela presença ou ausência de um limite de matriz principal (8.3.4).
Essa variação permitida não inclui declarar um nome como uma matriz em uma unidade de tradução e como um ponteiro em outra unidade de tradução.
5.2 Armadilha: Fazendo otimização prematura ( memset
e amigos).
Ainda não escrito
5.3 Armadilha: Usando o idioma C para obter o número de elementos.
Com profunda experiência em C, é natural escrever…
#define N_ITEMS( array ) (sizeof( array )/sizeof( array[0] ))
Como um array
decaimento para apontar para o primeiro elemento, quando necessário, a expressão sizeof(a)/sizeof(a[0])
também pode ser escrita como
sizeof(a)/sizeof(*a)
. Significa o mesmo, e não importa como está escrito, é o idioma C para encontrar os elementos numéricos da matriz.
Armadilha principal: o idioma C não é seguro. Por exemplo, o 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 );
}
passa um ponteiro para N_ITEMS
e, portanto, provavelmente produz um resultado errado. Compilado como um executável de 32 bits no Windows 7, produz…
7 elementos, chamando a exibição ...
1 elementos.
- O compilador reescreve
int const a[7]
para apenas int const a[]
.
- O compilador reescreve
int const a[]
para int const* a
.
N_ITEMS
é, portanto, invocado com um ponteiro.
- Para um executável de 32 bits
sizeof(array)
(tamanho de um ponteiro) é então 4.
sizeof(*array)
é equivalente a sizeof(int)
, que para um executável de 32 bits também é 4.
Para detectar esse erro no tempo de execução, você pode…
#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, chamando a exibição ...
Falha na asserção: ("N_ITEMS requer uma matriz real como argumento", typeid (a)! = Typeid (& * a)), arquivo runtime_detect ion.cpp, linha 16
Este aplicativo solicitou que o Runtime o encerrasse de uma maneira incomum.
Entre em contato com a equipe de suporte do aplicativo para obter mais informações.
A detecção de erros em tempo de execução é melhor do que nenhuma detecção, mas desperdiça um pouco de tempo do processador e talvez muito mais tempo do programador. Melhor com detecção em tempo de compilação! E se você estiver feliz por não suportar matrizes de tipos locais com C ++ 98, poderá fazer isso:
#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 essa definição substituída no primeiro programa completo, com g ++, obtive…
M: \ count> g ++ compile_time_detection.cpp
compile_time_detection.cpp: Na função 'void display (const int *)':
compile_time_detection.cpp: 14: erro: nenhuma função correspondente para chamar 'n_items (const int * &)'
M: \ count> _
Como funciona: a matriz é passada por referência e n_items
, portanto, não se deteriora para apontar para o primeiro elemento, e a função pode simplesmente retornar o número de elementos especificado pelo tipo.
Com o C ++ 11, você também pode usá-lo para matrizes do tipo local, e é o idioma C ++ seguro
para encontrar o número de elementos de uma matriz.
5.4 Armadilha C ++ 11 e C ++ 14: Usando uma constexpr
função de tamanho de matriz.
Com o C ++ 11 e posterior, é natural, mas como você verá perigoso !, substituir a função C ++ 03
typedef ptrdiff_t Size;
template< class Type, Size n >
Size n_items( Type (&)[n] ) { return n; }
com
using Size = ptrdiff_t;
template< class Type, Size n >
constexpr auto n_items( Type (&)[n] ) -> Size { return n; }
onde a mudança significativa é o uso de constexpr
, que permite que essa função produza uma constante de tempo de compilação .
Por exemplo, em contraste com a função C ++ 03, essa constante de tempo de compilação pode ser usada para declarar uma matriz do mesmo tamanho que outra:
// 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.
}
Mas considere este código usando a constexpr
versão:
// 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 );
}
A armadilha: em julho de 2015, o acima foi compilado com o MinGW-64 5.1.0 com
-pedantic-errors
, e testando com os compiladores on-line em gcc.godbolt.org/ , também com o clang 3.0 e o clang 3.2, mas não com o clang 3.3, 3.4. 1, 3.5.0, 3.5.1, 3.6 (rc1) ou 3.7 (experimental). E importante para a plataforma Windows, ele não é compilado com o Visual C ++ 2015. O motivo é uma instrução C ++ 11 / C ++ 14 sobre o uso de referências em constexpr
expressões:
C ++ 11 C ++ 14 $ 5,19 / 2 de nove
th traço
Uma expressão condicional e
é uma expressão constante central , a menos que a avaliação de e
, seguindo as regras da máquina abstrata (1.9), avalie uma das seguintes expressões:
⋮
- uma expressão id que se refere a uma variável ou membro de dados do tipo de referência, a menos que a referência tenha uma inicialização anterior e
- é inicializado com uma expressão constante ou
- é um membro de dados não estáticos de um objeto cuja vida útil começou dentro da avaliação de e;
Pode-se sempre escrever o mais detalhado
// 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
}
… Mas isso falha quando Collection
não é uma matriz bruta.
Para lidar com coleções que podem ser não matrizes, é necessário sobrecarregar uma
n_items
função, mas também, para uso em tempo de compilação, é necessário uma representação em tempo de compilação do tamanho da matriz. E a solução clássica C ++ 03, que também funciona bem em C ++ 11 e C ++ 14, é permitir que a função relate seu resultado não como um valor, mas por meio do tipo de resultado da função . Por exemplo, assim:
// 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 );
}
Sobre a escolha do tipo de retorno para static_n_items
: este código não usa std::integral_constant
porque com std::integral_constant
o resultado é representado diretamente como um constexpr
valor, reintroduzindo o problema original. Em vez de uma Size_carrier
classe, pode-se deixar que a função retorne diretamente uma referência a uma matriz. No entanto, nem todo mundo está familiarizado com essa sintaxe.
Sobre a nomeação: parte desta solução para o problema constexpr
-invalid devido à referência é tornar explícita a escolha do tempo de compilação constante.
Esperamos que o oops - houve uma referência envolvida no seu constexpr
problema - seja corrigido com o C ++ 17, mas até então uma macro como a STATIC_N_ITEMS
acima gera portabilidade, por exemplo, para os compiladores clang e Visual C ++, mantendo o tipo segurança.
Relacionado: macros não respeitam escopos, portanto, para evitar colisões de nomes, pode ser uma boa ideia usar um prefixo de nome, por exemplo MYLIB_STATIC_N_ITEMS
.