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 []  

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.


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


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ę.


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.


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


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
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.