Click here to Skip to main content
15,867,330 members
Articles / Programming Languages / C++14

Modules for Modern C++

Rate me:
Please Sign up or sign in to vote.
5.00/5 (5 votes)
18 Nov 2017CPOL8 min read 43.9K   483   8   5
The article explains the recent Modules TS Draft for a future C++ Standard and provides examples based on the modules implementation in Visual C++ 2017

Introduction

Modules TS is on the way to being approved. A proposal on Modules is available in [1]. We hope it will be in the C++20 Standard.

Issues that modules solve:

(1) Reduction of compile time. Insure that the code it compiled once. At present in C++, header files (especially those containing templates) take a lot of time to compile, and they may be compiled several times if called in various source files.

(2) Prevention of interdependency between various items of code. Usually #include directives often have macros that may influence each other. The order of import declarations for modules is irrelevant as long as there is no name clash. Modules do not export macros, which ensures that there is no collision between macro names.

(3) Avoidance of confusion between header files that exists when using libraries. In C, libraries and header files are stored separately, which makes it easy to confuse which header file belongs to a given library. The import declaration for module will automatically access the right interface.

Name clashes in issue (2) can be prevented by using different namespaces for the exported objects: for example, it is possible for a namespace to have the same name as the name of the module it belongs to.

Module Declarations

The general structure of a program with modules is shown in Figure 1.

Image 1

Figure 1. General Program Structure with Modules

Let's consider all the details. Each rectangle represents a translation unit, which is stored in a file and is compiled separately. Each module (I mean "named module", which has a name) can be composed of one interface unit and zero or more implementation units. An interface unit contains the following declaration:

export module module_name;

The item module_name, which consists of one or more identifiers separated by dots, is the name of the module to be exported. Each implementation unit (if available) should contain the following declaration:

module module_name;

Each translation unit should have no more than one module declaration ("export module module_name;" or "module module_name;"). A module declaration does not have to be the first declaration in a module. The declarations that occur before a module declaration belong to the global module that does not have any name. This allows to use various includes that can be shared between the modules, but will not be exported by the named module.

Each module usually has to export some entities: classes, variables, constants, functions, templates, etc. Those entities should be exported, which means that their declarations should be preceded by an "export" keyword.

If it is necessary to import a module, in order to use the entities exported by it, the following construct is used:

import module_name;

It is possible to import a module and export all of its contents into another module. In this case the following declatation can be used:

export import module_name;

In terms of compilation: each module is compiled once. Modules may be interdependent. But there cannot be any circular dependency between their interfaces. That means that the interfaces should be ordered: an interface that imports a module should be compiled after the interface that exports this module. The implementations should be compiled after the interfaces.

A Simple Example

Let's look and a simple example with one module S1, which is defined using only an interface unit:

// interface unit S1. File S1.ixx <span style="display: none;"> </span>import std.core;
export module S1;
export int n = 25; // exporting a variable
export constexpr double p = 3.2; // exporting a constant expression
export struct A // exportings a structure
{
    void print() const;
};

void A::print() const
{
    std::cout << "A. n:" << n << " p:" << p << std::endl;
    n = 22;
}<span style="display: none;"> </span>

The program file Test_S1.cpp contains the following code:

// File Test_S1.cpp <span style="display: none;"> </span>import std.core;
import S1;
int main()
{
     A x; 
     std::cout << "n:" << n << " p:" << p << std::endl;
     n= 27;
     x.print();
     std::cout << "n:" << n << " p:" << p << std::endl;
}

The program will print:

n:25 p:3.2
A. n:27 p:3.2
n:22 p:3.2

The standard library imports haven't been fully fixed yet. I am giving an example based on the Visual C++ 2017 implementation.

This example shows that a module can be easily defined using only an interface unit. As you see, the export declarations should follow the module declaration. If an entity is not exported it won't be imported.

It is possible to split the module definition between two units -- an interface and an implementation -- as follows:

// interface unit S1. File S1.ixx
export module S1; // export declaration
export int n = 25; // exporting a variable
export constexpr double p = 3.2; // exporting a constant expression

export struct A // exporting a structure
{
   void print();
};

// implementation unit S1. File S1.cxx
import std.core;
module S1;
void A::print()
{    
     std::cout << "A. n:" << n << " p:" << p << std::endl;
     n = 22;
}

But this does not make much sense here, although it will reduce the size of the interface file. The Test_S1.cpp will stay the same.

Mutual Dependency Between Modules

It is necessary to split modules between an interface and implementation only if there is a mutual dependency between modules. The typical example is mutually recursive functions. Consider the following two mutually-recursive integer functions.

Function f:

f(0) = 1

f(n) = 1+q(n-1), if n > 0

Function q:

q(0) = 0

q(n) = q(n-1)+n*f(n-1), if n > 0

In this case the interface units can be defined as follows:

// interface F1. File: F1.ixx
export module F1;
export int f(int n);

