Y a-t-il une utilisation légitime de void *?

87

Y a-t-il une utilisation légitime de void*en C ++? Ou cela a-t-il été introduit parce que C l'avait?

Juste pour récapituler mes pensées:

Entrée : Si nous voulons autoriser plusieurs types d'entrée, nous pouvons surcharger les fonctions et les méthodes, sinon nous pouvons définir une classe de base commune, ou un modèle (merci de l'avoir mentionné dans les réponses). Dans les deux cas, le code est plus descriptif et moins sujet aux erreurs (à condition que la classe de base soit implémentée de manière sensée).

Sortie : Je ne peux penser à aucune situation où je préférerais recevoir void*plutôt que quelque chose dérivé d'une classe de base connue.

Juste pour clarifier ce que je veux dire: je ne demande pas spécifiquement s'il y a un cas d'utilisation pour void*, mais s'il y a un cas où void*est le meilleur ou le seul choix disponible. Ce qui a été parfaitement répondu par plusieurs personnes ci-dessous.

magu_
la source
3
Que diriez-vous du moment où vous voulez avoir plusieurs types comme int & std :: string?
Amir
12
@Amir, variant, any, union étiquetée. Tout ce qui peut vous indiquer le type de contenu réel et plus sûr à utiliser.
Revolver_Ocelot
15
"C l'a" est une justification assez forte, pas besoin de chercher plus. Éviter autant que possible est une bonne chose dans l'une ou l'autre langue.
n. «pronoms» m.
1
Juste une chose: l'interopérabilité avec les API de style C est difficile sans elle.
Martin James
1
Un usage intéressant est l' effacement de type pour les vecteurs de pointeurs
Paolo M

Réponses:

81

void*est au moins nécessaire en tant que résultat de ::operator new(également tous les operator new...) et de mallocet comme argument de l' newopérateur de placement .

void*peut être considéré comme le supertype commun de chaque type de pointeur. Il ne s'agit donc pas exactement d'un pointeur vers void, mais d'un pointeur vers n'importe quoi.

BTW, si vous souhaitez conserver des données pour plusieurs variables globales non liées, vous pouvez en utiliser std::map<void*,int> score; ensuite, après avoir déclaré global int x;and double y;et std::string s;faire score[&x]=1;et score[&y]=2;etscore[&z]=3;

memset veut une void*adresse (les plus génériques)

De plus, les systèmes POSIX ont dlsym et son type de retour devrait évidemment êtrevoid*

Basile Starynkevitch
la source
1
C'est un très bon point. J'ai oublié de mentionner que je pensais davantage à un utilisateur de la langue qu'à l'implémentation. Une dernière chose que je ne comprends pas: est-ce qu'une fonction est nouvelle? J'y ai pensé plus comme un mot
magu_
9
new est un opérateur, comme + et sizeof.
Joshua
7
Bien sûr, la mémoire pourrait être renvoyée par un char *aussi. Ils sont très proches sur cet aspect, même si le sens est un peu différent.
edmz
@Joshua. Je ne savais pas ça. Merci de m'avoir appris cela. Depuis C++est écrit dans C++c'est une raison suffisante pour avoir void*. Je dois adorer ce site. Posez une question et apprenez beaucoup.
magu_
3
@black Retour dans les temps anciens, C n'a même pas avoir un void*genre. Toutes les fonctions de bibliothèque standard utiliséeschar*
Cole Johnson
28

Il y a plusieurs raisons d'utiliser void*, les 3 plus courantes étant:

  1. interagir avec une bibliothèque C en utilisant void*dans son interface
  2. effacement de type
  3. indiquant une mémoire non typée

Dans l'ordre inverse, désigner la mémoire non typée avec void*(3) au lieu de char*(ou des variantes) permet d'éviter l'arithmétique accidentelle des pointeurs; il y a très peu d'opérations disponibles void*, il faut donc généralement lancer un casting avant d'être utile. Et bien sûr, tout comme avec char*il n'y a aucun problème avec l'aliasing.

L'effacement de type (2) est toujours utilisé en C ++, en conjonction avec des templates ou non:

  • le code non générique aide à réduire le gonflement binaire, il est utile dans les chemins froids même dans le code générique
  • du code non générique est parfois nécessaire pour le stockage, même dans un conteneur générique tel que std::function

Et évidemment, lorsque l'interface que vous traitez utilise void*(1), vous n'avez guère le choix.

Matthieu M.
la source
15

Oh oui. Même en C ++, nous choisissons parfois void *plutôt que template<class T*>parce que parfois le code supplémentaire de l'expansion du modèle pèse trop.

Généralement, je l'utiliserais comme implémentation réelle du type, et le type de modèle en hériterait et encapsulerait les moulages.

