Subtleties of implementation of the library code. Part two

    In the previous part , we described a method by which you can reduce the amount of code when using helper classes and classes from other namespaces.

    This article will discuss how to implement the placement of library items in files. The issues of connecting library elements in user code will also be touched upon, and, of course, how “working” namespaces can help in implementing the library.

    Approaches to Organizing Library Files


    To begin with, we’ll decide that we will talk about libraries, all of whose code is delivered in the form of header files. When creating the file structure in such libraries, a number of rules are followed. Not all of them can be called "standard", but the application of the presented rules in existing libraries is not such a rare occurrence.

    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 you to "document" the use of the library in user code:
    #include 
    #include 

    2) Files containing "user" types and files providing implementation details should preferably be placed in different folders .

    "User" types are understood as types defined by the library and provided to the user for use in their code.

    Application of this rule by the library developer allows the library user to easily determine the files that he needs to include in his project without reading the related documentation.
    For example, some boost libraries host implementation files in the detail subfolder .

    3) For each class of the library, a separate file with the same name is often created .

    This approach allows the library user to easily understand its structure, and the developer provides ease of navigation through the classes in his library.

    4) Library files should be self-sufficient .

    Basically, this applies to those files in which "user" types are defined and which the library user connects to in his program or another library using #include .

    You can read more about this rule in the book “C ++ Programming Standards” by G. Satter and A. Alexandrescu (rule 23).

    Description of the test library


    Next, suppose we need to implement some SomeLib library . This library should provide the user with classes A_1 , A_2 and A_3 that implement some functionality. The green area in the figure represents the library itself, red represents the namespace, and blue represents the classes provided to the user.



    Let the SomeLib library be dependent on the STL library and contain auxiliary classes I_1 and I_2 that are “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 . In this case, class dependencies is understood as the use by it of other classes in the description of its data, member functions, or the implementation of these functions.



    Suppose that the library is supplied as header files and contains config.hpp as one of the files , which describes some of the "control" structures.

    So, let's begin…

    Test library implementation using the presented rules


    When implementing the library, we will use the standard approach described in the previous part. We will place user classes in the library namespace some_lib , and utility classes in the nested impl namespace .

    The library will be located in some_lib folder . In this folder there will be A _ *. Hpp files describing the "user" types. Files I _ *. Hpp , containing utility classes, we will place in the subfolder impl .



    Now you can start implementation. We skip the description of the coding process and immediately proceed 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)
      //...
    #elif defined(__GNUC__)
      //...
    #else 
      //...
    #endif
    #endif
    

    File some_lib / impl / I_1.hpp
    #ifndef SOME_LIB__IMPL__I_1_HPP
    #define SOME_LIB__IMPL__I_1_HPP
    #include 
    #include 
    namespace some_lib {
    namespace impl
    {
        class I_1
        {
        public:
            void func( std::vector 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 
    #include 
    namespace some_lib {
    namespace impl
    {
        class I_2
        {
        public:
            // интерфейс
        private:
            std::vector data_;
        };
    }}
    #endif
    

    File some_lib / A_1.hpp
    #ifndef SOME_LIB__A_1_HPP
    #define SOME_LIB__A_1_HPP
    #include 
    #include 
    #include 
    #include 
    namespace some_lib
    {
        class A_1
        {
        public:
            // интерфейс
        private:
            impl::I_1 a_;
            std::list data_;
            std::vector another_data_;
        };
    }
    #endif
    

    File some_lib / A_2.hpp
    #ifndef SOME_LIB__A_2_HPP
    #define SOME_LIB__A_2_HPP
    #include 
    #include 
    #include 
    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 
    #include 
    #include 
    #include 
    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 attaching one or more header files.
    #include 
    #include 
    #include 


    Test Library Implementation Considerations


    To slightly reduce the code, instead of the standard “guardians” of inclusion and implemented through #ifndef , #define , #endif , you can use #pragma once in the header files . But this method does not work on all compilers and, therefore, is not always applicable.

    Our library contains a relatively simple scheme of connections between elements. It is not difficult to imagine what the developer of the library will realize the implementation of more complex dependencies.

    It is worth noting another interesting point. When including 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 the question: is it really so necessary to implement for the library user the ability 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 compilation time with the full inclusion of all library elements. If there are almost no connections between library elements (not our case), then this is indeed so. And if there are many connections, the answer is ambiguous. When considering the answer, it is worth remembering that the “guardians” of the inclusion and the #include directive at the stage of processing the source files by the preprocessor during compilation have nonzero time costs.

    Suppose the answer to this question is "No." This is where the fun begins ...

    Implementing a test library using a single mount point


    The user now needs only one line of code to connect the library:
    #include 


    Now let’s dwell on the implementation points that the library developer can apply:
    1) Since there is only one connection point for the library ( some_lib / include.hpp file ), the library developer can get rid of all the “guards” of inclusion, except for one in the connection file of the entire library.
    2) Each file of a “custom” class or implementation element class is no longer required to contain file inclusions.
    3) The use of "working" namespaces eliminates the need for namespaces in each file.

    Since there is only one file for connecting a library to the user, you can review the structure of the library files.



    The library implementation may now look like this:

    file some_lib / include.hpp
    #ifndef SOME_LIB__INCLUDE_HPP
    #define SOME_LIB__INCLUDE_HPP
    #include 
    #include 
    #include 
    #include 
    namespace z_some_lib
    {
        using namespace std; 
        // или
        // using std::list;
        // using std::vector;
        // using std::string;
        #include 
        #include 
        #include 
        #include 
        #include 
    }
    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)
      //...
    #elif defined(__GNUC__)
      //...
    #else 
      //...
    #endif
    

    File some_lib / private / I_1.hpp
    class I_1
    {
    public:
        void func( vector const& );
    private:
        // члены-данные
    };
    

    File some_lib / private / I_2.hpp
    class I_2
    {
    public:
        // интерфейс
    private:
        vector data_;
    };
    

    File some_lib / public / A_1.hpp
    class A_1
    {
    public:
        // интерфейс
    private:
        I_1 a_;
        list data_;
        vector another_data_;
    };
    

    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 do not think it makes sense to describe the advantages and disadvantages of the presented approach to implementation - the code speaks for itself. Each developer will independently decide which scheme for implementing the library code he will choose for himself, 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 now ...

    Also popular now: