void* vs. char*



  • Heute kam in einer Diskussion der Standpunkt auf, dass es keinen Grund gäbe, void* als Argument für der aufgerufenen Funktion einzusetzen, wenn es sich um für die aufgerufene Funktion typlose Daten handelt. Mit einer unterschwelligen Botschaft, dass Leute die das dennoch machen irgendwie eine schlechte Lösung wählen. Man muss es ja immer emotional Herausfordern machen.

    Der Grund, void* herzunehmen, weil da kein falscher Gedanke aufkommt bezüglich der Semantik des angezeigten, wurde als Spitzfindigkeit abgetan.
    Andererseits habe ich nun auch keinen vernünftigeren Grund dafür gehört, uint8_t* oder char* oder was auch immer herzunehmen, als dass man darauf direkt Pointer-Arithmetik anwenden kann. Also eher ein Faulheitsargument.

    Eine schnelle Internetsuche hat auf mich den Eindruck gemacht, dass man bei dieser Frage schnell in der "Geschmacks"-Falle sitzt.

    Hat jemand von euch da rationalere Argumente pro und/oder contra?



  • Man sollte void * verwenden, um Casts zu vermeiden:

    void memcpy1(void *dst, void *src, unsigned len)
    {
        unsigned i;
        for ( i = 0; i < len; ++i )
            ((char *) dst)[i] = ((char *) src)[i];
    }
    
    void memcpy2(char *dst, char *src, unsigned len)
    {
        unsigned i;
        for ( i = 0; i < len; ++i )
            dst[i] = src[i];
    }
    
    int main()
    {
        int a, b;
    
        a = 1;
        memcpy1(&b, &a, sizeof(int));
        memcpy2(&b, &a, sizeof(int)); /* Compiler warning */
        memcpy2((char *) &b, (char *) &a, sizeof(int)); /* No warning */
    }
    


  • Naja, was bringt mir Pointer-Arithmetik auf char* anzuwenden wenn ich wenn ich eigentlich ein int* will? Müsste ich eh casten. Warum dann also char*? Deren Argument ist wahrscheinlich weil ein char ein Byte ist. O.K. kann man so sehen, aber man muß auch bedenken (und hier wird der Knackpunkt wahrscheinlich sein...), dass ein void-Zeiger implzit in andere Zeiger auf Daten übergeht, während man einen char* erst in type* typecasten müsste.

    Ich wette deine Kollegen schreiben solchen Code:

    int * func_doof (void) {
    	int *r = NULL;
    	r = (int *) malloc (100);
    	return r;
    }
    

    anstelle so:

    int * func_doll (void) {
    	int *r = NULL;
    	r = malloc (100);
    	return r;
    }
    


  • icarus2 schrieb:

    Man sollte void * verwenden, um Casts zu vermeiden:

    umgekehrt!
    void* ist gut, um rohe adressen herumzureichen.
    aber wenn man dereferenzieren will, muss man aber doch casten.



  • out of the void schrieb:

    void* ist gut, um rohe adressen herumzureichen.
    aber wenn man dereferenzieren will, muss man aber doch casten.

    Guter Punkt.



  • out of the void schrieb:

    icarus2 schrieb:

    Man sollte void * verwenden, um Casts zu vermeiden:

    umgekehrt!
    void* ist gut, um rohe adressen herumzureichen.
    aber wenn man dereferenzieren will, muss man aber doch casten.

    Damit es auch die ganz doofen verstehen: Man sollte void * verwenden, um Casts bei Funktionsaufrufen zu vermeiden.

    Sollte aus dem Kontext klar sein...



  • Meine Einstellung dazu ist, dass ich diesen Cast an der Stelle mit der Pointer-Arithmetik mache, denn dort muss ich ja sowieso argumentieren, dass das was ich da mache korrekt ist. Ansonsten verschmiere ich doch diesen Knackpunkt bloß auf viele Stellen. Aber ist das ein Argument, das "zählt"? Gibt es da überhaupt eines, das nicht auf bloße Gegenüberstellung der Anzahl der nötigen Casts hinausläuft?



  • Der echte Tim schrieb:

    int * func_doof (void) {
    	int *r = NULL;
    	r = (int *) malloc (100);
    	return r;
    }
    

    das ist besonders doof, wenn er include<stdlib.h> vergessen hat und sizeof(int)!=sizeof(void*) ist jedenfalls vor C11. casts verdecken potentielle fehler.



  • casting show schrieb:

    Der echte Tim schrieb:

    int * func_doof (void) {
    	int *r = NULL;
    	r = (int *) malloc (100);
    	return r;
    }
    

    das ist besonders doof [...]

    Das war genau sein Punkt! 🙄



  • icarus2 schrieb:

    casting show schrieb:

    Der echte Tim schrieb:

    int * func_doof (void) {
    	int *r = NULL;
    	r = (int *) malloc (100);
    	return r;
    }
    

    das ist besonders doof [...]

    Das war genau sein Punkt! 🙄

    der bleibt übrig, denn meinen punkt hast du rausgelöscht. 😉



  • casting show schrieb:

    das ist besonders doof, wenn er include<stdlib.h> vergessen hat und sizeof(int)!=sizeof(void*) ist jedenfalls vor C11. casts verdecken potentielle fehler.

    Das ist imho ein recht konstruiertes Beispiel, bzw. wenn solche Fehler passieren (also das Vergessen elementarer Header), dann ist Hopfen und Malz verloren.

    Meine Argumentation gegenüber den Leuten wäre einfach: Der Standard sieht es so vor, warum also meinen cleverer zu sein bzw. eine Konvention mit Füssen zu treten? Aber gut, ich kann mir die "Argumentation" der Kollegen vorstellen, ... ignore? 😃



  • Der echte Tim schrieb:

    casting show schrieb:

    das ist besonders doof, wenn er include<stdlib.h> vergessen hat und sizeof(int)!=sizeof(void*) ist jedenfalls vor C11. casts verdecken potentielle fehler.

    Das ist imho ein recht konstruiertes Beispiel, bzw. wenn solche Fehler passieren (also das Vergessen elementarer Header), dann ist Hopfen und Malz verloren.

    Hast du Recht. Fast jeder verwendet heute eine IDE, die on-the-fly prüft was einer reinhackt. Vergessene headers werden umgehend angemeckert oder gleich automatisch #included.

    Wer aber als Nostalgiker immer noch VI, Emacs, Notepad, NEdit u.ä. und einen alten Compiler benutzt, der wird oft nicht einmal ein Warning bekommen, wenn er eine Library-Funktion ohne Prototyp aufruft.

    Allgemein sollte man bei Casting sehr vorsichtig sein. Ist jedenfalls meine Meinung.



  • Danke für eure Standpunkte!



  • casting show schrieb:

    sizeof(int)!=sizeof(void*)

    diese bedingung ist nur ganz zufällig auf bestimmten systemen erfüllt.
    allgemein steht da aber äpfel != birnen, weil das eine die adressbreite ist und das andere die anzahl der bytes, die eine variable im speicher belegt.

    RollerCaster schrieb:

    Allgemein sollte man bei Casting sehr vorsichtig sein. Ist jedenfalls meine Meinung.

    ein möglicher grund für void-zeiger wäre z.b. sowas:

    void Zeichenfunktion(void *zeichenobjekt, int objekttyp)
    {
    switch(objekttyp)
    {
    case KREIS:
    ZeichneKreis(zeichenobjekt);
    break;
    case RECHTECK:
    ZeichneRechteck(zeichenobjekt);
    break;
    }
    }
    

    der grund dafür wäre, dass sich das "hauptprogramm" nicht weiter darum zu kümmern braucht, welche funktion aufgerufen werden muss, um die gewünschte figur zu zeichnen, was der übersichtlichkeit wieder zugute kommt.



  • Was soll dieses Geflenne, ob void* oder char*?
    Wenn ein Typ undefiniert ist, dann nimmt man void*, da braucht man nicht zu diskutieren.
    Gut, es mag sein, daß das für einen C-Programmierer schon etwas zu abstrakt ist, aber diese Abstraktion sollte man schon einer Hochsprache zuerkennen. 😉



  • Das ist natürlich Quatsch.
    Erstmal ist void* kein undefinierter Typ sondern ein definierter Typ.
    Und weiterhin ist Sinn und Zweck eines (Daten)Zeigers die Dereferenzierung,
    und da wird bei void* nun mal der originale Ausgangstyp (oder kompatibel) benötigt um type punning und damit UB zu vermeiden.
    Ausschließlich für Zwecke, bei denen der Speicher als rohe Bytefolge verarbeitet werden kann/soll, ist void* ohne Originaltypinfo ohne UB verwendbar, da hierbei mit char/signed char/unsigned char ohne strict aliasing break(d.h. ohne UB) gefahrlos gearbeitet werden kann, da void* genau das gleiche (und beste) Alignment gemäß Standard besitzt wie char/signed char/unsigned char.
    Beispiele sind memcpy,fread,fwrite



  • Wutz schrieb:

    ... da void* genau das gleiche (und beste) Alignment gemäß Standard besitzt wie char/signed char/unsigned char.
    Beispiele sind memcpy,fread,fwrite

    Einn schnelles memcpy schaufelt niemals byteweise, sondern mit den größten Schaufeln, die die CPU zur Verfügung stellt. Einzelne Bytes nur für den Rest, oder wenn weniger gefragt ist. 🙂



  • Du hast keine Ahnung, wovon du redest. Also halt die Klappe.
    Wenn du nicht weißt, was Alignment bedeutet, dann halt die Klappe und zeige nicht öffentlich deine Inkompetenz.
    Wenn du nicht weißt, dass der Standard keinerlei Implementierungsvorschriften für Standardfunktionen vorschreibt, dann halt die Klappe und zeige nicht öffentlich deine Inkompetenz.
    Wenn du nicht weißt, dass das Alignmentproblem naturgemäß immer vor jeglicher konkreter Kopieraktion durch die CPU zuschlägt, dann halt die Klappe und zeige nicht öffentlich deine Inkompetenz.



  • Wutz schrieb:

    ... dass das Alignmentproblem naturgemäß immer vor jeglicher konkreter Kopieraktion durch die CPU zuschlägt
    ...

    Aus diesem Grund muss der Speicher in drei Schritten kopiert werden:

    1. Byteweises Kopieren bis zur Alignment-Grenze,
    2. Turbo einschalten: (DMA, Schleife unter Nutzung voller Registerbreiten, usw.),
    3. Byteweises Kopieren des Rests.

    Die Argumente der memcpy-Funktion bieten alles, was für dafür nötig ist.

    Aber es ist wohl müssig, das einem Informatikstudium-Abbrecher wie dir zu erzählen, der außer Uni-Übungen und privaten Gehversuchen in C diesbezüglich noch nichts geleistet hat.



  • Kritiker1 schrieb:

    Wutz schrieb:

    ... dass das Alignmentproblem naturgemäß immer vor jeglicher konkreter Kopieraktion durch die CPU zuschlägt
    ...

    Aus diesem Grund muss der Speicher in drei Schritten kopiert werden:

    ...

    Aber es ist wohl müssig, das einem Informatikstudium-Abbrecher wie dir zu erzählen, der außer Uni-Übungen und privaten Gehversuchen in C diesbezüglich noch nichts geleistet hat.

    Klingt so, als ob Wutz was anderes behauptet hätte, hat er aber doch nicht?!


Anmelden zum Antworten