¿Por qué C ++ no le permite tomar la dirección de un constructor?

14

¿Existe una razón específica por la que esto rompería el lenguaje conceptualmente o una razón específica por la que esto es técnicamente inviable en algunos casos?

El uso sería con un nuevo operador.

Editar: Voy a renunciar a la esperanza de tener a mi "nuevo operador" y "operador nuevo" directamente y ser directo.

El punto de la pregunta es: ¿por qué los constructores son especiales ? Tenga en cuenta, por supuesto, que las especificaciones de idioma nos dicen qué es legal, pero no necesariamente moral. Lo que es legal generalmente se informa por lo que es lógicamente consistente con el resto del lenguaje, lo que es simple y conciso, y lo que es factible que los compiladores implementen. La posible justificación del comité de normas para sopesar estos factores es deliberada e interesante, de ahí la pregunta.

Praxeolítico
fuente
No sería un problema tomar la dirección de un constructor, sino poder pasar el tipo. Las plantillas pueden hacer eso.
Eufórico
¿Qué sucede si tiene una plantilla de función que desea construir un objeto utilizando un constructor que se especificará como argumento de la función?
Praxeolítico
1
Habrá alternativas para cualquier ejemplo que pueda pensar, pero aún así, ¿por qué los constructores deberían ser especiales? Hay muchas cosas que probablemente no usará en la mayoría de los lenguajes de programación, pero los casos especiales como este generalmente tienen una justificación.
Praxeolítico
1
@RobertHarvey La pregunta se me ocurrió cuando estaba a punto de escribir una clase de fábrica.
Praxeolítico
1
Me pregunto si el C ++ 11 std::make_uniquey std::make_sharedpuede resolver adecuadamente la motivación práctica subyacente para esta pregunta. Estos son métodos de plantilla, lo que significa que uno necesita capturar los argumentos de entrada para el constructor y luego reenviarlos al constructor real.
rwong

Respuestas:

10

Las funciones de punteros a miembro solo tienen sentido si tiene más de una función de miembro con la misma firma; de lo contrario, solo habría un valor posible para su puntero. Pero eso no es posible para los constructores, ya que en C ++ diferentes constructores de la misma clase deben tener firmas diferentes.

La alternativa para Stroustrup hubiera sido elegir una sintaxis para C ++ donde los constructores pudieran tener un nombre diferente al nombre de la clase, pero eso habría evitado algunos aspectos muy elegantes de la sintaxis ctor existente y habría complicado el lenguaje. Para mí, eso parece un precio alto solo para permitir una característica rara vez necesaria que se puede simular fácilmente mediante la "subcontratación" de la inicialización de un objeto desde el ctor a una initfunción diferente (una función miembro normal para la que se puede puntero a miembros creado).

Doc Brown
fuente
2
Aún así, ¿por qué prevenir memcpy(buffer, (&std::string)(int, char), size)? (Probablemente extremadamente poco kosher, pero esto es C ++ después de todo).
Thomas Eding
3
lo siento, pero lo que escribiste no tiene sentido. No veo nada de malo en tener un puntero al miembro apuntando a un constructor. Además, parece que has citado algo, sin enlace a la fuente.
B 21овић
1
@ThomasEding: ¿qué esperas exactamente que haga esa declaración? Copiando el código de ensamblaje de la cadena ctor somewgere? ¿Cómo se determinará el "tamaño" (incluso si intenta algo equivalente para una función miembro estándar)?
Doc Brown
Esperaría que hiciera lo mismo que haría con la dirección de un puntero de función libre memcpy(buffer, strlen, size). Presumiblemente copiaría la asamblea, pero quién sabe. Si se puede invocar o no el código sin fallar, se requiere conocimiento sobre el compilador que utiliza. Lo mismo vale para determinar el tamaño. Sería muy dependiente de la plataforma, pero en el código de producción se utilizan muchas construcciones de C ++ no portátiles. No veo ninguna razón para prohibirlo.
Thomas Eding
@ThomasEding: se espera que un compilador de C ++ conforme dé un diagnóstico cuando intente acceder a un puntero de función como si fuera un puntero de datos. Un compilador de C ++ no conforme podría hacer cualquier cosa, pero también puede proporcionar una forma de acceso a un constructor que no sea C ++. Esa no es una razón para agregar una característica a C ++ que no tiene usos en el código conforme.
Bart van Ingen Schenau
5

Un constructor es una función que se llama cuando el objeto aún no existe, por lo que no podría ser una función miembro. Podría ser estático.

En realidad, se llama a un constructor con este puntero, después de que se haya asignado la memoria pero antes de que se haya inicializado por completo. Como consecuencia, un constructor tiene una serie de características privilegiadas.

Si tuviera un puntero a un constructor, tendría que ser un puntero estático, algo así como una función de fábrica o un puntero especial a algo que se llamaría inmediatamente después de la asignación de memoria. No podría ser una función miembro ordinaria y seguir funcionando como constructor.

