Czy istnieje uzasadnione użycie void *?

87

Czy istnieje uzasadnione użycie void*w C ++? A może zostało to wprowadzone, ponieważ C to miał?

Podsumowując moje przemyślenia:

Dane wejściowe : jeśli chcemy zezwolić na wiele typów danych wejściowych, możemy przeciążać funkcje i metody, alternatywnie możemy zdefiniować wspólną klasę bazową lub szablon (dziękujemy za wspomnienie o tym w odpowiedziach). W obu przypadkach kod jest bardziej opisowy i mniej podatny na błędy (pod warunkiem, że klasa bazowa jest zaimplementowana w rozsądny sposób).

Wynik : nie przychodzi mi do głowy żadna sytuacja, w której wolałbym otrzymywać, void*w przeciwieństwie do czegoś pochodzącego ze znanej klasy bazowej.

Żeby wyjaśnić, co mam na myśli: nie pytam konkretnie, czy istnieje przypadek użycia void*, ale czy istnieje przypadek, w którym void*jest najlepszy lub jedyny dostępny wybór. Na co kilka osób poniżej doskonale odpowiedziało.

magu_
źródło
3
A co z czasem, kiedy chcesz mieć wiele typów, takich jak int & std :: string?
Amir
12
@Amir, variant, any, rekord z wariantami. Wszystko, co wskazuje rzeczywisty typ treści i jest bezpieczniejsze w użyciu.
Revolver_Ocelot
15
„C ma to” to wystarczająco mocne uzasadnienie, nie trzeba szukać więcej. Unikanie go tak często, jak to możliwe, jest dobrą rzeczą w obu językach.
n. zaimki m.
1
Jedna rzecz: współdziałanie z interfejsami API w stylu C jest bez niego niewygodne.
Martin James
1
Ciekawym zastosowaniem jest wymazywanie typów dla wektorów wskaźników
Paolo M

Odpowiedzi:

81

void*jest co najmniej konieczny jako wynik ::operator new(również every operator new...) malloci jako argument newoperatora umieszczania .

void*można uważać za wspólny nadtyp każdego typu wskaźnika. Więc nie jest to dokładnie wskaźnik do celu void, ale wskaźnik do czegokolwiek.

BTW, jeśli chcesz zachować jakieś dane dla kilku niepowiązanych zmiennych globalnych, możesz użyć ich std::map<void*,int> score; wtedy, po zadeklarowaniu globalnych int x;i double y;i std::string s;zrób score[&x]=1;i score[&y]=2;iscore[&z]=3;

memset chce void*adresu (najbardziej ogólnego)

Ponadto systemy POSIX mają dlsym i jego typem zwracanym powinien byćvoid*

Basile Starynkevitch
źródło
1
To bardzo dobra uwaga. Zapomniałem wspomnieć, że myślę bardziej o użytkowniku języka niż po stronie implementacji. Jeszcze jedno, czego nie rozumiem: czy nowa funkcja to? Myślałem o tym bardziej jako o słowie kluczowym
magu_
9
new to operator, taki jak + i sizeof.
Joshua,
7
Oczywiście pamięć może zostać zwrócona przez char *. Są bardzo blisko w tym aspekcie, chociaż znaczenie jest nieco inne.
edmz
@Joshua. Nie wiedziałem tego. Dziękuję, że mnie tego nauczyłeś. Skoro C++jest w C++tym napisane, to wystarczy void*. Uwielbiam tę stronę. Zadaj jedno pytanie, wiele się naucz.
magu_
3
@black powrotem w dawnych czasach, C nawet nie mają na void*typ. Wszystkie używane standardowe funkcje bibliotekichar*
Cole Johnson,
28

Istnieje wiele powodów, dla których warto używać void*, z których 3 najczęściej to:

  1. współdziałanie z biblioteką C za pomocą void*jej interfejsu
  2. wymazywanie czcionek
  3. oznaczające nie wpisaną pamięć

W odwrotnej kolejności oznaczanie niepisanej pamięci za pomocą void*(3) zamiast char*(lub wariantów) pomaga zapobiegać przypadkowej arytmetyce wskaźnika; jest bardzo mało dostępnych operacji, void*więc zwykle wymaga odlewania, zanim będzie użyteczny. I oczywiście podobnie jak w char*przypadku aliasingu nie ma problemu.

