
In the
previous section , I presented a way to reduce the amount of code when using helper classes and classes from other namespaces.
This article will discuss how you can implement the placement of library items on files. There will also be questions about the connection of library elements in user code, and, of course, how “working” namespaces can help with library implementation.
Approaches used in organizing library files
To begin with, we’ll decide that we’ll talk about libraries, all of whose code comes in the form of header files. When creating a file structure in such libraries follow a number of rules. Not all of them can be called “standard”, but the application of the presented rules in existing libraries is not such a rare phenomenon.
1)
Library header files are usually placed in a separate folder .
')
The folder name contains the name of the library or namespace used in the library. This allows user documentation to “document” the use of the library:
#include <boost/array.hpp> #include <boost/scoped_ptr.hpp>
2)
Files containing “user-defined” types and files that provide implementation details should preferably be placed in different folders .
By “user-defined” types are meant the types defined by the library and provided to the user for use in their code.
The application of this rule by the library developer allows the library user to easily determine the files he needs to include in his project without reading the accompanying documentation.
For example, some
boost libraries place implementation files in the
detail subfolder.
3)
For each class library often creates a separate file with the same name .
This approach allows the library user to easily understand its structure, and the developer provides ease of navigating the classes in his library.
4)
Library files must be self-contained .
Basically, this applies to those files in which the “user-defined” types are defined and which the user of the library connects to his program or another library using
#include .
More details about this rule can be found in the book “C ++ Programming Standards” by G.Sattera and A.Aleksandrescu (rule 23).
Test Library Description
Further assume that we need to implement some library
SomeLib . This library should provide the user with the
A_1 ,
A_2 and
A_3 classes that implement some functionality. The green area in the figure is the library itself, the red is the namespace, and the blue are the classes provided to the user.
Let the
SomeLib library have a dependency on the
STL library and contain auxiliary classes
I_1 and
I_2 “invisible” to the user, which are shown in orange in the figure. The arrows indicate the dependence of classes on others. For example, the class
A_1 depends on the classes
list ,
vector and
I_1 . Under class dependencies in this case means the use of other classes when describing their data, member functions, or the implementation of these functions.
Suppose the library comes in the form of header files and as one of the files contains
config.hpp , which describes some of the “control” structures.
So, let's begin…
Implementation of the test library using the presented rules
When implementing the library, we will use the standard approach described in the previous section. Custom classes will be placed in the library namespace
some_lib , and utility classes in the nested namespace
impl .
The library will be located in the
some_lib folder. In this folder there will be files
A _ *. Hpp , describing the “user-defined” types.
I _ *. Hpp files containing utility classes will be placed in the
impl subfolder .
Now you can begin to implement. Let's skip the description of the coding process and go straight to the results.
File
some_lib / impl / config.hpp #ifndef SOME_LIB__IMPL__CONFIG_HPP #define SOME_LIB__IMPL__CONFIG_HPP #if defined(_MSC_VER)
File
some_lib / impl / i_1.hpp #ifndef SOME_LIB__IMPL__I_1_HPP #define SOME_LIB__IMPL__I_1_HPP #include <vector> #include <some_lib/impl/config.hpp> namespace some_lib { namespace impl { class I_1 { public: void func( std::vector<int> const& ); private: // - }; }} #endif
File
some_lib / impl / i_2.hpp #ifndef SOME_LIB__IMPL__I_2_HPP #define SOME_LIB__IMPL__I_2_HPP #include <vector> #include <some_lib/impl/config.hpp> namespace some_lib { namespace impl { class I_2 { public: // private: std::vector<int> data_; }; }} #endif
File
some_lib / a_1.hpp #ifndef SOME_LIB__A_1_HPP #define SOME_LIB__A_1_HPP #include <list> #include <vector> #include <some_lib/impl/config.hpp> #include <some_lib/impl/I_1.hpp> namespace some_lib { class A_1 { public: // private: impl::I_1 a_; std::list<int> data_; std::vector<int> another_data_; }; } #endif
File
some_lib / A_2.hpp #ifndef SOME_LIB__A_2_HPP #define SOME_LIB__A_2_HPP #include <string> #include <some_lib/impl/config.hpp> #include <some_lib/impl/I_2.hpp> namespace some_lib { class A_2 { public: A_2( std::string const& ); private: impl::I_2 a_; }; } #endif
File
some_lib / A_3.hpp #ifndef SOME_LIB__A_3_HPP #define SOME_LIB__A_3_HPP #include <string> #include <some_lib/impl/config.hpp> #include <some_lib/impl/I_2.hpp> #include <some_lib/A_2.hpp> namespace some_lib { class A_3 { public: A_3( std::string const& ); void func( A_2& ); private: impl::I_2 a_; std::string name_; }; } #endif
The user can now use our library by connecting one or more header files.
#include <some_lib/A_1.hpp> #include <some_lib/A_2.hpp> #include <some_lib/A_3.hpp>
Notes on the implementation of the test library
For a slight reduction in the code, instead of the standard “guarding” of inclusion and implemented through
#ifndef ,
#define ,
#endif ,
#pragma once can be used in header files. But this method does not work on all compilers and, therefore, is not always applicable.
Our library contains a relatively simple pattern of relationships between elements. It is not difficult to imagine the implementation of more complex dependencies for a library developer.
It is worth noting another interesting point. When you include only one header file
some_lib / A_3.hpp, the user actually connects more than half of the library (to be more precise, 4/6 of the source files).
And if you now ask yourself: is it really necessary to implement the ability for a library user to connect its individual elements?
The main argument in favor of the answer “Yes” will be that this approach will reduce the compilation time when connecting individual elements compared to the compilation time with full inclusion of all library elements. If there are almost no links between the library elements (not our case), then this is true. And if there are a lot of connections, the answer is ambiguous. When thinking about the answer, it is worth remembering that the “guards” of inclusion and
#include directives at the stage of processing the source files by the preprocessor during compilation have non-zero time costs.
Suppose the answer to this question is “No.” This is where the fun begins ...
Implement a test library using a single connection point
The user now only needs one line of code to connect the library:
#include <some_lib/include.hpp>
Now we’ll focus on the implementation points that the library developer can apply:
1) Since there is only one library connection point (file
some_lib / include.hpp ), the library developer can get rid of all the inclusion “guards”, except for one - in the connection file of the entire library.
2) Each file of a “custom” class or class-member of an implementation is no longer required to contain the inclusion of files containing dependent elements.
3) The use of "working" namespaces allows you to get rid of the assignment of namespaces in each file.
Since there is only one file for connecting the library by the user, it is possible to review the structure of the library files.
The library implementation can now look like this:
File
some_lib / include.hpp #ifndef SOME_LIB__INCLUDE_HPP #define SOME_LIB__INCLUDE_HPP #include <list> #include <vector> #include <string> #include <some_lib/private/config.hpp> namespace z_some_lib { using namespace std; // // using std::list; // using std::vector; // using std::string; #include <some_lib/private/I_1.hpp> #include <some_lib/private/I_2.hpp> #include <some_lib/public/A_1.hpp> #include <some_lib/public/A_2.hpp> #include <some_lib/public/A_3.hpp> } namespace some_lib { using z_some_lib::A_1; using z_some_lib::A_2; using z_some_lib::A_3; } #endif
File
some_lib / private / config.hpp #if defined(_MSC_VER)
File
some_lib / private / i_1.hpp class I_1 { public: void func( vector<int> const& ); private:
File
some_lib / private / i_2.hpp class I_2 { public:
File
some_lib / public / A_1.hpp class A_1 { public:
File
some_lib / public / A_2.hpp class A_2 { public: A_2( string const& ); private: I_2 a_; };
File
some_lib / public / A_3.hpp class A_3 { public: A_3( string const& ); void func( A_2& ); private: I_2 a_; string name_; };
Conclusion
I think it makes no sense to describe the advantages and disadvantages of the presented approach to implementation - the code itself speaks for itself. Each developer will independently decide which scheme he chooses for himself when implementing the library code, while weighing all the pros and cons.
And if you look closely at the presented scheme, that is, the feeling of something familiar. But this is not about now ...