Wykrywanie błędnego użycia delete [] vs. delete w czasie kompilacji

19

Chciałbym wiedzieć, czy możliwe jest wykrycie deletebłędu skomentowanego poniżej podczas kompilacji? Szczególnie chciałbym usłyszeć o kompilatorze g ++.

ClassTypeA *abc_ptr = new ClassTypeA[100];  
abc_ptr[10].data_ = 1;  
delete abc_ptr; // error, should be delete []  
SebGR
źródło
7
I tak nie powinieneś ręcznie nazywać usuwania.
Martin York
9
@LokiAstari Czy naprawdę uważasz, że ten komentarz był pomocny?
James
5
@James: Tak. Klucz to „ręcznie”.
Martin York
Czasami zastosowanie się do tego wymagałoby przepisania dużej ilości starszego kodu
Nick Keighley
Użyj, std::unique_ptr<ClassTypeA[]>a potem nie musisz.
user253751

Odpowiedzi:

6

Ogólnie kompilator nie może wykryć takich błędów. Przykład: załóżmy, że konstruktor dla pewnej klasy przydziela część danych przy użyciu new TypeName[], ale destruktor błędnie używa deletezamiast delete[]. Jeśli konstruktor i destruktor są zdefiniowane w osobnych jednostkach kompilacyjnych, to skąd kompilator powinien wiedzieć, kiedy kompiluje plik, który definiuje destruktor, że użycie jest niezgodne z użytym w osobnym kompilowanym pliku, który definiuje konstruktor?

W odniesieniu do kompilatorów GNU tak nie jest. Jak wspomniano powyżej, nie można tego zrobić w ogólnym przypadku. Kompilator nie musi wykrywać takich niedopasowanych błędów nowego / usuwania, ponieważ jest to niezdefiniowane zachowanie. UB to karta „wyjdź z więzienia” producenta kompilatora.

Narzędzia takie jak valgrind mogą wykrywać tego rodzaju niedopasowania nowe / usuwające, ale robią to w czasie wykonywania. Może istnieć narzędzie do analizy statycznej, które przegląda wszystkie pliki źródłowe, które ostatecznie zostaną skompilowane w celu utworzenia pliku wykonywalnego, ale nie mam takiego narzędzia do analizy statycznej, które wykryłoby ten rodzaj błędu.

David Hammen
źródło
Użyłem narzędzia do analizy statycznej o nazwie Parasoft, które zdecydowanie ma regułę dla tego konkretnego scenariusza. Działa na wszystkich plikach w danym projekcie (jeśli został poprawnie skonfigurowany). Biorąc to pod uwagę, nie jestem pewien, jak dobrze radzi sobie ze scenariuszami, takimi jak komentarz Pete Kirkham do odpowiedzi Kiliana Fotha.
Velociraptors
28

Możesz użyć odpowiednich klas RAII do delete. Jest to jedyny bezpieczny sposób, aby to zrobić, a ten błąd jest tylko jednym z bardzo wielu osób, które mogą się nazywać delete.

Zawsze używaj klas do zarządzania dynamicznymi dożywotnimi zasobami, a system typów wymusi prawidłowe niszczenie zasobów.

Edycja: „Co jeśli kontrolujesz kod i nie możesz go zmienić?” Jesteś pieprzony

DeadMG
źródło
18
-1, ponieważ tak naprawdę nie odpowiada na pytanie.
Mason Wheeler,
2
Jedynym sposobem na wykrycie niezgodności jest użycie systemu typów, który wymaga użycia klas RAII.
DeadMG
9
... to ma jeszcze mniej sensu. Co zastosowanie klas RAII - mechanizmu wykonawczego - ma wspólnego z informacjami o systemie typu statycznego, o których kompilator wie w czasie kompilacji?
Mason Wheeler,
6
@MasonWheeler patrz przykłady boost :: shared_ptr i boost :: shared_array jako przykłady. Zniszczenie współużytkowanego pliku usuwa obiekt, niszcząc tablicę współużytkowaną delete []. Nie możesz przypisać tablicy współużytkowanej do tablicy współużytkowanej, więc - dopóki nie zbudujesz tablicy współużytkowanej z tablicą - system typów zapobiega użyciu niewłaściwego usuwania.
Pete Kirkham
4
Zwykle taka odpowiedź jest bardziej nieznośna niż pomocna. Jednak w tym przypadku tak naprawdę jest. Szuka egzekwowania powszechnego błędu przez kompilator, a prawidłowe użycie RAII zapobiega temu błędowi, dając mu dokładnie to, czego chce. +1
riwalk
10

Ten konkretny błąd - tak. Tego rodzaju błąd ogólnie: niestety nie! Oznaczałoby to przewidywanie przepływu wykonania bez jego wykonania, a nie jest to możliwe w przypadku dowolnych programów. (Dlatego większość kompilatorów nawet nie próbuje wykryć prostych przypadków, takich jak twój przykład).

Dlatego odpowiedź DeadMG jest właściwa: nie próbuj robić tego dobrze, zwracając uwagę - ludzka uwaga jest omylna. Użyj środków podanych w języku i pozwól komputerowi zwrócić uwagę.

