Gibt es eine legitime Verwendung für void *?

87

Gibt es eine legitime Verwendung void*in C ++? Oder wurde dies eingeführt, weil C es hatte?

Nur um meine Gedanken zusammenzufassen:

Eingabe : Wenn wir mehrere Eingabetypen zulassen möchten, können wir Funktionen und Methoden überladen. Alternativ können wir eine gemeinsame Basisklasse oder Vorlage definieren (danke, dass Sie dies in den Antworten erwähnt haben). In beiden Fällen wird der Code aussagekräftiger und weniger fehleranfällig (vorausgesetzt, die Basisklasse wird auf vernünftige Weise implementiert).

Ausgabe : Ich kann mir keine Situation vorstellen, in der ich lieber empfangen würde void*als etwas, das von einer bekannten Basisklasse abgeleitet ist.

Nur um klar zu machen, was ich meine: Ich frage nicht speziell, ob es einen Anwendungsfall void*gibt, sondern ob es einen Fall void*gibt, in dem die beste oder einzige verfügbare Wahl ist. Was von mehreren Personen unten perfekt beantwortet wurde.

magu_
quelle
3
Wie wäre es mit der Zeit, in der Sie mehrere Typen wie int & std :: string haben möchten?
Amir
12
@Amir, variant, any, getaggt Vereinigung. Alles, was Ihnen die tatsächliche Art des Inhalts und die Sicherheit der Verwendung mitteilen kann.
Revolver_Ocelot
15
"C hat es" ist eine hinreichend starke Rechtfertigung, keine Notwendigkeit, nach mehr zu suchen. Es ist in beiden Sprachen gut, es so weit wie möglich zu vermeiden.
n. 'Pronomen' m.
1
Nur eines: Die Interaktion mit APIs im C-Stil ist ohne sie umständlich.
Martin James
1
Eine interessante Verwendung ist das Löschen von Typen für Vektoren von Zeigern
Paolo M

Antworten:

81

void*ist zumindest als Ergebnis von ::operator new(auch jedem operator new...) und von mallocund als Argument des Platzierungsoperators notwendig new.

void*kann als der gemeinsame Supertyp jedes Zeigertyps angesehen werden. Es bedeutet also nicht genau Zeiger auf void, sondern Zeiger auf irgendetwas.

Übrigens, wenn Sie einige Daten für mehrere nicht verwandte globale Variablen behalten möchten, können Sie einige verwenden std::map<void*,int> score; , nachdem Sie global int x;und double y;und std::string s;do score[&x]=1;und score[&y]=2;und deklariert habenscore[&z]=3;

memset will eine void*Adresse (die allgemeinsten)

Außerdem haben POSIX-Systeme dlsym und sein Rückgabetyp sollte offensichtlich seinvoid*

Basile Starynkevitch
quelle
1
Das ist ein sehr guter Punkt. Ich habe vergessen zu erwähnen, dass ich eher von einem Sprachbenutzer als von der Implementierungsseite dachte. Eine weitere Sache verstehe ich allerdings nicht: Ist neu eine Funktion? Ich dachte eher an ein Schlüsselwort
magu_
9
Neu ist ein Operator wie + und sizeof.
Joshua
7
Natürlich könnte die Erinnerung auch durch eine zurückgegeben char *werden. Sie sind sich in diesem Aspekt sehr nahe, obwohl die Bedeutung etwas anders ist.
Edmz
@ Joshua. Wusste das nicht. Danke, dass du mir das beigebracht hast. Da C++in C++diesem geschrieben steht, ist Grund genug zu haben void*. Ich muss diese Seite lieben. Stellen Sie eine Frage, lernen Sie viel.
magu_
3
@black Zurück in den alten Tagen, C nicht einmal hat eine void*Art. Alle verwendeten Standardbibliotheksfunktionenchar*
Cole Johnson
28

Es gibt mehrere Gründe für die Verwendung void*, die drei häufigsten sind:

  1. Interaktion mit einer C-Bibliothek unter Verwendung void*ihrer Schnittstelle
  2. Typ-Löschung
  3. bezeichnet nicht typisierten Speicher

