¿Existe un uso legítimo para void *?

87

¿Existe un uso legítimo de void*en C ++? ¿O se introdujo porque C lo tenía?

Solo para recapitular mis pensamientos:

Entrada : Si queremos permitir múltiples tipos de entrada, podemos sobrecargar funciones y métodos, alternativamente podemos definir una clase base común o plantilla (gracias por mencionar esto en las respuestas). En ambos casos, el código se vuelve más descriptivo y menos propenso a errores (siempre que la clase base se implemente de una manera sensata).

Resultado : No puedo pensar en ninguna situación en la que prefiera recibir void*en lugar de algo derivado de una clase base conocida.

Solo para dejar en claro lo que quiero decir: no estoy preguntando específicamente si hay un caso de uso para void*, sino si hay un caso en el que void*sea ​​la mejor o la única opción disponible. Lo cual ha sido perfectamente respondido por varias personas a continuación.

magu_
fuente
3
¿Qué tal el momento en que desea tener varios tipos como int & std :: string?
Amir
12
@Amir, variant, any, etiquetada unión. Cualquier cosa que pueda decirle el tipo real de contenido y que sea más seguro de usar.
Revolver_Ocelot
15
"C lo tiene" es una justificación suficientemente fuerte, no hay necesidad de buscar más. Evitarlo tanto como sea posible es algo bueno en cualquier idioma.
n. 'pronombres' m.
1
Solo una cosa: la interoperabilidad con API de estilo C es incómoda sin ella.
Martin James
1
Un uso interesante es el borrado de tipos para vectores de punteros
Paolo M

Respuestas:

81

void*es al menos necesario como resultado de ::operator new(también cada operator new...) y de mallocy como argumento del newoperador de colocación .

void*se puede pensar como el supertipo común de todo tipo de puntero. Por lo tanto, no significa exactamente puntero void, sino puntero a cualquier cosa.

Por cierto, si desea conservar algunos datos para varias variables globales no relacionadas, puede usar algunos std::map<void*,int> score; luego, después de haber declarado global int x;and double y;and std::string s;do score[&x]=1;and score[&y]=2;andscore[&z]=3;

memset quiere una void*dirección (las más genéricas)

Además, los sistemas POSIX tienen dlsym y su tipo de retorno evidentemente debería servoid*

Basile Starynkevitch
fuente
1
Ese es un buen punto. Olvidé mencionar que estaba pensando más en el usuario del lenguaje que en la implementación. Sin embargo, una cosa más que no entiendo: ¿es nueva una función? Lo pensé más como una palabra clave
magu_
9
new es un operador, como + y sizeof.
Joshua
7
Por supuesto, la memoria también se puede devolver a través de un char *. Están muy cerca en este aspecto, aunque el significado es un poco diferente.
edmz
@Joshua. No lo sabía. Gracias por enseñarme esto. Ya que C++está escrito en C++esto es motivo suficiente para tenerlo void*. Me encanta este sitio. Haz una pregunta y aprende mucho.
magu_
3
@black En los viejos tiempos, C ni siquiera tenía un void*tipo. Todas las funciones de biblioteca estándar utilizadaschar*
Cole Johnson
28

Existen múltiples razones para usarlo void*, siendo las 3 más comunes:

  1. interactuando con una biblioteca C usando void*en su interfaz
  2. borrado de tipo
  3. que denota memoria no escrita

En orden inverso, denotar memoria sin tipear con void*(3) en lugar de char*(o variantes) ayuda a prevenir la aritmética de puntero accidental; hay muy pocas operaciones disponibles en, void*por lo que generalmente requiere fundición antes de ser útil. Y, por supuesto, al igual char*que con el aliasing, no hay problema.

Type-erasure (2) todavía se usa en C ++, junto con plantillas o no:

  • el código no genérico ayuda a reducir la hinchazón binaria, es útil en rutas frías incluso en código genérico
  • El código no genérico es necesario para el almacenamiento a veces, incluso en un contenedor genérico como std::function

Y obviamente, cuando la interfaz con la que trabaja usa void*(1), tiene pocas opciones.

Matthieu M.
fuente
15

Oh si. Incluso en C ++ a veces vamos con void *más que template<class T*>porque a veces el código extra de la expansión de la plantilla pesa demasiado.

Por lo general, lo usaría como la implementación real del tipo, y el tipo de plantilla lo heredaría y envolvería los moldes.

Además, los asignadores de losas personalizados (nuevas implementaciones del operador) deben usar void *. Esta es una de las razones por las que g ++ agregó una extensión para permitir la aritmática del puntero void *como si fuera de tamaño 1.

Joshua
fuente
Gracias por su respuesta. Tienes razón, olvidé mencionar las plantillas. Pero, ¿las plantillas no serían aún más adecuadas para la tarea ya que rara vez introducen una penalización de rendimiento? ( Stackoverflow.com/questions/2442358/… )
magu_
1
Las plantillas introducen una penalización en el tamaño del código, que también es una penalización en el rendimiento en la mayoría de los casos.
Joshua
struct wrapper_base {}; template<class T> struct wrapper : public wrapper_base {T val;} typedef wrapper* like_void_ptr;es un simulador de vacío mínimo - * - que utiliza plantillas.
user253751
3
@Joshua: Vas a necesitar una cita para "la mayoría".
user541686
@Mehrdad: Ver caché L1.
Joshua
10

