Dlaczego `` void * '' nie jest niejawnie obsadzony w C ++?


30

W C nie ma potrzeby rzucania void *żadnego innego wskaźnika, zawsze jest bezpiecznie promowany. Jednak w C ++ tak nie jest. Na przykład,

int *a = malloc(sizeof(int));

działa w C, ale nie w C ++. (Uwaga: Wiem, że nie powinieneś używać mallocw C ++ ani w tym przypadkunew , a zamiast tego powinieneś preferować inteligentne wskaźniki i / lub STL; jest to pytane wyłącznie z ciekawości) Dlaczego standard C ++ nie pozwala na taką niejawną obsadę, podczas gdy standard C robi?


3
long *a = malloc(sizeof(int));Ups, ktoś zapomniał zmienić tylko jeden typ!
Doval

4
@Doval: Nadal łatwo to naprawić, używając sizeof(*a)zamiast tego.
wolfPack88

3
Uważam, że @ ratchetfreak ma na myśli to, że powodem, dla którego C dokonuje tej niejawnej konwersji jest to, że mallocnie może zwrócić wskaźnika do przydzielonego typu. newto C ++ zwraca wskaźnik do przydzielonego typu, więc poprawnie napisany kod C ++ nigdy nie będzie miał żadnych void *rzutów.
Gort the Robot

3
Nawiasem mówiąc, nie jest to żaden inny typ wskaźnika. Obowiązują tylko wskaźniki danych.
Deduplicator

3
@ wolfPack88 masz błędną historię. C ++ miał void, C nie . Kiedy to słowo / pomysł został dodany do C, to zmienił go do potrzeb użytkownika c. Było to krótko po tym, jak typy wskaźników zaczęły być w ogóle sprawdzane . Sprawdź, czy możesz znaleźć broszurę z opisem K&R C online, czy też starą kopię tekstu programowego C, takiego jak C Primer firmy Waite Group . ANSI C było pełne, funkcji wspieranych lub inspirowanych przez C ++, a K&R C był znacznie prostszy. Bardziej poprawne jest więc to, że C ++ rozszerzył C tak, jak istniał w tym czasie, a C, o którym wiesz, że zostało usunięte z C ++.
JDługosz

Odpowiedzi:


39

Ponieważ konwersje typu niejawnego są zwykle niebezpieczne, a C ++ przyjmuje bezpieczniejsze podejście do pisania niż C.

C zwykle zezwala na konwersje niejawne, nawet jeśli istnieje duże prawdopodobieństwo, że konwersja jest błędem. To dlatego, że C zakłada, że ​​programista wie dokładnie, co robią, a jeśli nie, to jest to problem programisty, a nie problem kompilatora.

C ++ zwykle nie zezwala na rzeczy, które mogą potencjalnie być błędami, i wymaga jawnego określenia swojej intencji za pomocą rzutowania typu. To dlatego, że C ++ stara się być przyjazny dla programistów.

Możesz zapytać, dlaczego jest przyjazny, gdy w rzeczywistości wymaga więcej pisania.

Widzisz, każdy wiersz kodu, w dowolnym programie, w dowolnym języku programowania, będzie na ogół czytany wiele razy, niż zostanie napisany (*). Zatem łatwość czytania jest o wiele ważniejsza niż łatwość pisania. A podczas czytania wyróżnianie się potencjalnie niebezpiecznymi konwersjami za pomocą rzutowania typu jawnego pomaga zrozumieć, co się dzieje i mieć pewien poziom pewności, że to, co się dzieje, faktycznie jest tym, co miało się wydarzyć.

Poza tym niedogodność związana z wpisywaniem jawnej obsady jest trywialna w porównaniu z niedogodnościami godzinnego rozwiązywania problemów w celu znalezienia błędu, który został spowodowany przez błędne zadanie, o którym można było Cię uprzedzić, ale którego nigdy nie było.

(*) Najlepiej, jeśli zostanie napisany tylko raz, ale będzie czytany za każdym razem, gdy ktoś będzie musiał go przejrzeć, aby określić, czy nadaje się do ponownego użycia, i za każdym razem, gdy pojawia się problem, i za każdym razem, gdy ktoś musi dodać kod w pobliżu a następnie za każdym razem, gdy pojawia się problem z pobliskim kodem i tak dalej. Dzieje się tak we wszystkich przypadkach, z wyjątkiem skryptów „napisz raz, uruchom, a potem wyrzuć”, dlatego nic dziwnego, że większość języków skryptowych ma składnię, która ułatwia pisanie z całkowitym lekceważeniem łatwości czytania. Czy kiedykolwiek myślałeś, że perl jest całkowicie niezrozumiały? Nie jesteś sam. Pomyśl o takich językach jak o językach „tylko do zapisu”.


C jest zasadniczo o jeden krok powyżej kodu maszynowego. Mogę wybaczyć C za takie rzeczy.
Qix,

8
Programy (niezależnie od języka) są przeznaczone do odczytu. Wyraźne operacje wyróżniają się.
Matthieu M.

10
Warto zauważyć, że przerzucanie void*jest bardziej niebezpieczne w C ++, ponieważ sposób, w jaki niektóre funkcje OOP są implementowane w C ++, wskaźnik do tego samego obiektu może mieć różną wartość w zależności od typu wskaźnika.
hyde

