5. Typowe pułapki przy korzystaniu z tablic.
5.1 Pułapka: ufanie linkom niebezpiecznym dla typu.
OK, powiedziano ci lub przekonałeś się, że globale (zmienne zakresu nazw, do których można uzyskać dostęp poza jednostką tłumaczącą) są Evil ™. Ale czy wiesz, jak naprawdę są Evil ™? Rozważ poniższy program, składający się z dwóch plików [main.cpp] i [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};
W Windows 7 kompiluje się i łączy dobrze zarówno z MinGW g ++ 4.4.1, jak i Visual C ++ 10.0.
Ponieważ typy się nie zgadzają, program ulega awarii po uruchomieniu.
Wyjaśnienie formalne: program ma niezdefiniowane zachowanie (UB) i zamiast zawieszać się, może po prostu zawiesić się, a może nic nie robić, lub może wysyłać groźne wiadomości e-mail do prezydentów USA, Rosji, Indii, Chiny i Szwajcaria i spraw, aby demony nosowe wyleciały z twojego nosa.
Wyjaśnienie praktyczne: w main.cpp
tablicy jest traktowany jako wskaźnik, umieszczony pod tym samym adresem co tablica. W przypadku 32-bitowego pliku wykonywalnego oznacza to, że pierwsza
int
wartość w tablicy jest traktowana jako wskaźnik. Czyli w zmienna zawiera, lub wydaje się zawierać . Powoduje to, że program uzyskuje dostęp do pamięci na samym dole przestrzeni adresowej, która jest tradycyjnie zarezerwowana i powoduje pułapki. Wynik: masz awarię.main.cpp
numbers
(int*)1
Kompilatory mają pełne prawo nie zdiagnozować tego błędu, ponieważ C ++ 11 §3.5 / 10 mówi o wymaganiu kompatybilnych typów deklaracji,
[N3290 §3,5 / 10]
Naruszenie tej reguły dotyczącej tożsamości typu nie wymaga diagnozy.
W tym samym akapicie opisano dopuszczalną odmianę:
… Deklaracje dla obiektu tablicowego mogą określać typy tablic, które różnią się obecnością lub brakiem głównej powiązanej tablicy (8.3.4).
Ta dozwolona odmiana nie obejmuje zadeklarowania nazwy jako tablicy w jednej jednostce tłumaczenia oraz jako wskaźnika w innej jednostce tłumaczenia.
5.2 Pułapka: Przedwczesna optymalizacja ( memset
i przyjaciele).
Jeszcze nie napisane
5.3 Pułapka: użycie idiomu C w celu uzyskania liczby elementów.
Dzięki głębokiemu doświadczeniu w C pisanie…
#define N_ITEMS( array ) (sizeof( array )/sizeof( array[0] ))
Ponieważ array
rozpada się na wskaźnik do pierwszego elementu w razie potrzeby, wyrażenie sizeof(a)/sizeof(a[0])
można również zapisać jako
sizeof(a)/sizeof(*a)
. Oznacza to to samo i bez względu na to, jak jest napisane, jest to idiom C do znajdowania elementów liczbowych tablicy.
Główna pułapka: idiom C nie jest bezpieczny dla typów. Na przykład kod…
#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 );
}
przekazuje wskaźnik do N_ITEMS
, a zatem najprawdopodobniej daje zły wynik. Skompilowany jako 32-bitowy plik wykonywalny w systemie Windows 7 produkuje…
7 elementów, wywoływanie wyświetlacza ...
1 elementów.
- Kompilator przepisuje
int const a[7]
na just int const a[]
.
- Kompilator przepisuje
int const a[]
na int const* a
.
N_ITEMS
jest zatem wywoływany za pomocą wskaźnika.
- W przypadku 32-bitowego pliku wykonywalnego
sizeof(array)
(rozmiar wskaźnika) wynosi wtedy 4.
sizeof(*array)
jest równoważne sizeof(int)
, który dla 32-bitowego pliku wykonywalnego to również 4.
Aby wykryć ten błąd w czasie wykonywania, możesz…
#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 elementów, wywoływanie display ...
Asercja nie powiodła się: („N_ITEMS wymaga rzeczywistej tablicy jako argumentu”, typeid (a)! = Typeid (& * a)), plik runtime_detect ion.cpp, wiersz 16
Ta aplikacja poprosiła środowisko wykonawcze o zakończenie go w nietypowy sposób.
Skontaktuj się z zespołem pomocy technicznej aplikacji, aby uzyskać więcej informacji.
Wykrywanie błędów w czasie wykonywania jest lepsze niż brak wykrywania, ale marnuje trochę czasu procesora i być może znacznie więcej czasu programisty. Lepiej z wykrywaniem w czasie kompilacji! A jeśli nie chcesz obsługiwać tablic typów lokalnych w C ++ 98, możesz to zrobić:
#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 )
Kompilując tę definicję podstawioną do pierwszego kompletnego programu z g ++, mam…
M: \ count> g ++ compile_time_detection.cpp
compile_time_detection.cpp: W funkcji 'void display (const int *)':
compile_time_detection.cpp: 14: error: brak pasującej funkcji dla wywołania 'n_items (const int * &)'
M: \ count> _
Jak to działa: tablica jest przekazywana przez odwołanie do n_items
, a więc nie rozkłada się na wskaźnik do pierwszego elementu, a funkcja może po prostu zwrócić liczbę elementów określoną przez typ.
W C ++ 11 możesz tego również używać do tablic typu lokalnego, i jest to bezpieczny typ
języka C ++ do znajdowania liczby elementów tablicy.
5.4 Pułapka C ++ 11 i C ++ 14: Korzystanie z constexpr
funkcji rozmiaru tablicy.
Z C ++ 11 i nowszymi jest to naturalne, ale jak zobaczysz niebezpieczne !, zastąpienie funkcji C ++ 03
typedef ptrdiff_t Size;
template< class Type, Size n >
Size n_items( Type (&)[n] ) { return n; }
z
using Size = ptrdiff_t;
template< class Type, Size n >
constexpr auto n_items( Type (&)[n] ) -> Size { return n; }
gdzie istotną zmianą jest użycie constexpr
, co pozwala tej funkcji wygenerować stałą czasową kompilacji .
Na przykład, w przeciwieństwie do funkcji C ++ 03, taką stałą czasową kompilacji można wykorzystać do zadeklarowania tablicy o tym samym rozmiarze co inna:
// 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.
}
Ale rozważ ten kod za pomocą constexpr
wersji:
// 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 );
}
Pułapka: od lipca 2015 r. Powyższe kompiluje się z MinGW-64 5.1.0 z
-pedantic-errors
testami z kompilatorami online na gcc.godbolt.org/ , również z clang 3.0 i clang 3.2, ale nie z clang 3.3, 3.4. 1, 3.5.0, 3.5.1, 3.6 (rc1) lub 3.7 (doświadczalnie). I ważne dla platformy Windows, nie kompiluje się z Visual C ++ 2015. Powodem jest instrukcja C ++ 11 / C ++ 14 na temat używania referencji w constexpr
wyrażeniach:
C ++ C ++ 11 14 $ 5,19 / 2 dziewięć
th kreska
Warunkowe wyrażenie e
jest rdzeń stałym wyrażeniem chyba oceny e
, zgodnie z zasadami abstrakcyjnej maszynie (1.9), by ocenić jedno z następujących wyrażeń:
⋮
- ID ekspresja , który odnosi się do elementu lub zmiennych danych typu o ile z wzorcowy poprzedzający inicjalizacji i albo
- jest inicjowany stałym wyrażeniem lub
- jest niestatycznym elementem danych obiektu, którego okres istnienia rozpoczął się w ramach oceny e;
Zawsze można napisać bardziej szczegółowe
// 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
}
… Ale to się nie udaje, gdy Collection
nie jest surową tablicą.
Aby poradzić sobie z kolekcjami, które mogą być nie-tablicami, potrzebna jest przeciążalność
n_items
funkcji, ale także, do wykorzystania w czasie kompilacji, potrzebna jest reprezentacja rozmiaru tablicy w czasie kompilacji. A klasyczne rozwiązanie C ++ 03, które działa dobrze również w C ++ 11 i C ++ 14, pozwala funkcji zgłaszać swój wynik nie jako wartość, ale poprzez typ wyniku funkcji . Na przykład tak:
// 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 );
}
O wyborze typu zwrotu dla static_n_items
: ten kod nie używa, std::integral_constant
ponieważ std::integral_constant
wynik jest reprezentowany bezpośrednio jako constexpr
wartość, przywracając pierwotny problem. Zamiast Size_carrier
klasy można pozwolić, aby funkcja bezpośrednio zwróciła odwołanie do tablicy. Jednak nie wszyscy znają tę składnię.
Informacje na temat nazewnictwa: częścią tego rozwiązania problemu constexpr
-invalid-due-to-reference jest wyraźne wybranie stałej czasowej kompilacji.
Mam nadzieję, że constexpr
problem zostanie rozwiązany w C ++ 17, ale do tego czasu makro takie jak STATIC_N_ITEMS
powyższe zapewnia przenośność, np. Do kompilatora clang i Visual C ++, zachowując typ bezpieczeństwo.
Powiązane: makra nie respektują zakresów, więc aby uniknąć kolizji nazw, dobrym pomysłem może być użycie przedrostka nazwy, np MYLIB_STATIC_N_ITEMS
.