Información general de los módulos en C++

C++20 presenta módulos. Un módulo es un conjunto de archivos de código fuente que se compilan independientemente de los archivos de origen (o más concretamente, las unidades de traducción que las importan.

Los módulos eliminan o reducen muchos de los problemas asociados al uso de archivos de encabezado. A menudo reducen los tiempos de compilación, a veces significativamente. Las macros, las directivas de preprocesador y los nombres no exportados declarados en un módulo no son visibles fuera del módulo. No tienen efecto en la compilación de la unidad de traducción que importa el módulo. Puede importar módulos en cualquier orden sin preocuparse por las redefiniciones de las macros. Las declaraciones de la unidad de traducción de importación no participan en la resolución de sobrecarga ni en la búsqueda de nombres en el módulo importado. Una vez compilado un módulo, los resultados se almacenan en un archivo binario que describe todos los tipos, funciones y plantillas exportados. El compilador puede procesar ese archivo mucho más rápido que un archivo de encabezado. Además, el compilador puede reutilizarlo en cualquier lugar donde se importe el módulo en un proyecto.

Se pueden usar módulos en paralelo con los archivos de encabezado. Un archivo de código fuente de C++ puede importar import módulos y también incluir #include archivos de encabezado. En algunos casos, puede importar un archivo de encabezado como módulo, que es más rápido que usar #include para procesarlo con el preprocesador. Se recomienda que use módulos en proyectos nuevos en lugar de archivos de encabezado tanto como sea posible. En el caso de los proyectos existentes de mayor envergadura y en desarrollo activo, experimente convertir encabezados heredados en módulos. Base su adopción sobre si obtiene una reducción significativa en los tiempos de compilación.

Para comparar módulos con otras formas de importar la biblioteca estándar, consulte Comparación de unidades de encabezado, módulos y encabezados precompilados.

Habilite los módulos en el compilador de Microsoft C++

A partir de la versión 17.1 de Visual Studio 2022, los módulos estándar de C++20 se implementan completamente en el compilador de Microsoft C++.

Antes de que el estándar C++20 lo especificase, Microsoft tenía compatibilidad experimental con módulos. El compilador también admite la importación de módulos precompilados de la biblioteca estándar, que se describen a continuación.

A partir de la versión 17.5 de Visual Studio 2022, la importación de la biblioteca estándar como módulo está estandarizada e implementada completamente en el compilador de Microsoft C++. En esta sección se describe el método experimental anterior, que todavía se admite. Para obtener información sobre la nueva forma estandarizada de importar la biblioteca estándar mediante módulos, consulte Importación de la biblioteca estándar de C++ mediante módulos.

Se puede usar la característica de módulos para crear módulos de partición única e importar los módulos de la Biblioteca Estándar proporcionados por Microsoft. Para habilitar la compatibilidad con los módulos de la Biblioteca Estándar, compile con /experimental:module y /std:c++latest. En un proyecto de Visual Studio, haga clic con el botón derecho en el nodo del proyecto en el Explorador de soluciones y elija Propiedades. Establezca la lista desplegable Configuración en Todas las configuraciones y después elija Propiedades de configuración C>/C++>Lenguaje>Habilitar módulos C++ (experimental).

Un módulo y el código que lo consume deben compilarse con las mismas opciones del compilador.

Consumo de la biblioteca estándar de C++ como módulos (experimental)

En esta sección se describe la implementación experimental, que todavía se admite. La nueva forma estandarizada de consumir la biblioteca estándar de C++ como módulos se describe en Importación de la biblioteca estándar de C++ mediante módulos.

Al importar la Biblioteca Estándar de C++ como módulos en lugar de incluirla a través de archivos de encabezado, se pueden acelerar los tiempos de compilación en función del tamaño del proyecto. La biblioteca experimental se divide en los siguientes módulos con nombre:

  • std.regex proporciona el contenido del encabezado <regex>
  • std.filesystem proporciona el contenido del encabezado <filesystem>
  • std.memory proporciona el contenido del encabezado <memory>
  • std.threadingproporciona el contenido de los encabezados <atomic>, <condition_variable>, <future>, <mutex>, <shared_mutex> y <thread>
  • std.core proporciona todo lo demás en la Biblioteca Estándar de C++

Para consumir estos módulos, agregue una declaración de importación a la parte superior del archivo de código fuente. Por ejemplo:

import std.core;
import std.regex;

Para consumir los módulos de la Biblioteca Estándar de Microsoft, compile el programa con las opciones /EHsc y /MD.

Ejemplo

En el ejemplo siguiente se muestra una definición de módulo simple en un archivo de código fuente denominado Example.ixx. La extensión .ixx es necesaria para los archivos de interfaz de módulo en Visual Studio. En este ejemplo, el archivo de interfaz contiene tanto la definición de función y como la declaración. Sin embargo, también se pueden colocar las definiciones en uno o varios archivos de implementación de módulo independientes, como se muestra en un ejemplo posterior.

La instrucción export module Example; indica que este archivo es la interfaz principal de un módulo denominado Example. El modificador export en f() indica que esta función está visible cuando otro programa o módulo importa Example.

// Example.ixx
export module Example;

#define ANSWER 42

namespace Example_NS
{
   int f_internal() {
        return ANSWER;
      }

   export int f() {
      return f_internal();
   }
}

El archivo MyProgram.cpp usa import para tener acceso al nombre exportado por Example. El nombre Example_NS del espacio de nombres está visible aquí, pero no todos sus miembros porque no se exportan. Además, la macro ANSWER no está visible porque no se exportan las macros.

// MyProgram.cpp
import Example;
import std.core;

using namespace std;

int main()
{
   cout << "The result of f() is " << Example_NS::f() << endl; // 42
   // int i = Example_NS::f_internal(); // C2039
   // int j = ANSWER; //C2065
}

La declaración import solo puede aparecer en el ámbito global.

Gramática del módulo

module-name:
module-name-qualifier-seqoptidentifier

module-name-qualifier-seq:
identifier .
module-name-qualifier-seq identifier .

module-partition:
: module-name

module-declaration:
exportoptmodulemodule-namemodule-partitionoptattribute-specifier-seqopt;

module-import-declaration:
exportoptimportmodule-nameattribute-specifier-seqopt;
exportoptimportmodule-partitionattribute-specifier-seqopt;
exportoptimportheader-nameattribute-specifier-seqopt;

Implementación de módulos

Una interfaz de módulo exporta el nombre del módulo y todos los espacios de nombres, tipos, funciones, etc. que componen la interfaz pública del módulo.
Una implementación de módulo define las cosas exportadas por el módulo.
En su forma más sencilla, un módulo puede ser un único archivo que combina la interfaz del módulo y la implementación. También se puede colocar la implementación en uno o varios archivos de implementación de módulo independientes, de forma similar a cómo los archivos .h y .cpp lo hacen.

En el caso de los módulos más grandes, se pueden dividir partes del módulo en submódulos denominados particiones. Cada partición consta de un archivo de interfaz de módulo que exporta el nombre de partición de módulo. Una partición también puede tener uno o varios archivos de implementación de partición. El módulo en su conjunto tiene una interfaz de módulo principal, que es la interfaz pública del módulo. Puede exportar las interfaces de partición, si lo desea.

Un módulo consta de una o más unidades de módulo. Una unidad de módulo es una unidad de traducción (un archivo de origen) que contiene una declaración de módulo. Hay varios tipos de unidades de módulo:

  • Una unidad de interfaz de módulo exporta un nombre de módulo o un nombre de partición de módulo. Una unidad de interfaz de módulo tiene export module en su declaración de módulo.
  • Una unidad de implementación de módulo no exporta un nombre de módulo o un nombre de partición de módulo. Como su nombre indica, implementa un módulo.
  • Una unidad de interfaz de módulo principal exporta el nombre del módulo. Debe haber una sola unidad de interfaz de módulo principal en un módulo.
  • Una unidad de interfaz de partición de módulo exporta un nombre de partición de módulo.
  • Una unidad de implementación de particiones de módulo tiene un nombre de partición de módulo en su declaración de módulo, pero no hay ninguna palabra clave export.

La palabra clave export solo se usa en archivos de interfaz. Un archivo de implementación puede importar import otro módulo, pero no puede exportar export ningún nombre. Los archivos de implementación pueden tener cualquier extensión.

Módulos, espacios de nombres y búsqueda dependiente de argumentos

Las reglas para los espacios de nombres en los módulos son las mismas que cualquier otro código. Si se exporta una declaración dentro de un espacio de nombres, el espacio de nombres envolvente (excepto los miembros que no se exportan explícitamente en ese espacio de nombres) también se exporta implícitamente. Si se exporta explícitamente un espacio de nombres, se exportan todas las declaraciones dentro de esa definición de espacio de nombres.

Cuando el compilador realiza una búsqueda dependiente del argumento para las resoluciones de sobrecarga en la unidad de traducción de importación, considera las funciones declaradas en la misma unidad de traducción (incluidas las interfaces de módulo) en la que se define el tipo de argumentos de la función.

Particiones de módulo

Una partición de módulo es similar a un módulo, con las siguientes excepciones:

  • Comparte la propiedad de todas las declaraciones de todo el módulo.
  • Todos los nombres exportados por los archivos de interfaz de la partición son importados y exportados por el archivo de interfaz principal.
  • El nombre de una partición debe comenzar con el nombre del módulo seguido de dos puntos (:).
  • Las declaraciones de cualquiera de las particiones son visibles en todo el módulo.\
  • No se necesitan precauciones especiales para evitar los errores de la regla de definición única (ODR). Puede declarar un nombre (función, clase, etc.) en una partición y definirlo en otro.

Un archivo de implementación de particiones comienza de esta manera, y es una partición interna desde una perspectiva de estándares de C++:

module Example:part1;

Un archivo de interfaz de partición comienza de la siguiente manera:

export module Example:part1;

Para acceder a las declaraciones de otra partición, una partición debe importarla. Pero solo puede usar el nombre de la partición, no el nombre del módulo:

module Example:part2;
import :part1;

La unidad de interfaz principal debe importar y volver a exportar todos los archivos de partición de la interfaz del módulo de la siguiente manera:

export import :part1;
export import :part2;

La unidad de interfaz principal puede importar archivos de implementación de particiones, pero no puede exportarlos. Esos archivos no pueden exportar ningún nombre. Esta restricción permite a un módulo mantener los detalles de implementación internos en el módulo.

Módulos y archivos de encabezado

Se pueden incluir archivos de encabezado en un archivo de origen del módulo colocando una directiva #include antes de la declaración del módulo. Estos archivos se consideran en el fragmento de módulo global. Un módulo solo puede ver los nombres del fragmento de módulo global que se encuentran en los encabezados que incluye explícitamente. El fragmento de módulo global solo contiene símbolos que se usan.

// MyModuleA.cpp

#include "customlib.h"
#include "anotherlib.h"

import std.core;
import MyModuleB;

//... rest of file

Se puede usar un archivo de encabezado tradicional para controlar qué módulos se importan:

// MyProgram.h
import std.core;
#ifdef DEBUG_LOGGING
import std.filesystem;
#endif

Archivos de encabezado importados

Algunos encabezados son suficientemente independientes que se pueden incorporar mediante la palabra clave import. La principal diferencia entre un encabezado importado y un módulo importado es que las definiciones del preprocesador del encabezado son visibles en el programa de importación inmediatamente después de la instrucción import.

import <vector>;
import "myheader.h";

Consulte también

module, import, export
Tutorial de módulos con nombre
Comparación de unidades de encabezado, módulos y encabezados precompilados