In umgekehrter Reihenfolge hilft das Bezeichnen von nicht typisiertem Speicher mit void*(3) anstelle von char*(oder Varianten) dabei, eine versehentliche Zeigerarithmetik zu verhindern. Es sind nur sehr wenige Operationen verfügbar, void*daher ist normalerweise ein Gießen erforderlich, bevor es nützlich ist. Und natürlich gibt es ähnlich wie char*bei Aliasing kein Problem.

Type-Erasure (2) wird in C ++ weiterhin in Verbindung mit Vorlagen verwendet oder nicht:

  • Nicht generischer Code hilft, das Aufblähen von Binärdateien zu reduzieren. Er ist in kalten Pfaden sogar in generischem Code nützlich
  • Nicht generischer Code ist manchmal für die Speicherung erforderlich, selbst in generischen Containern wie z std::function

Und wenn die Schnittstelle, mit der Sie sich befassen, verwendet wird void*(1), haben Sie offensichtlich keine andere Wahl.

Matthieu M.
quelle
15

Oh ja. Sogar in C ++ gehen wir manchmal void *eher mit als template<class T*>weil manchmal der zusätzliche Code aus der Vorlagenerweiterung zu viel wiegt.

Normalerweise würde ich es als eigentliche Implementierung des Typs verwenden, und der Vorlagentyp würde davon erben und die Casts umbrechen.

Außerdem müssen benutzerdefinierte Plattenzuordnungen (neue Implementierungen des Bedieners) verwendet werden void *. Dies ist einer der Gründe, warum g ++ eine Erweiterung für das Zulassen von Zeigerarithmatik hinzugefügt hat, void *als wäre sie von Größe 1.

Joshua
quelle
Vielen Dank für Ihre Antwort. Sie haben Recht, ich habe vergessen, Vorlagen zu erwähnen. Aber wären Vorlagen nicht noch besser für die Aufgabe geeignet, da sie selten eine Leistungsminderung einführen? ( Stackoverflow.com/questions/2442358/… )
magu_
1
Mit Vorlagen wird eine Strafe für die Codegröße eingeführt, die in den meisten Fällen auch eine Leistungsstrafe darstellt.
Joshua
struct wrapper_base {}; template<class T> struct wrapper : public wrapper_base {T val;} typedef wrapper* like_void_ptr;ist ein minimaler void - * - Simulator, der Vorlagen verwendet.
user253751
3
@ Joshua: Du wirst ein Zitat für "die meisten" brauchen.
user541686
@Mehrdad: Siehe L1-Cache.
Joshua
10

Eingabe: Wenn wir mehrere Eingabetypen zulassen möchten, können wir Funktionen und Methoden überladen

Wahr.

alternativ können wir eine gemeinsame Basisklasse definieren.

Dies ist teilweise richtig: Was ist, wenn Sie keine gemeinsame Basisklasse, Schnittstelle oder ähnliches definieren können? Um diese zu definieren, müssen Sie Zugriff auf den Quellcode haben, was häufig nicht möglich ist.

Sie haben keine Vorlagen erwähnt. Vorlagen können Ihnen jedoch beim Polymorphismus nicht helfen: Sie arbeiten mit statischen Typen, die zur Kompilierungszeit bekannt sind.

void*kann als kleinster gemeinsamer Nenner betrachtet werden. In C ++ benötigen Sie es normalerweise nicht , weil (i) Sie von Natur aus nicht viel damit anfangen können und (ii) es fast immer bessere Lösungen gibt.

Darüber hinaus werden Sie es normalerweise in andere konkrete Typen umwandeln. Aus diesem Grund char *ist dies normalerweise besser, obwohl dies möglicherweise darauf hinweist, dass Sie eher eine Zeichenfolge im C-Stil als einen reinen Datenblock erwarten. Deshalb void*ist es besser als char*das, weil es implizite Umwandlung von anderen Zeigertypen erlaubt.

Sie sollten einige Daten empfangen, damit arbeiten und eine Ausgabe erstellen. Um dies zu erreichen, müssen Sie die Daten kennen, mit denen Sie arbeiten. Andernfalls haben Sie ein anderes Problem, das nicht das ist, das Sie ursprünglich gelöst haben. Viele Sprachen haben void*und haben zum Beispiel kein Problem damit.

Eine andere legitime Verwendung