// interface Q1. File: Q1.ixx
export module Q1;
export int q(int n);

The interfaces are present. In this case, in the implementation units we can import these modules:

// implementation F1. File F1.cxx
import Q1;
module F1;

int f(int n)
{
    if (n == 0)
        return 1;

    return 1+q(n-1);
}                                        

// implementation Q1. File Q1.cxx
import F1;
module Q1;

int q(int n)
{
    if (n == 0)
        return 1;

    return q(n-1)+n*f(n-1);  
}

The program file F1Q1_Test.cpp can be as follows:

import std.core;
import Q1;
import F1;                                        

int main()
{
    for (int n = 0; n <= 10; n++)
    {
       std::cout << "n= " << n << " f(n)= " << f(n) << " q(n)= " << q(n) << std::endl;
    }
}

The output of this program will be:

n= 0 f(n)= 1 q(n)= 1
n= 1 f(n)= 2 q(n)= 2
n= 2 f(n)= 3 q(n)= 6
n= 3 f(n)= 7 q(n)= 15
n= 4 f(n)= 16 q(n)= 43
n= 5 f(n)= 44 q(n)= 123
n= 6 f(n)= 124 q(n)= 387
n= 7 f(n)= 388 q(n)= 1255
n= 8 f(n)= 1256 q(n)= 4359
n= 9 f(n)= 4360 q(n)= 15663
n= 10 f(n)= 15664 q(n)= 59263

A Bit More Complicated Example: Importing and Exporting

Here is an example, which I found on the internet [2]. I have slightly modified it to use the right syntax of Modules TS, and added the pets.cat module.

We start with the base class:

// Interface pets.pet. File pets.pet.ixx

import std.core;
export module pets.pet;
export class Pet
{
public:
 virtual std::string says() = 0;
};

Two derived classes Cat and Dog are defined in separate modules:

// Interface pets.cat. File pets.cat.ixx

import std.core;
export module pets.cat;
import pets.pet;

export class Cat : public Pet
{
public:
 std::string says() override;
};

std::string Cat::says()
{
   return "Miaow";
}


// Interface pets.dog. File pets.dog.ixx
import std.core;
export module pets.dog;
import pets.pet;

export class Dog : public Pet
{
public:
  std::string says() override;
};

std::string Dog::says()
{
  return "Woof!";
}

Now we can combine all this definitions in one module, so that only one module can be imported instead of three:

// Interface pets. File pets.ixx

export module pets;

export import pets.pet;
export import pets.dog;
export import pets.cat;

Now the main module can import the module pets and use all the three modules:

//File Pets_Test.cpp
import pets;
import std.core;
import std.memory;

int main()
{
  std::unique_ptr<Pet> pet1 = std::make_unique<Dog>();
  std::cout << "Pet1 says: "
   << pet1->says() << std::endl;

  std::unique_ptr<Pet> pet2 = std::make_unique<Cat>();
  std::cout << "Pet2 says: "
   << pet2->says() << std::endl;
}

The output of the program will be:

Pet1 says: Woof!
Pet2 says: Miaow

Avoiding Name Clashes: Using Namespaces

In general, modules will not eliminate problems with name classes. There will be a problem, when two modules are imported that have objects with the same names. This can be avoided if the exported objects are included into a namespace that has the same name as the module. Here is an example, based on the module S1 that I showed before:

// Interface S2. File S2.ixx

import std.core;
export module S2;
export namespace S2
{
    int n = 25;
    constexpr double p = 3.2;
    struct A
    {
        void print() const;
    }; 

    void A::print() const
    {
        std::cout << "A. n:" << n << " p:" << p << std::endl;
        n = 22;
    } 
}

The main module can look as follows:

// File Test_S2.cpp

import std.core;
import S2;
int main()
{
     S2::A x; 
     std::cout << "n:" << S2::n << " p:" << S2::p << std::endl;
     S2::n= 27;
     x.print();
     std::cout << "n:" << S2::n << " p:" << S2::p << std::endl;
}

If output of this program will be the same as before with module S1:

n:25 p:3.2
A. n:27 p:3.2
n:22 p:3.2

Running Examples in CppModuleBuilder

I have created a simple IDE environment, that compiles and builds the modules using Visual C++ 2017 command line. It is easier to try modules in an IDE, which builds the whole program automatically out of the available modules, than to execute several command lines, take care of their order. This tool is designed for demonstartion only and is not intended for serious development.

The IDE is written in C# and looks like this:

Image 2

The icons on the toolbar are self-explanatory: New File, Load File, Save File, Find, Replace, Remove File, Load Project, Save Project, Close Project, Build, Run. When a new file is created the user must give it a name immediately. The name of the files should correspond the name of the modules used. The interface modules should have the extension .ixx; whereas the implementation modules, the extension .cxx. The main module should have extension .cpp. There can be no more than one implementation for each module. The whole structure of the program is shown in Figure 2.

