¿La plantilla externa evita la inserción de funciones?

10 minutos de lectura

Avatar de usuario de Dennis Zickefoose
dennis zickefoose

No tengo del todo claro cómo funciona el nuevo extern template La función está diseñada para funcionar en C++ 11. Entiendo que su objetivo es ayudar a acelerar el tiempo de compilación y simplificar los problemas de vinculación con bibliotecas compartidas. ¿Eso significa que el compilador ni siquiera analiza el cuerpo de la función, lo que obliga a realizar una llamada no en línea? ¿O simplemente indica al compilador que no genere un cuerpo de método real cuando se realiza una llamada no en línea? Obviamente, a pesar de la generación de código en tiempo de enlace.

Como ejemplo concreto de dónde podría importar la diferencia, considere una función que opera en un tipo incompleto.

//Common header
template<typename T>
void DeleteMe(T* t) {
    delete t;
}

struct Incomplete;
extern template void DeleteMe(Incomplete*);

//Implementation file 1
#include common_header
struct Incomplete { };
template void DeleteMe(Incomplete*);

//Implementation file 2
#include common_header
int main() {
   Incomplete* p = factory_function_not_shown();
   DeleteMe(p);
}

Dentro del “Archivo de implementación 2”, no es seguro delete un puntero a Incomplete. Así que una versión en línea de DeleteMe fallaría. Pero si se deja como una llamada de función real y la función en sí se generó dentro del “Archivo de implementación 1”, todo funcionará correctamente.

Como corolario, ¿las reglas son las mismas para las funciones miembro de las clases con plantillas con una estructura similar? extern template class ¿declaración?

Para propósitos experimentales, MSVC produce la salida correcta para el código anterior, pero si el extern se elimina la línea genera una advertencia sobre la eliminación de un tipo incompleto. Sin embargo, estos son los restos de una extensión no estándar que introdujeron hace años, por lo que no estoy seguro de cuánto puedo confiar en este comportamiento. No tengo acceso a ningún otro entorno de construcción para experimentar [save ideone et al, but being limited to one translation unit is rather limiting in this case].

  • +1 por una pregunta que no entendí completamente para el Correcto razones. Refrescante.

    – Carreras de ligereza en órbita

    17 de julio de 2011 a las 20:36

  • Me gusta la pregunta porque no encontré casos de uso útiles y elegantes para esta característica de C++ 11 en proyectos del mundo real y espero inspirarme en la discusión.

    – Kan Li

    9 de enero de 2015 a las 18:28

Avatar de usuario de Peter Alexander
Pedro Alejandro

La idea detrás de las plantillas externas es hacer que las instancias de plantillas explícitas sean más útiles.

Como sabe, en C++03, puede crear una instancia explícita de una plantilla usando esta sintaxis:

template class SomeTemplateClass<int>;
template void foo<bool>();

Esto le dice al compilador que cree una instancia de la plantilla en la unidad de traducción actual. Sin embargo, esto no evita que ocurran las instancias implícitas: el compilador todavía tiene que realizar todas las instancias implícitas y luego fusionarlas nuevamente durante la vinculación.

Ejemplo:

// a.h
template <typename> void foo() { /* ... */ }

// a.cpp
#include "a.h"
template void foo<int>();

// b.cpp
#include "a.h"
int main()
{
    foo<int>();
    return 0;
} 

Aquí, a.cpp instancia explícitamente foo<int>()pero una vez que vamos a compilar b.cpplo instanciará de nuevo porque b.cpp no tiene idea de que a.cpp va a instanciarlo de todos modos. Para funciones grandes con muchas unidades de traducción diferentes que realizan instanciaciones implícitas, esto puede aumentar significativamente el tiempo de compilación y enlace. También puede hacer que la función se inserte innecesariamente, lo que puede generar una gran cantidad de código.

Con las plantillas externas, puede informar a otros archivos de origen que planea crear una instancia de la plantilla explícitamente:

// a.h
template <typename> void foo() { /* ... */ }
extern template void foo<int>();

De esta manera, b.cpp no causará una instanciación de foo<int>(). La función será instanciada en a.cpp y se vinculará como cualquier función normal. También es mucho menos probable que esté en línea.

Tenga en cuenta que esto no prevenir en línea: la función aún podría estar en línea en el momento del enlace exactamente de la misma manera que una función normal no en línea todavía puede estar en línea.