Kilian Foth
źródło
Jak to wymaga przewidywania przepływu wykonania? Dla mnie wygląda to na czysto statyczną wiedzę z okresu kompilacji; system typów kompilatora wie, co to jest tablica, a co nie.
Mason Wheeler,
Nawet w obecności obsad? Przepraszam, jeśli się mylę, usunę odpowiedź.
Kilian Foth
12
@MasonWheeler jest statycznym typem abc_ptr, ClassTypeA*więc można wstawić linię między nowym a usuwanym. if ( rand() % 2 == 1 ) abc_ptr = new ClassTypeA;Nic w systemie typu statycznego nie pokazuje, czy abc_ptrwskazuje na tablicę, obiekt dynamiczny, czy częściowo w inny obiekt lub tablicę.
Pete Kirkham
... och, racja. Jestem tak przyzwyczajony do pracy z językami z prawdziwymi typami tablic, że ciągle zapominam o tym, jak jest popieprzony w C-land. :(
Mason Wheeler,
1
@Pete Kirkham, @Mason Wheeler: Ale wciąż środowisko wykonawcze powinno zobaczyć, ile obiektów jest przechowywanych pod wskazanym adresem abc_ptr, w przeciwnym razie, w jaki sposób mógłby być w stanie zwolnić odpowiednią ilość pamięci? Dlatego środowisko wykonawcze wie, ile obiektów należy zwolnić.
Giorgio
4

Trywialny przypadek, który pokazujesz, można wykryć w czasie kompilacji, ponieważ tworzenie i niszczenie obiektu są w tym samym zakresie. Zasadniczo usunięcie nie ma tego samego zakresu, ani nawet tego samego pliku źródłowego, co utworzenie. A typ wskaźnika C ++ nie przenosi informacji o tym, czy odwołuje się do pojedynczego obiektu tego typu, czy tablicy, nie mówiąc już o schemacie alokacji. Dlatego nie jest możliwe zdiagnozowanie tego w ogóle podczas kompilacji.

Dlaczego nie zdiagnozować możliwych przypadków specjalnych?

W C ++ istnieją już narzędzia do radzenia sobie z wyciekiem zasobów dynamicznych powiązanych z zakresami, a mianowicie inteligentne wskaźniki i tablice wyższego poziomu ( std::vector).

Nawet jeśli użyjesz prawidłowego deletesmaku, Twój kod nadal nie jest wyjątkowo bezpieczny. Jeśli kod między new[]i delete[]kończy się dynamicznym wyjściem, usuwanie nigdy się nie wykonuje.

Jeśli chodzi o wykrywanie w czasie wykonywania, Valgrindnarzędzie dobrze wykrywa je w czasie wykonywania. Zegarek:

==26781== Command: ./a.out
==26781==
==26781== Mismatched free() / delete / delete []
==26781==    at 0x402ACFC: operator delete(void*) (in /usr/lib/valgrind/vgpreload_memcheck-x86-linux.so)
==26781==    by 0x8048498: main (in /home/kaz/test/a.out)
==26781==  Address 0x4324028 is 0 bytes inside a block of size 80 alloc'd
==26781==    at 0x402B454: operator new[](unsigned int) (in /usr/lib/valgrind/vgpreload_memcheck-x86-linux.so)
==26781==    by 0x8048488: main (in /home/kaz/test/a.out)

Oczywiście Valgrind nie działa na wszystkich platformach i nie zawsze jest praktyczne lub możliwe odtworzenie wszystkich sytuacji w czasie wykonywania za pomocą tego narzędzia.

Kaz
źródło
mówisz, że ten trywialny przypadek można wykryć podczas kompilacji. Czy możesz mi powiedzieć, jakiego polecenia kompilacji używasz, aby to osiągnąć?
SebGR
„można wykryć w czasie kompilacji” oznacza tutaj, że łatwo jest go zaimplementować w kompilatorze, a nie że g ++ go ma. Podczas przetwarzania tego zakresu kompilator ma cały czas istnienia identyfikatora i może propagować informacje o alokacji jako atrybuty semantyczne powiązane ze składnią.
Kaz
-3

Kilka trywialnych przykładów wykrywania w czasie kompilacji / analizy statycznej:

Na hoście RHEL7 z cppcheck 1.77 and 1.49

> cat test.cc
#include <memory>
int main(){char* buf = new char[10];delete buf;}

http://cppcheck.sourceforge.net/

> cppcheck -x c++ test.cc
Checking test.cc ...
[test.cc:2]: (error) Mismatching allocation and deallocation: buf

Z clang++ 3.7.1na RHEL7

> clang++ --analyze -x c++ test.cc
test.cc:2:37: warning: Memory allocated by 'new[]' should be deallocated by
'delete[]', not 'delete'
int main(){char* buf = new char[10];delete buf;}
                                    ^~~~~~~~~~
1 warning generated.

Analizator statyczny Clang może również wykryć, kiedy std::unique_ptrnie jest przekazywany<char[]>

> cat test2.cc
#include <memory>
int main(){std::unique_ptr<char> buf(new char[10]);}

https://clang-analyzer.llvm.org/

> clang++ --analyze -x c++ -std=c++11 test2.cc
In file included from test2.cc:1:
In file included from /opt/rh/devtoolset-4/root/usr/lib/gcc/x86_64-redhat-linux/5.3.1/
../../../../include/c++/5.3.1/memory:81:
/opt/rh/devtoolset-4/root/usr/lib/gcc/x86_64-redhat-linux/5.3.1/
../../../../include/c++/5.3.1/bits/unique_ptr.h:76:2: 
warning: Memory allocated by
      'new[]' should be deallocated by 'delete[]', not 'delete'
        delete __ptr;
        ^~~~~~~~~~~~
1 warning generated.

Zaktualizuj poniżej link do pracy, która dodała to do clang, testów i jednego błędu, który znalazłem.

Zostało to dodane do clang with reviews.llvm.org/D4661 - „Wykryj niedopasowane zastosowania„ nowe ”i„ usuń ” .

Testy są w teście / Analiza / NiezgodnośćDeallocator-checker-test.mm

Znalazłem ten otwarty błąd - bugs.llvm.org/show_bug.cgi?id=24819

to imię bezpieczne
źródło
Nikt nie wątpi, że można znaleźć analizator statyczny, który wykrywa jedno konkretne niewłaściwe użycie, zamiast tego, który wykrywa wszystkie złe zastosowania (i miejmy nadzieję, że nie popełnia niewłaściwych zastosowań)
Caleth