Image 3

Figure 2. The structure of the program in CppModuleBuilder

A project can be loaded by clicking on the Load Project button on the Toolbar and selecting a file with the extension .proj. There are soveral sample projects that you may use. The first time you use the build a dialog box will appear, as shown in Figure 3.

Image 4

Figure 3. Select Developer Command Prompt Dialog Box

In this case, you must select the VsDevCmd.bat path, which corresponds to the latest Visual Studio that you would like to use. In this example, it will be the first line.

When starting a new project, you may close the previous one, create new files and save them as a project. You must always give extension .proj for project files.

Any extra information about C++ Modules in Visual Studio in available here [3,4].

CppModuleBuilder processes the files and copies the processed files into the cpp_module_builder directory in the user's "My Directory" folder. There they are compiled.

I have also introduced some "tweaks" -- extra preprocessing of the source code:

(1) The "export module" is actually is not available yet. VC++ uses "export". CppModuleBuilder remove "export".

(2) If both the interface and the implementation of a module are present and a variable is exported, the compilation failes if the variable is not defined in the implementation file as well. I had to put it into an .hxx file, which is imported at the beginning of the implementation file.

All these tweaks will not be necessary in the future, when the modules are properly implemented.

Other Issues: Mutual Dependency of Classes

Consider the following example:

// Interface P1. File P1.ixx

export module P1;
struct SP2;
export struct  SP1
{
   SP1(int m):v1(m) {};
   SP2* p2;
   void print();
   int v1;
}; 

export struct  SP2
{
   SP2(int m):v2(m) {}
   SP1* p1;
   void print();
   int v2;
};                                    


//Implementation P1. File P1.cxx

import std.core;
module P1;                                      
void SP1::print()
{
   std::cout << "SP1 self:" << v1<< " the other:" << p2->v2 << std::endl;
}

void SP2::print()
{
 std::cout << "SP2 self:" << v2<< " the other:" << p1->v1 << std::endl;
}

The main module may look like this:

// File P1_Test.cpp

import P1;

int main()
{
   SP1* s1 = new SP1(10);
   SP2* s2 = new SP2(5);

   s1->p2 = s2;
   s2->p1 = s1;

   s1->print();
   s2->print();

   delete s1;
   delete s2;
};

This program can be compiled and run in the VC++2017. You may use CppModuleBuilder. But what if we want to split the module P1 into two modules, so that each class will be in a separate module. The problem here is that we have to reference the other module in the interface. We cannot import them: circular references are not allowed. Modules can be compiler in a sequence, but each module can be compiled only once.

How can we possibly reference an object in another module without importing the module? The so-called proclaimed ownership declaration helps. It reminds me function forwarding. We don't fully declare an object: just reference it, saying that it will be declared later. It's good enough to define a pointer to it. The proclaimed ownership declaration has the following syntax:

extern module module_name: declaration;

Here is how we can split the interface between the modules.

// Interface SP1M.
export module SP1M;
extern module SP2M: struct SP2; // proclaimed ownership declaration
export struct  SP1
{
   SP1(int m):v1(m) {};
   SP2* p2;
   void print();
   int v1;
};

// Interface SP2M.
export module SP2M;
import SP1M;
export struct  SP2
{
   SP2(int m):v2(m) {}
   SP1* p1;
   void print();
   int v2;
};

I deliberately did not give the names of the files because the implementation is not available yet. You cannot try this example.

Further Information

You may wish to look at Clang [5] and download Clang 5.0.0 [6]. I have tried it in Windows.

References

  1. www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/n4689.pdf
  2. https://schneide.wordpress.com/2017/07/09/c-modules-example/
  3. https://blogs.msdn.microsoft.com/vcblog/2017/05/05/cpp-modules-in-visual-studio-2017/
  4. https://blogs.msdn.microsoft.com/vcblog/2015/12/03/c-modules-in-vs-2015-update-1/
  5. https://clang.llvm.org/docs/ReleaseNotes.html
  6. http://releases.llvm.org/download.html

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
United Kingdom United Kingdom
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
Questioncan you please publish the source for CppModuleBuilder Pin
Member 318791330-Dec-17 7:08
Member 318791330-Dec-17 7:08 
QuestionNicely Done Pin
Rick York1-Dec-17 15:42
mveRick York1-Dec-17 15:42 
This article is very good in its coverage of the topic.

I mean no offense to you by writing this but I can see myself never using this particular language feature.
QuestionI think c + + can develop, should absorb good design concepts, Pin
Tao craig1-Dec-17 13:12
Tao craig1-Dec-17 13:12 
Question- Pin
CTAPOBEP21-Nov-17 11:15
CTAPOBEP21-Nov-17 11:15 
AnswerRe: - Pin
Mikhail Semenov21-Nov-17 11:48
Mikhail Semenov21-Nov-17 11:48 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.