Type-erasure (2) jest nadal używany w C ++, w połączeniu z szablonami lub nie:

  • Kod nieogólny pomaga zredukować binarne wzdęcia, jest przydatny w zimnych ścieżkach nawet w kodzie ogólnym
  • Kod nieogólny jest czasem potrzebny do przechowywania, nawet w ogólnym kontenerze, takim jak std::function

I oczywiście, gdy interfejs, z którym masz do czynienia, używa void*(1), masz niewielki wybór.

Matthieu M.
źródło
15

O tak. Nawet w C ++ czasami idziemy void *raczej z niż template<class T*>dlatego, że czasami dodatkowy kod z rozszerzenia szablonu waży zbyt dużo.

Zwykle używałbym go jako rzeczywistej implementacji typu, a typ szablonu odziedziczyłby po nim i zawinąłby rzutowania.

Ponadto muszą być używane niestandardowe podzielniki płyt (nowe implementacje operatorów) void *. Jest to jeden z powodów, dla których g ++ dodał rozszerzenie pozwalające na arytmatyzację wskaźnika void *tak, jakby miał rozmiar 1.

Joshua
źródło
Dziękuję za Twoją odpowiedź. Masz rację, zapomniałem wspomnieć o szablonach. Ale nie szablony być jeszcze lepiej nadaje się do tego zadania, ponieważ rzadko wprowadzają karę wydajności (? Stackoverflow.com/questions/2442358/... )
magu_
1
Szablony wprowadzają ograniczenie rozmiaru kodu, co w większości przypadków jest również spadkiem wydajności.
Joshua
struct wrapper_base {}; template<class T> struct wrapper : public wrapper_base {T val;} typedef wrapper* like_void_ptr;to minimalny symulator void - * - wykorzystujący szablony.
user253751
3
@Joshua: Będziesz potrzebował cytatu dla „większości”.
user541686
@Mehrdad: Zobacz pamięć podręczną L1.
Joshua
10

Dane wejściowe: jeśli chcemy zezwolić na wiele typów danych wejściowych, możemy przeciążać funkcje i metody

Prawdziwe.

alternatywnie możemy zdefiniować wspólną klasę bazową.

Jest to częściowo prawda: a co, jeśli nie możesz zdefiniować wspólnej klasy bazowej, interfejsu lub podobnego? Aby je zdefiniować, musisz mieć dostęp do kodu źródłowego, co często nie jest możliwe.

Nie wspomniałeś o szablonach. Jednak szablony nie mogą pomóc w polimorfizmie: działają z typami statycznymi, tj. Znanymi w czasie kompilacji.

void*można uznać za najniższy wspólny mianownik. W C ++ zazwyczaj nie jest to potrzebne, ponieważ (i) z natury nie można z nim zrobić zbyt wiele i (ii) prawie zawsze są lepsze rozwiązania.

Co więcej, zazwyczaj kończy się konwersją na inne typy betonu. Dlatego char *jest zwykle lepszy, chociaż może to oznaczać, że spodziewasz się łańcucha w stylu C, a nie czystego bloku danych. Dlatego void*jest lepsze niż char*do tego, ponieważ umożliwia niejawne rzutowanie z innych typów wskaźników.

Powinieneś otrzymać jakieś dane, popracować z nimi i wygenerować wynik; Aby to osiągnąć, musisz znać dane, z którymi pracujesz, w przeciwnym razie masz inny problem, który nie jest tym, który pierwotnie rozwiązałeś. Na przykład wiele języków nie ma void*i nie ma z tym problemu.

Kolejne legalne użycie

Podczas drukowania adresów wskaźników z funkcjami takimi jak printfwskaźnik powinny mieć void*typ i dlatego może być konieczne rzutowanie na void*

edmz
źródło
7

Tak, jest tak samo przydatna jak każda inna rzecz w języku.
Na przykład możesz go użyć do usunięcia typu klasy, którą możesz statycznie rzutować na odpowiedni typ w razie potrzeby, aby uzyskać minimalny i elastyczny interfejs.

