2019-02-10

7: Creating a C++ Standard-Container-Compatible Custom Container

<The previous article in this series | The table of contents of this series | The next article in this series>

The standard 'map', 'list', 'set', 'vector', 'queue', 'stack', and 'priority_queue' are discouraged to be extended. Well . . ., so?

Topics


About: C++

The table of contents of this article


Starting Context


  • The reader has a basic knowledge on C++, even if he or she doesn't accurately understand its some widely-misrepresented elements.

Target Context


  • The reader will understand how to create a standard-container-compatible custom container in C++.

Orientation


Hypothesizer 7
I want a map in which the order of the elements is preserved, which means that any element is inserted into the map at the specified position and is retrieved as such.

The standard 'map' is not such a map.

I do not want my map to be one that is unrelated with the standard 'map', but a kind of the standard 'map' that can be used where the standard 'map' is expected.

So, my initial, natural (I think that this is natural according to the object-oriented programming paradigm) plan was to publicly extend the standard 'map' because that way, the subclass map would be able to be transparently passed into any function that had an argument that was a standard 'map' pointer or reference (usually, such an argument is a pointer or a reference because passing a map by pass-by-value can be costly in general).

However, many people say that it is not recommendable, and some of them even vehemently declare that it is never allowed. Hmm . . .. They say that the standard 'map' or any standard container does not have any virtual destructor. Um, then, certainly, it is problematic.

But, then, what am I supposed to do?

Some people say that I should use 'aggregation' instead of 'inheritance'.

. . . Well, any aggregation does not solve my problem, does it? . . . I have to pass my map into functions that have standard 'map' pointer or reference arguments; I cannot pass such any aggregation class instance into the functions as it is not any descendant of the standard 'map' . . .

Some other people recommend 'private inheritance'.

Any private inheritance does not solve my problem either: I cannot pass any instance of such any class that has privately extended the standard 'map', into the functions that have standard 'map' pointer or reference arguments, as it is not really any descendant of the standard 'map' . . .

After all, publicly extending the standard 'map' seems to be inevitable in order to solve my problem . . .

But many people declare, "You should never publicly extend any standard container!".

. . . Then, how can I solve my problem?


Main Body


1: The Standard Containers Are Not Designed on the Concept of 'Inheritance-based Object-oriented Programming', but of 'Nameless-interfaces-based Object-oriented Programming'


Hypothesizer 7
In order to understand how the standard containers are supposed to be used, I have to first understand the concept on which they are designed.

In fact, the standard containers are not designed on the concept of 'inheritance-based object-oriented programming', but of 'nameless-interfaces-based object-oriented programming', which is a term I have coined right now, because I cannot find any appropriate (no-misnomer) term for the concept in the world.

In order to understand what 'nameless-interfaces-based object-oriented programming' means, let me see an example.

@C++ Source Code
#include <list>
#include <map>
#include <string>

			template <typename T> void namelessInterfaceBasedFunction (T const & a_map) {
				a_map.begin ();
				a_map.end ();
			}
			
			class ABeginEndClass {
				public:
					void begin () const;
					void end () const;
			};
			
			void ABeginEndClass::begin ()  const {
			}
			
			void ABeginEndClass::end () const {
			}
			
			void test () {
				namelessInterfaceBasedFunction (::std::map <string, string> { {string ("a"), string ("aa")}, {string ("b"), string ("bb")}});
				namelessInterfaceBasedFunction (::std::list <string> {string ("a"), string ("b")});
				namelessInterfaceBasedFunction (ABeginEndClass ());
			}

That code compiles and works fine; all what the function, 'namelessInterfaceBasedFunction', requires is that the function can call the two methods, 'begin' and 'end', on the argument object. That qualification required for the argument object is an interface and defined as a named interface in any inheritance-based object-oriented programming paradigm, but in that code, the interface is not defined as any explicit named interface, but defined by the contents of the function. So, I called the interface a nameless interface.

Note that a term like "behavior-based" is a misnomer, for behavior (what the two methods do or not do) does not matter at all.

In fact, although I have meant the argument to be a map (as the name, 'a_map', suggests), the function accepts a list, or even an object that is not any container.

While my original question was 'How am I supposed to create a custom map whose instances can be passed into the functions that have standard 'map' pointer or reference arguments?', the answer is 'I am not supposed to create such arguments unless the arguments are meant to accept only instances of the exact standard 'map' type; I am supposed to make the arguments template types if necessary.'.


2: So, What Should My Standard-Map-Compatible Custom Map Be Like?