@MatthieuM. bardzo prawdziwe. Dziękujemy za dodanie tego, że warto być częścią odpowiedzi.
Mike Nakis,

1
@ MatthieuM .: Ach, ale tak naprawdę nie chcesz robić wszystkiego wyraźnie. Czytelność nie poprawia się dzięki większej możliwości czytania. Chociaż w tym punkcie równowaga jest wyraźnie za wyraźne.
Deduplicator

28

Oto, co mówi Stroustrup :

W C możesz niejawnie przekonwertować void * na T *. To jest niebezpieczne

Następnie pokazuje przykład, jak pustka * może być niebezpieczna i mówi:

... W związku z tym, w C ++, aby uzyskać T * z pustki *, potrzebujesz jawnej obsady. ...

Wreszcie zauważa:

Jednym z najczęstszych zastosowań tej niebezpiecznej konwersji w C jest przypisanie wyniku malloc () do odpowiedniego wskaźnika. Na przykład:

int * p = malloc (sizeof (int));

W C ++ użyj nowego operatora typeafe:

int * p = nowy int;

Zagłębia się bardziej szczegółowo w The Design and Evolution of C ++ .

Odpowiedź sprowadza się więc do: Projektant języka uważa, że ​​jest to niebezpieczny wzorzec, dlatego uczynił go nielegalnym i zapewnił alternatywne sposoby osiągnięcia tego, do czego normalnie był używany.


1
Odnosząc się do tego mallocprzykładu, warto zauważyć, że malloc(oczywiście) nie wywoła konstruktora, więc jeśli jest to typ klasy, dla której alokujesz pamięć, możliwość niejawnego rzutowania na typ klasy byłaby myląca.
chbaker0

To bardzo dobra odpowiedź, prawdopodobnie lepsza niż moja. Tylko nieznacznie nie lubię podejścia „argument z autorytetu”.
Mike Nakis,

„new int” - wow, jako nowicjusz w C ++ miałem problem z myśleniem o sposobie dodania podstawowego typu do stosu i nawet nie wiedziałem, że możesz to zrobić. -1 użyj dla malloc.
Katana314,

3
@MikeNakisFWIW, moim zamiarem było uzupełnienie twojej odpowiedzi. W tym przypadku pytanie brzmiało „dlaczego to zrobili”, więc uznałem, że wysłuchanie przez głównego projektanta jest uzasadnione.
Gort the Robot

11

W C nie ma potrzeby rzucania pustki * na inny typ wskaźnika, zawsze jest on bezpiecznie promowany.

To jest zawsze promowane, tak, ale prawie nie bezpiecznie .

C ++ wyłącza to zachowanie właśnie dlatego, że próbuje mieć bezpieczniejszy system typów niż C, a to zachowanie nie jest bezpieczne.


Rozważ ogólnie następujące 3 podejścia do konwersji typów:

  1. zmusza użytkownika do napisania wszystkiego, więc wszystkie konwersje są jawne
  2. Załóżmy, że użytkownik wie, co robi, i przekonwertuj dowolny typ na dowolny inny
  3. wdrożyć kompletny system typów z rodzajami bezpiecznymi (szablony, nowe wyrażenia), jawne operatory konwersji zdefiniowane przez użytkownika i wymuszać jawne konwersje tylko rzeczy, których kompilator nie widzi, są domyślnie bezpieczne

Cóż, 1 jest brzydka i stanowi praktyczną przeszkodę w zrobieniu czegokolwiek, ale może być naprawdę używana tam, gdzie potrzebna jest wielka ostrożność. C z grubsza wybrał 2, które jest łatwiejsze do wdrożenia, i C ++ dla 3, które jest trudniejsze do wdrożenia, ale bezpieczniejsze.


Droga do 3 była trudna, z wyjątkiem wyjątków dodanych później, a szablonów później.
JDługosz

Cóż, szablony nie są tak naprawdę potrzebne dla tak kompletnego bezpiecznego systemu typów: języki oparte na Hindley-Milner radzą sobie bez nich całkiem dobrze, bez żadnych ukrytych konwersji i nie trzeba też jawnie pisać typów. (Oczywiście te języki opierają się na usuwaniu tekstu / usuwaniu śmieci / lepszym polimorfizmie, czego C ++ raczej unika.)
leftarabout około

1

Z definicji wskaźnik pustki może wskazywać na wszystko. Dowolny wskaźnik można przekonwertować na wskaźnik pustki, dzięki czemu będzie można przekonwertować wracając do tej samej wartości. Jednak wskaźniki do innych typów mogą mieć ograniczenia, takie jak ograniczenia wyrównania. Na przykład wyobraź sobie architekturę, w której znaki mogą zajmować dowolny adres pamięci, ale liczby całkowite muszą zaczynać się od równych granic adresów. W niektórych architekturach wskaźniki całkowite mogą nawet liczyć 16, 32 lub 64 bity jednocześnie, tak że char * może faktycznie mieć wielokrotność wartości liczbowej int *, wskazując jednocześnie to samo miejsce w pamięci. W takim przypadku konwersja z pustki * faktycznie zaokrągliłaby bity, których nie można odzyskać, a zatem nie jest odwracalna.

Mówiąc prościej, wskaźnik pustki może wskazywać na wszystko, w tym na rzeczy, na które inne wskaźniki mogą nie być w stanie wskazać. Zatem konwersja do pustego wskaźnika jest bezpieczna, ale nie na odwrót.

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.