W tej odpowiedzi jest przykład użycia, który powinien dać ci pomysł.
Kopiuję i wklejam poniżej dla zachowania przejrzystości:

class Dispatcher {
    Dispatcher() { }

    template<class C, void(C::*M)() = C::receive>
    static void invoke(void *instance) {
        (static_cast<C*>(instance)->*M)();
    }

public:
    template<class C, void(C::*M)() = &C::receive>
    static Dispatcher create(C *instance) {
        Dispatcher d;
        d.fn = &invoke<C, M>;
        d.instance = instance;
        return d;
    }

    void operator()() {
        (fn)(instance);
    }

private:
    using Fn = void(*)(void *);
    Fn fn;
    void *instance;
};

Oczywiście jest to tylko jedno z wielu zastosowań void*.

skypjack
źródło
4

Współpraca z zewnętrzną funkcją biblioteki, która zwraca wskaźnik. Oto jeden dla aplikacji Ada.

extern "C" { void* ada_function();}

void* m_status_ptr = ada_function();

To zwraca wskaźnik do tego, o czym Ada chciała ci powiedzieć. Nie musisz z tym robić nic wymyślnego, możesz oddać go Adzie, aby zrobiła następną rzecz. W rzeczywistości rozplątanie wskaźnika Ada w C ++ jest nietrywialne.

RedSonja
źródło
Czy nie moglibyśmy użyć autozamiast tego w tym przypadku? Zakładając, że typ jest znany w czasie kompilacji.
magu_
Ach, teraz prawdopodobnie moglibyśmy. Ten kod pochodzi z kilku lat temu, kiedy zrobiłem kilka opakowań Ada.
RedSonja
Typy Ada mogą być diabelskie - wiecie, tworzą własne i czerpią perwersyjną przyjemność, czyniąc je podstępnymi. Nie pozwolono mi zmienić interfejsu, to byłoby zbyt łatwe, i zwróciło to trochę paskudnych rzeczy ukrytych w tych pustych wskaźnikach. Fuj.
RedSonja
2

Krótko mówiąc, C ++ jako język ścisły (nie biorąc pod uwagę reliktów C, takich jak malloc () ) wymaga void *, ponieważ nie ma wspólnego rodzica wszystkich możliwych typów. W przeciwieństwie na przykład do ObjC, który ma obiekt .

nredko
źródło
malloci newoba wracają void *, więc potrzebowałbyś, gdyby nawet istniała klasa obiektów w C ++
Dmitry Grigoryev
malloc jest reliktem, ale w surowych językach nowy powinien zwrócić obiekt *
nredko
jak więc przydzielisz tablicę liczb całkowitych?
Dmitrij Grigoriew
@DmitryGrigoryev operator new()zwraca void *, ale newwyrażenie nie
MM
1. Nie jestem pewien, czy chciałbym zobaczyć wirtualny obiekt klasy bazowej nad każdą klasą i typem , w tym int2. Jeśli jest to nie-wirtualny EBC, to czym się różni od void*?
lorro
1

Pierwszą rzeczą, która przychodzi mi do głowy (podejrzewam, że jest to konkretny przypadek kilku powyższych odpowiedzi), jest możliwość przekazania instancji obiektu do procedury wątku w systemie Windows.

Mam kilka klas C ++, które muszą to zrobić, mają implementacje wątku roboczego, a parametr LPVOID w API CreateThread () otrzymuje adres implementacji metody statycznej w klasie, dzięki czemu wątek roboczy może wykonać pracę z konkretna instancja klasy. Proste rzutowanie statyczne z powrotem w Threadproc daje wystąpienie do pracy, umożliwiając każdemu obiektowi, którego wystąpienie, wątek roboczy z pojedynczej implementacji metody statycznej.

AndyW
źródło
0

W przypadku dziedziczenia wielokrotnego, jeśli chcesz uzyskać wskaźnik do pierwszego bajtu fragmentu pamięci zajmowanego przez obiekt, możesz to dynamic_castzrobić void*.

Niewielkie zagrożenie
źródło