El único propósito útil que viene a la mente es un tipo especial de puntero que podría pasarse al nuevo operador para permitirle indirectamente sobre qué constructor usar. Supongo que eso podría ser útil, pero requeriría una nueva sintaxis significativa y presumiblemente la respuesta es: lo pensaron y no valió la pena el esfuerzo.

Si solo desea refactorizar un código de inicialización común, una función de memoria ordinaria suele ser una respuesta suficiente, y puede obtener un puntero a uno de esos.

david.pfx
fuente
Esta parece ser la respuesta más correcta. Recuerdo un artículo de hace muchos (muchos) años sobre el operador nuevo y el funcionamiento interno de "el nuevo operador". El operador new () asigna espacio. El nuevo operador llama al constructor con ese espacio asignado. Tomar la dirección de un constructor es "especial" porque llamar al constructor requiere espacio. El acceso para llamar a un constructor como este es con la ubicación nueva.
Bill Door
1
La palabra "existe" oscurece el detalle de que un objeto puede tener una dirección y tener memoria asignada pero que no se puede inicializar. En función de miembro o no, creo que obtener este puntero hace que una función sea una función miembro porque está claramente asociada con una instancia de objeto (incluso si no está inicializada). Dicho esto, la respuesta plantea un buen punto: el constructor es la única función miembro que se puede invocar en un objeto no inicializado.
Praxeolitic
No importa, aparentemente tienen la designación de "funciones miembro especiales". Cláusula 12 del estándar C ++ 11: "El constructor predeterminado (12.1), el constructor de copia y el operador de asignación de copia (12.8), el constructor de movimiento y el operador de asignación de movimiento (12.8) y el destructor (12.4) son funciones miembro especiales ".
Praxeolitic
Y 12.1: "Un constructor no debe ser virtual (10.3) o estático (9.4)". (mi énfasis)
Praxeolitic
1
El hecho es que si compila con símbolos de depuración y busca una traza de pila, en realidad hay un puntero al constructor. Lo que nunca fue capaz de encontrar es la sintaxis para obtener este puntero ( &A::Ano funciona en cualquiera de los compiladores que he probado.)
ALFC
-3

Esto se debe a que no es un tipo de constructor de retorno y no está reservando ningún espacio para el constructor en la memoria. Como lo hace en caso de variable durante la declaración. Por ejemplo: si escribe una variable simple X Entonces el compilador generará un error porque el compilador no entenderá el significado de esto. Pero cuando escribes Int x; Luego, el compilador se da cuenta de que int escribe la variable de datos, por lo que reservará algo de espacio para la variable.

Conclusión: la conclusión es que, debido a la exclusión del tipo de retorno, no obtendrá la dirección en la memoria.

adorable Goyal
fuente
1
El código en el constructor debe tener una dirección en la memoria porque debe estar en algún lugar. No es necesario reservar espacio en la pila, pero debe estar en algún lugar de la memoria. Usted puede tomar la dirección de las funciones que no devuelven valores. (void)(*fptr)()declara un puntero a una función sin valor de retorno.
Praxeolítico
2
Te perdiste el punto de la pregunta: la publicación original preguntaba por tomar la dirección del código para el constructor, no el resultado que proporcionó el constructor. Además, en este foro, utilice palabras completas: "u" no es un reemplazo aceptable para "usted".
BobDalgleish
Señor praxeolitic, creo que si no mencionamos ningún tipo de retorno, el compilador no establecerá una ubicación de memoria particular para ctor y su ubicación se configura internamente ... ¿Podemos obtener la dirección de cualquier cosa en c ++ que no esté dada por ¿compilador? Si estoy equivocado
corrígeme
Y también cuéntame sobre la variable de referencia. ¿Podemos obtener la dirección de la variable de referencia? Si no, entonces qué dirección printf ("% u", & (& (j))); está imprimiendo si & j = x donde x = 10? Debido a que la dirección impresa por printf y la dirección de x no son las mismas
Goyal adora el
-4

Voy a adivinar:

El constructor y destructor de C ++ no son funciones en absoluto: son macros. Se insertan en el alcance donde se crea el objeto y el alcance donde se destruye el objeto. A su vez, no hay constructor ni destructor, el objeto simplemente ES.

En realidad, creo que las otras funciones en la clase tampoco son funciones, sino funciones en línea que NO se insertan porque tomas la dirección de ellas (el compilador se da cuenta de que estás en él y no inserta o inserta el código en la función y optimiza esa función) y, a su vez, la función parece "seguir estando allí", aunque no lo haría si no se hubiera tomado la dirección.

La tabla virtual del "objeto" C ++ no es como un objeto JavaScript, donde puede obtener su 'constructor y crear objetos a partir de él en tiempo de ejecución new XMLHttpRequest.constructor, sino más bien una colección de punteros a funciones anónimas que actúan como medios para interactuar con este objeto , excluyendo la capacidad de crear el objeto. Y ni siquiera tiene sentido "eliminar" el objeto, porque es como tratar de eliminar una estructura, no puedes: es solo una etiqueta de pila, solo escríbela como quieras debajo de otra etiqueta: eres libre de usa una clase como 4 enteros:

/* i imagine this string gets compiled into a struct, one of which's members happens to be a const char * which is initialized to exactly your string: no function calls are made during construction. */
std::string a = "hello, world";
int *myInt = (int *)(*((void **)&a));
myInt[0] = 3;
myInt[1] = 9;
myInt[2] = 20;
myInt[3] = 300;

No hay pérdida de memoria, no hay problemas, excepto que efectivamente desperdició un montón de espacio de pila reservado para la interfaz de objetos y la cadena, pero no va a destruir su programa (siempre y cuando no intente usarlo como una cuerda nunca más).

En realidad, si mis suposiciones anteriores son correctas: el costo completo de la cadena es solo el costo de almacenar estos 32 bytes y el espacio constante de la cadena: las funciones solo se usan en el momento de la compilación, y también pueden alinearse y desecharse después el objeto se crea y se usa (como si estuviera trabajando con una estructura y solo se refiriera a él directamente sin llamadas a funciones, asegúrese de que haya llamadas duplicadas en lugar de saltos de funciones, pero esto generalmente es más rápido y usa menos espacio). En esencia, cada vez que llama a cualquier función, el compilador simplemente reemplaza esa llamada con las instrucciones para hacerlo literalmente, con las excepciones que los diseñadores de idiomas han establecido.

Resumen: los objetos C ++ no tienen idea de lo que son; Todas las herramientas para interactuar con ellas están alineadas estáticamente y se pierden en tiempo de ejecución. Esto hace que trabajar con clases sea tan eficiente como llenar estructuras con datos y trabajar directamente con esos datos sin llamar a ninguna función (estas funciones están en línea).

Esto es completamente diferente de los enfoques de COM / ObjectiveC y javascript, que retienen la información de tipo dinámicamente, a costa de tiempo de ejecución, administración de memoria, llamadas de construcciones, ya que el compilador no puede tirar esta información: es necesario para despacho dinámico. Esto a su vez nos da la capacidad de "Hablar" con nuestro programa en tiempo de ejecución y desarrollarlo mientras se ejecuta al tener componentes reflectantes.

Dmitry
fuente
2
lo siento, pero algunas partes de esta "respuesta" son incorrectas o peligrosamente engañosas. Lamentablemente, el espacio de comentarios es demasiado pequeño para enumerarlos a todos (la mayoría de los métodos no se alinearán, esto evitaría el despacho virtual e hincharía el binario; incluso si está alineado, puede haber una copia direccionable en algún lugar accesible; ejemplo de código irrelevante que en el peor de los casos corrompe su pila y en el mejor de los casos no se ajusta a sus suposiciones; ...)
hoffmale
La respuesta es ingenua, solo quería expresar mi suposición de por qué no se puede hacer referencia al constructor / destructor. Estoy de acuerdo en que en el caso de las clases virtuales, la tabla debe persistir y el código direccionable debe estar en la memoria para que la tabla pueda hacer referencia a él. Sin embargo, las clases que no implementan una clase virtual parecen estar en línea, como en el caso de std :: string. No todo se alinea, pero las cosas que no parecen estar colocadas mínimamente en un bloque de código "anónimo" en algún lugar de la memoria. Además, ¿cómo corrompe el código la pila? Claro que perdimos la cadena, pero de lo contrario todo lo que hicimos fue reinterpretar.
Dmitry
La corrupción de la memoria ocurre en un programa de computadora cuando el contenido de una ubicación de memoria se modifica involuntariamente. Este programa lo hace intencionalmente y ya no intenta usar esa cadena, por lo que no hay corrupción, solo desperdicia espacio en la pila. Pero sí, la cadena invariante ya no se mantiene, sí abarrota el alcance (al final del cual, la pila se recupera).
Dmitry
dependiendo de la implementación de la cadena, puede escribir sobre bytes que no desea. Si la cadena es algo así struct { int size; const char * data; };(como parece suponer), escribe 4 * 4 bytes = 16 bytes en una dirección de memoria donde solo reservó 8 bytes en una máquina x86, por lo que 8 bytes se escriben sobre otros datos (que pueden dañar su pila ) Afortunadamente, std::stringnormalmente tiene algo de optimización en el lugar para cadenas cortas, por lo que debe ser lo suficientemente grande para su ejemplo cuando se usa alguna implementación estándar.
hoffmale
@hoffmale tienes toda la razón, podría ser de 4 bytes, podría ser de 8, o incluso 1 byte. Sin embargo, una vez que conoce el tamaño de la cadena, también sabe que esa memoria está en la pila en el ámbito actual, y puede usarla como desee. Mi punto era que si conoces la estructura, está empaquetada de forma independiente de cualquier información sobre la clase, a diferencia de los objetos COM que tienen un uuid que identifica su clase como parte de la vtable de IUnknown. A su vez, el compilador accede a estos datos directamente a través de funciones inlíneas o estáticas destrozadas.
Dmitry