Wenn Sie Zeigeradressen mit Funktionen wie printfdem Zeiger drucken , muss der void*Typ angezeigt werden. Daher benötigen Sie möglicherweise eine Umwandlung in void*

edmz
quelle
7

Ja, es ist genauso nützlich wie alles andere in der Sprache.
Sie können es beispielsweise verwenden, um den Typ einer Klasse zu löschen, die Sie bei Bedarf statisch in den richtigen Typ umwandeln können, um eine minimale und flexible Schnittstelle zu erhalten.

In dieser Antwort gibt es ein Anwendungsbeispiel, das Ihnen eine Idee geben soll.
Ich kopiere es und füge es der Klarheit halber unten ein:

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;
};

Offensichtlich ist dies nur eine der vielen Verwendungsmöglichkeiten von void*.

Skypjack
quelle
4

Schnittstelle mit einer externen Bibliotheksfunktion, die einen Zeiger zurückgibt. Hier ist eine für eine Ada-Anwendung.

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

void* m_status_ptr = ada_function();

Dies gibt einen Zeiger auf das zurück, worüber Ada Ihnen erzählen wollte. Sie müssen nichts Besonderes damit anfangen, Sie können es Ada zurückgeben, um das nächste zu tun. Tatsächlich ist das Entwirren eines Ada-Zeigers in C ++ nicht trivial.

RedSonja
quelle
Könnten wir autoin diesem Fall nicht stattdessen verwenden? Angenommen, der Typ ist zur Kompilierungszeit bekannt.
magu_
Ah, heutzutage könnten wir wahrscheinlich. Dieser Code stammt von vor einigen Jahren, als ich einige Ada-Wrapper hergestellt habe.
RedSonja
Ada-Typen können teuflisch sein - sie machen ihre eigenen, wissen Sie, und haben eine perverse Freude daran, es schwierig zu machen. Ich durfte die Benutzeroberfläche nicht ändern, das wäre zu einfach gewesen, und es gab einige böse Dinge zurück, die in diesen leeren Zeigern versteckt waren. Pfui.
RedSonja
2

Kurz gesagt, C ++ als strenge Sprache (ohne Berücksichtigung von C-Relikten wie malloc () ) erfordert void *, da es kein gemeinsames übergeordnetes Element aller möglichen Typen hat. Im Gegensatz zu ObjC zum Beispiel, das Objekt hat .

nredko
quelle
mallocund newbeide kehren zurück void *, also würden Sie es brauchen, selbst wenn es eine Objektklasse in C ++
Dmitry Grigoryev
malloc ist Relikt, aber in strengen Sprachen neu sollte Objekt zurückgeben *
nredko
Wie ordnet man dann ein Array von ganzen Zahlen zu?
Dmitry Grigoryev
@DmitryGrigoryev operator new()kehrt zurück void *, aber der newAusdruck nicht
MM
1. Ich bin nicht sicher, ob ich ein virtuelles Basisklassenobjekt über jeder Klasse und jedem Typ sehen möchte , einschließlich int2. Wenn es sich um ein nicht virtuelles EBC handelt, wie unterscheidet es sich dann von void*?
Lorro
1

Das erste, was mir in den Sinn kommt (was meiner Meinung nach ein konkreter Fall einiger der oben genannten Antworten ist), ist die Fähigkeit, eine Objektinstanz an einen Thread-Prozess in Windows zu übergeben.

Ich habe ein paar C ++ - Klassen, die dies tun müssen, sie haben Worker-Thread-Implementierungen und der LPVOID-Parameter in der CreateThread () -API erhält eine Adresse einer statischen Methodenimplementierung in der Klasse, damit der Worker-Thread die Arbeit erledigen kann eine bestimmte Instanz der Klasse. Durch einfaches statisches Zurücksetzen im Threadproc wird die Instanz bereitgestellt, mit der gearbeitet werden kann, sodass jedes instanziierte Objekt einen Arbeitsthread aus einer einzelnen statischen Methodenimplementierung haben kann.

AndyW
quelle
0

Im Falle von Mehrfachvererbung, wenn Sie einen Zeiger auf das erste Byte eines Speicher Brocken bekommen müssen durch ein Objekt besetzt, Sie können dynamic_castzu void*.

Kleine Bedrohung
quelle