EDITAR: Para aquellos que tienen curiosidad, acabo de hacer una prueba rápida para ver cuánto tiempo gasta g ++ en la creación de instancias de plantillas. Traté de instanciar std::sort<int*> en un número variable de unidades de traducción, con y sin la supresión de la instanciación. El resultado fue concluyente: 30ms por instanciación de std::sort. Definitivamente hay tiempo para ahorrar aquí en un gran proyecto.

  • Tenga en cuenta que en C++03, cuando usa la especialización de plantilla explícita, puede declarar solo la interfaz en el encabezado y dejar la implementación en el archivo cpp. El inconveniente de hacer esto es que cualquier intento de usar una versión no especializada de la plantilla resultará en un error de enlace.

    – fbafelipe

    17 de julio de 2011 a las 21:31

  • Gracias por la explicación detallada. Entiendo que no habrá una función real. foo<int>() en cualquier archivo de objeto excepto el que especifique. Pero, ¿tiene el compilador la opción de colocar el código directamente en su lugar sin generar técnicamente ese cuerpo de función? Veo que dices que es mucho menos probable que sea inline, pero es porque no se puede [until link time]o porque esa es la forma en que se escriben actualmente los compiladores?

    –Dennis Zickefoose

    17 de julio de 2011 a las 21:59


  • En términos generales, si coloca el cuerpo de la plantilla en una TU separada, el compilador no puede alinearlo, pero el enlazador sí. La razón de esto es que los compiladores compilan los archivos fuente por separado, de manera completamente independiente de otros archivos fuente, por lo que simplemente no saben cómo es el cuerpo de la función y, por lo tanto, no pueden alinearlo. Creo que los compiladores a veces pueden hacerlo si compila todos los archivos fuente a la vez (en una ejecución del compilador), pero no conozco los detalles al respecto.

    – Pedro Alejandro

    17 de julio de 2011 a las 22:27

  • @ Dennis Zickefoose: la separación del compilador y el enlazador es virtual, no necesariamente física. La generación de código ocurre tanto en tiempo de compilación como de enlace. En Visual Studio, hay una opción para la optimización completa del programa para eso.

    – Gene Bushuyev

    20 de julio de 2011 a las 18:13

  • En mi humilde opinión, la respuesta está totalmente fuera de tema y no es el OP solicitado. El OP preguntó si el compilador puede, en principio, hacer la función en línea si ve el cuerpo de la función en la TU pero se declara como extern template.

    – Kan Li

    9 de enero de 2015 a las 18:22

Usando extern template class no parece prevenir la alineación. Ilustraré esto a través de un ejemplo, es un poco complicado pero es lo más simple que se me ocurre.

En el archivo ah definimos la clase de plantilla CFoo,

#ifndef A_H
#define A_H
#include <iostream>

template <typename T> class CFoo{
  public: CFoo(){
      std::cout << "CFoo Constructor, edit 0" << std::endl;
    }
};

extern template class CFoo<int>;
#endif

Al final de ah usamos extern template class CFoo<int> para indicar a cualquier unidad de traducción con #include a.h que no necesita generar ningún código para CFoo. Es una promesa que hacemos de que todas las cosas de CFoo se vincularán sin problemas.

En el archivo c.cpp tenemos,

#include "a.h"

void run(){
  CFoo<int> cf;
}

Debido a la extern template class promise' at the end of a.h, the translation unit of c.cpp does notnecesita generar cualquier código para la clase CFoo.

Finalmente declaramos una función principal en b.cpp,

void run();
int main(){
  run();
  return 0;
}

No hay nada lujoso en b.cpp, simplemente declaramos void run() que estará vinculado a la implementación de la unidad de traducción b.cpp en tiempo de enlace. Para completar, aquí hay un archivo MAKE

cflags = -std=c++11 -O1

b : b.o a.o c.o
  g++ ${cflags} b.o a.o c.o -o b

b.o : b.cpp 
  g++ ${cflags} -c b.cpp -o b.o

c.o : c.cpp 
  g++ ${cflags} -c c.cpp -o c.o

a.o : a.cpp a.h
  g++ ${cflags} -c a.cpp -o a.o

clean:
  rm -rf a.o b.o c.o b

El uso de este archivo MAKE compila y vincula un ejecutable a que genera “CFoo Constructor, edit 0” cuando se ejecuta. ¡Pero nota! En el ejemplo anterior, no parece que hayamos declarado CFoo<int> en cualquier sitio : CFoo<int> definitivamente no se declara en la unidad de traducción b.cpp ya que el encabezado no aparece en esa unidad de traducción, y se le dijo a la unidad de traducción c.cpp que no necesitaba implementar CFoo. Entonces, ¿qué está pasando?

Realice un cambio en el archivo MAKE: reemplace -O1 con -O0 y haga clean make

Ahora, la llamada de enlace da como resultado un error (usando gcc 4.8.4)

c.o: In function `run()':
c.cpp:(.text+0x10): undefined reference to `CFoo<int>::CFoo()'

Este es el error que esperaríamos si no hubiera ninguna línea en primer lugar. Al menos esta es la conclusión a la que llegué, más ideas son bienvenidas.

Para vincularnos con -O1, debemos cumplir nuestra promesa y proporcionar una implementación de CFoo, esto lo proporcionamos en el archivo a.cpp

#include "a.h"
template void foo<int>();

Ahora podemos estar seguros de que CFoo aparece en la unidad de traducción de a.cpp, y nuestra promesa se cumplirá. Como un aparte, tenga en cuenta que template void foo<int>() en a.cpp en precedido por extern template void foo<int>() a través de la inclusión de ah, lo cual no es problemático.

Finalmente, encuentro molesto este comportamiento impredecible dependiente de la optimización, ya que significa que las modificaciones a ah y la recompilación de a.cpp podrían no reflejarse en run() como se esperaba si no hubiera una línea (intente cambiar la salida estándar del constructor Foo y la nueva versión).

Aquí hay un ejemplo interesante:

#include <algorithm>
#include <string>

extern template class std::basic_string<char>;
int foo(std::string s)
{
    int res = s.length();
    res += s.find("some substring");
    return res;
}

Cuando se compila con g++-7.2 en -O3, esto produce una llamada no en línea a string::find PERO una llamada en línea a string::size.

Mientras que sin la plantilla externa, todo está en línea. Clang tiene el mismo comportamiento y MSVC casi no puede insertar nada en ningún caso.

Entonces la respuesta es: depende, y los compiladores pueden tener heurísticas especiales para esto.

¿Ha sido útil esta solución?