En outre, les allocateurs de dalles personnalisés (nouvelles implémentations d'opérateur) doivent utiliser void *. C'est l'une des raisons pour lesquelles g ++ a ajouté une extension permettant l'arithmatique du pointeur void *comme s'il était de taille 1.

Joshua
la source
Merci pour votre réponse. Vous avez raison, j'ai oublié de mentionner les modèles. Mais les modèles ne seraient-ils pas encore mieux adaptés à la tâche, car ils introduisent rarement une pénalité de performance? ( Stackoverflow.com/questions/2442358/… )
magu_
1
Les modèles introduisent une pénalité de taille de code, qui est également une pénalité de performances dans la plupart des cas.
Joshua
struct wrapper_base {}; template<class T> struct wrapper : public wrapper_base {T val;} typedef wrapper* like_void_ptr;est un simulateur de vide minimal - * - utilisant des modèles.
user253751
3
@Joshua: Vous allez avoir besoin d'une citation pour "la plupart".
user541686
@Mehrdad: Voir cache L1.
Joshua
10

Entrée: si nous voulons autoriser plusieurs types d'entrée, nous pouvons surcharger les fonctions et méthodes

Vrai.

alternativement, nous pouvons définir une classe de base commune.

C'est partiellement vrai: que se passe-t-il si vous ne pouvez pas définir une classe de base commune, une interface ou similaire? Pour les définir, vous devez avoir accès au code source, ce qui n'est souvent pas possible.

Vous n'avez pas mentionné de modèles. Cependant, les templates ne peuvent pas vous aider avec le polymorphisme: ils fonctionnent avec des types statiques c'est à dire connus au moment de la compilation.

void*peut être considéré comme le plus petit dénominateur commun. En C ++, vous n'en avez généralement pas besoin car (i) vous ne pouvez pas en faire grand chose en soi et (ii) il y a presque toujours de meilleures solutions.

Encore plus loin, vous finirez généralement par le convertir en d'autres types de béton. C'est pourquoi char *c'est généralement mieux, bien que cela puisse indiquer que vous attendez une chaîne de style C, plutôt qu'un bloc de données pur. C'est pourquoi void*c'est mieux que char*pour cela, car cela permet une conversion implicite à partir d'autres types de pointeurs.

Vous êtes censé recevoir des données, travailler avec elles et produire une sortie; pour y parvenir, vous devez connaître les données avec lesquelles vous travaillez, sinon vous avez un problème différent qui n'est pas celui que vous résolviez à l'origine. De nombreuses langues n'ont pas void*et n'ont aucun problème avec cela, par exemple.

Une autre utilisation légitime

Lors de l'impression des adresses de pointeur avec des fonctions telles que printfle pointeur doit avoir le void*type et, par conséquent, vous devrez peut-être un cast en void*

edmz
la source
7

Oui, c'est aussi utile que n'importe quelle autre chose dans la langue.
Par exemple, vous pouvez l'utiliser pour effacer le type d'une classe que vous êtes capable de convertir statiquement dans le bon type en cas de besoin, afin d'avoir une interface minimale et flexible.

Dans cette réponse, il y a un exemple d'utilisation qui devrait vous donner une idée.
Je le copie et le colle ci-dessous par souci de clarté:

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

De toute évidence, ce n'est que l'une des utilisations de void*.

skypjack
la source
4

Interfaçage avec une fonction de bibliothèque externe qui renvoie un pointeur. En voici un pour une application Ada.

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

void* m_status_ptr = ada_function();

Cela renvoie un pointeur sur tout ce dont Ada voulait vous parler. Vous n'avez rien à faire de fantaisie avec, vous pouvez le rendre à Ada pour faire la prochaine chose. En fait, démêler un pointeur Ada en C ++ n'est pas trivial.

RedSonja
la source
Ne pourrions-nous pas utiliser à la autoplace dans ce cas? En supposant que le type est connu au moment de la compilation.
magu_
Ah, de nos jours, nous pourrions probablement. Ce code date d'il y a quelques années, lorsque j'ai créé des wrappers Ada.
RedSonja
Les types Ada peuvent être diaboliques - ils font les leurs, vous savez, et prennent un plaisir pervers à rendre les choses délicates. Je n'ai pas été autorisé à changer l'interface, cela aurait été trop facile, et cela a renvoyé des trucs désagréables cachés dans ces pointeurs vides. Pouah.
RedSonja
2

En bref, C ++ en tant que langage strict (ne prenant pas en compte les reliques C comme malloc () ) nécessite void * car il n'a pas de parent commun de tous les types possibles. Contrairement à ObjC, par exemple, qui a un objet .

Nredko
la source
mallocet les newdeux reviennent void *, vous auriez donc besoin si même s'il y avait une classe d'objet en C ++
Dmitry Grigoryev
malloc est relique, mais dans des langages stricts, le nouveau devrait renvoyer un objet *
nredko
comment allouez-vous alors un tableau d'entiers?
Dmitry Grigoryev
@DmitryGrigoryev operator new()retourne void *, mais l' newexpression ne le fait pas
MM
1. Je ne suis pas sûr que j'aimerais voir un objet de classe de base virtuelle au -dessus de chaque classe et type , y compris int2. s'il s'agit d'EBC non virtuel, en quoi est-il différent de void*?
lorro
1

La première chose qui me vient à l'esprit (ce que je soupçonne est un cas concret de quelques-unes des réponses ci-dessus) est la capacité de passer une instance d'objet à un threadproc dans Windows.

J'ai quelques classes C ++ qui doivent faire cela, elles ont des implémentations de thread de travail et le paramètre LPVOID dans l'API CreateThread () obtient une adresse d'implémentation de méthode statique dans la classe afin que le thread de travail puisse faire le travail avec une instance spécifique de la classe. Un simple cast statique dans le threadproc produit l'instance avec laquelle travailler, permettant à chaque objet instancié d'avoir un thread de travail à partir d'une seule implémentation de méthode statique.

AndyW
la source
0

En cas d'héritage multiple, si vous avez besoin d'obtenir un pointeur vers le premier octet d'un bloc de mémoire occupé par un objet, vous pouvez le dynamic_castfaire void*.

Menace mineure
la source