Entrada: si queremos permitir múltiples tipos de entrada, podemos sobrecargar funciones y métodos

Cierto.

alternativamente, podemos definir una clase base común.

Esto es parcialmente cierto: ¿qué pasa si no puede definir una clase base común, una interfaz o similar? Para definirlos, debe tener acceso al código fuente, lo que a menudo no es posible.

No mencionaste las plantillas. Sin embargo, las plantillas no pueden ayudarlo con el polimorfismo: funcionan con tipos estáticos, es decir, conocidos en tiempo de compilación.

void*puede considerarse como el mínimo común denominador. En C ++, normalmente no lo necesita porque (i) no puede hacer mucho con él por naturaleza y (ii) casi siempre hay mejores soluciones.

Aún más, normalmente terminará convirtiéndolo en otros tipos de concreto. Por eso char *suele ser mejor, aunque puede indicar que está esperando una cadena de estilo C, en lugar de un bloque puro de datos. Por eso void*es mejor que char*eso, porque permite la conversión implícita de otros tipos de punteros.

Se supone que debe recibir algunos datos, trabajar con ellos y producir una salida; para lograrlo, necesita conocer los datos con los que está trabajando; de lo contrario, tiene un problema diferente que no es el que estaba resolviendo originalmente. Muchos idiomas no tienen void*y no tienen ningún problema con eso, por ejemplo.

Otro uso legítimo

Al imprimir direcciones de puntero con funciones como printfel puntero, debe tener void*tipo y, por lo tanto, es posible que necesite una conversión a void*

edmz
fuente
7

Sí, es tan útil como cualquier otra cosa en el idioma.
Como ejemplo, puede usarlo para borrar el tipo de una clase que puede convertir estáticamente al tipo correcto cuando sea necesario, para tener una interfaz mínima y flexible.

En esa respuesta hay un ejemplo de uso que debería darte una idea.
Lo copio y pego a continuación en aras de la claridad:

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

Obviamente, este es solo uno de los muchos usos de void*.

skypjack
fuente
4

Interfaz con una función de biblioteca externa que devuelve un puntero. Aquí hay uno para una aplicación Ada.

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

void* m_status_ptr = ada_function();

Esto devuelve un puntero a lo que sea que Ada quería contarte. No tienes que hacer nada elegante con él, puedes devolvérselo a Ada para que haga lo siguiente. De hecho, desenredar un puntero Ada en C ++ no es trivial.

RedSonja
fuente
¿No podríamos usar autoen su lugar en este caso? Suponiendo que el tipo se conoce en tiempo de compilación.
magu_
Ah, hoy en día probablemente podríamos. Ese código es de hace algunos años cuando hice algunos envoltorios de Ada.
RedSonja
Los tipos Ada pueden ser diabólicos: hacen los suyos, ya sabes, y disfrutan perversamente de hacerlo complicado. No se me permitió cambiar la interfaz, eso habría sido demasiado fácil, y devolvió algunas cosas desagradables escondidas en esos punteros vacíos. Ugh.
RedSonja
2

En resumen, C ++ como lenguaje estricto (sin tener en cuenta las reliquias de C como malloc () ) requiere void * ya que no tiene un padre común de todos los tipos posibles. A diferencia de ObjC, por ejemplo, que tiene objeto .

nredko
fuente
mallocy newambos regresan void *, por lo que necesitaría incluso si hubiera una clase de objeto en C ++
Dmitry Grigoryev
malloc es un relicto, pero en lenguajes estrictos el nuevo debe devolver el objeto *
nredko
Entonces, ¿cómo se asigna una matriz de números enteros?
Dmitry Grigoryev
@DmitryGrigoryev operator new()regresa void *, pero la newexpresión no
MM
1. No estoy seguro de que me gustaría ver un objeto de clase base virtual por encima de cada clase y tipo , incluido int2. si es EBC no virtual, ¿en qué se diferencia de void*?
lorro
1

Lo primero que se me ocurre (sospecho que es un caso concreto de algunas de las respuestas anteriores) es la capacidad de pasar una instancia de objeto a un threadproc en Windows.

Tengo un par de clases de C ++ que necesitan hacer esto, tienen implementaciones de subprocesos de trabajo y el parámetro LPVOID en la API CreateThread () obtiene una dirección de una implementación de método estático en la clase para que el subproceso de trabajo pueda hacer el trabajo con una instancia específica de la clase. La conversión estática simple en el threadproc produce la instancia con la que trabajar, lo que permite que cada objeto instanciado tenga un subproceso de trabajo a partir de una implementación de método estático único.

AndyW
fuente
0

En caso de herencia múltiple, si necesita obtener un puntero al primer byte de un fragmento de memoria ocupado por un objeto, puede dynamic_casthacerlo void*.

Amenaza menor
fuente