Hypothesizer 7
According to that intended paradigm, in order for my custom map to be able to be used in lieu of the standard 'map' in all the possible and allowed cases, my custom map has to implement all the public elements (not only members but also types) each of which has the same signature with the corresponding standard 'map' public element.

I said "allowed" because when an argument explicitly uses the standard 'map' type (whether the argument is a pointer or a reference), not any template type, any custom map is not allowed to be used there.

If it is OK that my custom map is able to be used in lieu of the standard 'map' in only specific cases, my custom map can implement just the public elements that are used in the specific cases, so that those elements fit into the specific cases (each of those elements does not have to necessarily have the same signature with the corresponding standard 'map' public element).

The custom map can have a standard 'map' instance or multiple standard 'map' instances as a member or members, or privately extend the standard 'map' (and also have a standard 'map' instance or multiple standard 'map' instances as a member or members, if necessary), but basically should not publicly extend the standard 'map', as many people rightly say.

Of course, also each of the corresponding iterators has to defined likewise.

Well, as they have many elements, it involves some tiresome work, but what to be done are clear.


3: Some Complaints


Hypothesizer 7
Although I have explained how the standard containers are supposed to be used and what custom containers are supposed to be like, I did not say that I liked them; honestly speaking, I loathe them.

A problem is that in general, the details of the unnamed interface required for the argument of the function cannot be easily known: the possibly long contents of the function has to be examined in how the argument is used. As instances of a class are meant to be passed into multiple functions, the requirements for the class can be known only after such all the unnamed interfaces are merged. And also whether a class implements the unnamed interface cannot be easily known: the class definition has to be examined in whether each required element is implemented in the class.

Another problem is that any modification to the contents of the function is not contained inside the function. I mean, if the function is based on the explicit named interface arguments, however the function uses the arguments inside the function does not break any caller of the function; so I can concentrate on the modification inside the function (I have thought that containing the effects of any modification in the possibly smallest area is a major concern of any decent programming language). On the other hand, if the function is based on an unnamed interface argument, some modifications to the function can break some callers of the function, as such modifications can change the unnamed-interface.

In fact, 'nameless-interfaces-based object-oriented programming' is a kind of so-called "duck typing", and honestly, I loathe "duck typing".

Some "duck typing" enthusiasts claim that "duck typing" reduces modification work, but I doubt that. I mean, it may reduce modification work in certain cases, but modification work involves checking that the modification does not break the program, and "duck typing" that necessitates checking the whole program just in order to modify the contents of a function does not necessarily mean reduced work.

It is very fine that some programming languages are for "duck typing" enthusiasts (honestly, I do not care what the programming languages I do not have any intention to use are like), but I did not want C++, which is an enhancement of C, to go in that direction.

In fact, I understand a legitimate reason why the standard containers shunned 'inheritance-based object-oriented programming': the performance disadvantage. Certainly, polymorphism at run time is costly performance-wise, and performance is crucial for C++.

However, in my opinion, there can and should be a way (as a new language feature) that enforces an explicit named interface to the template type argument and enforces that only the elements of the interface can be used in the function. That way, there will be no performance disadvantage: polymorphism is not used (the members of the template type argument object do not have to be virtual, as the concrete subclass, not the interface, is used as the template type).

Although there are some people who claim that just decent documents or descend names of variables, etc. will solve my insisted problems, let me ask this: who is really writing such decent documents or decent source code? . . .

In fact, I must point out that most existing software documents and source code in the world are indecent: inappropriate terminologies are used, unreasonable explanations are made, and abbreviated names only the writers can understand (and even the writers wouldn't understand after a while) are used. In fact, I have pointed out and will point out many indecent prevalent descriptions (by some authorities or not) (for example, on 'reference', "lvalue", "rvalue", "xvalue", "prvalue", and "glvalue") in this series and some other series (for example, 'Exploiting an Open-Source Office Suite', 'Let Me Understand the Java Programming Language', and 'Let Me Understand Git').

Certainly, my insisted problems would be solved IF decent documents or decent source code were written, but based on the track records, I cannot delude myself into expecting that many decent documents or decent source code will be written by many people, especially by the ones who breezily claim that just decent documents or decent source code will solve the problems.


4: The Conclusion and Beyond


Hypothesizer 7
Now, I seem to understand the basic idea on how to create a standard-container-compatible custom container in C++.

Although the basic idea is simple, as the standard 'map' and its iterators have many public elements, really creating a custom map is not so a piece of cake.

I will create my elements-order-preserved map in the next article.


References


<The previous article in this series | The table of contents of this series | The next article in this series>