Learning notes of c + + Advanced Programming 6

Keywords: C++

Other library tools

ratio Library

Any finite rational number that can be used at compile time can be accurately represented through the ratio library. The ratio object is used in the std::chrono::duration class. Everything related to rational numbers is defined in the header file and in the std namespace. The numerator and denominator of rational numbers are of type std::intmax_t is a signed integer type whose maximum width is specified by the compiler. Due to the compile time characteristics of these rational numbers, they look complex and unusual when used. The definition method of ratio object is different from that of ordinary object, and the method of ratio object cannot be called. A type alias is required. For example, the following line of code defines a rational compile time constant representing 1 / 60:

using r1 = ratio<1,60>;

The numerator and denominator of rl rational numbers are compile time constants and can be accessed in the following ways:

intmax_t num = r1::num;intmax_t den = r1::den;

Remember that ratio is a compile time constant, that is, the numerator and denominator need to be determined at compile time. The following code will generate compilation errors;

intmax_t n = 1;intmax_t d = 60;using r1 = ratio<n,d>; //Error

If n and d are defined as constants, there will be no compilation errors:

const intmax_t n = 1;
const intmax_t d = 60;
using r1 = ratio<n,d>; //Ok

Rational numbers are always simplified. For rational number ratio < n, d >, the maximum common divisor gcd, numerator num and denominator den are defined as follows:

num = sign(n)*sign(d)*abs(n)/gcb;
den = abs(d)/gcb;

The ratio library supports the addition, subtraction, multiplication and division of rational numbers. Since all of these operations are performed at compile time, you cannot use standard arithmetic operators, but use a specific combination of templates and type aliases. The available arithmetic ratio templates include ratio_add,ratio_subtract ratio_multiply and ratio_divide. These templates evaluate the results to the new ratio type. This type can be accessed through an embedded type alias named type. For example, the following code first defines two ratio objects, one representing 1 / 60 and the other representing 1 / 30. ratio_ The add template adds two rational numbers, and the result rational number should be 1 / 20 after simplification.

using r1 = ratio<1,60>;
using r2 = ratio<1,30>;
using result = ratio_add<r1,r2>::type;

The C + + standard also defines some ratio comparison templates: ratio_equal,ratio_not_ equal,ratio_less,ratio_less_equal,ratio_greater and ratio_greater_equal. Like the arithmetic ratio template, the ratio comparison template is evaluated at compile time. These comparison templates create a new type of std::bool_constant to represent the result. bool_constant is also std::integral_constant, the struct template, holds a type and a compile time constant value. For example, integral_constant < int 15 > holds an integer value of 15. bool_constant is also an integral of boolean type_ constant. For example, bool_constant is integral_constant < bool, true >, stores Boolean values with a value of true. The result of the ratio comparison template is either bool constant < bool, true >, or bool_constant<bool,false> . With bool constant or integral_ The value of the constant association is accessible through the value data member. The following code demonstrates ratio_ Use of less. Chapter 13 discusses how to use boolalpha to output Boolean values in the form of true or false:

using r1 = ratio<1,60>;
using r2 = ratio<1,30>;
using res = ratio_less<r2,r1>;
cout<<boolalpha<<res::value<< endl;

The following example integrates all the content. Note that since ratio is a compile time constant, you cannot write code such as cout < < RL. Instead, you need to obtain the numerator and denominator and print them separately.

//Defaine a compile-time rational number
using r1 = ratio<1,60>;
cout<<"1) "<<r1::num<<"/"<<r1::den<<endl;

//get numerator end denominator
intmax_t num = r1::num;
intmax_t den = r1::den;
cout<<"2) "<<num<<"/"<<den<<endl;

//Add two rational numbers
using r2 = ratio<1,30>;
cout<<"3) "<<r2::num<<"/"<<r2::den<<endl;

//Compare two rational numbers
using res = ratio_less<r2,r1>;
cout<<"5) "<<boolalpha<<res::value<<endl;

output

xz@xiaqiu:~/study/test/test$ ./test1) 1/602) 1/603) 1/305) false

For convenience, the ratio library also provides some Si (International System of units) type aliases, as follows:

using yocto = ratio<1, 1'000'000'000'000'000'000'000'000>; // *
using zepto = ratio<1, 1'000'000'000'000'000'000'000>; // *
using atto = ratio<1, 1'000'000'000'000'000'000>;
using femto = ratio<1, 1'000'000'000'000'000>;
using pico = ratio<1, 1'000'000'000'000>;
using nano = ratio<1, 1'000'000'000>;
using micro = ratio<1, 1'000'000>;
using milli = ratio<1, 1'000>;
using centi = ratio<1, 100>;
using deci = ratio<1, 10>;
using deca = ratio<10, 1>;
using hecto = ratio<100, 1>;
using kilo = ratio<1'000, 1>;
using mega = ratio<1'000'000, 1>;
using giga = ratio<1'000'000'000, 1>;
using tera = ratio<1'000'000'000'000, 1>;
using peta = ratio<1'000'000'000'000'000, 1>;
using exa = ratio<1'000'000'000'000'000'000, 1>;
using zetta = ratio<1'000'000'000'000'000'000'000, 1>; // *
using yotta = ratio<1'000'000'000'000'000'000'000'000, 1>; // *

SI units marked with an asterisk at the end are defined only if the compiler can represent constant numerator and denominator values with the type alias intmax t. Examples of the use of these predefined SI units are given later in this chapter when discussing duration.

chrono Library

The chrono library is a set of libraries for operating time. This library contains the following components:;

Duration

Clock

point of time

All components are defined in the std::chrono namespace and need to include header files. Each component is explained below.

Duration

Duration represents the interval between two time points, which is represented by the templated duration class. The duration class holds the number of ticks and tick period. Tick cycle refers to the number of seconds between two ticks. It is a compile time ratio constant, that is, it can be a fraction of 1 second. The duration template receives two template parameters, which are defined as follows:

template<class Rep,class Period = ratio<1>> class duration{...}

The first template parameter Rep represents the variable type for saving the number of ticks. It should be an arithmetic type, such as long and double. The second template parameter Period is a rational constant representing the tick Period. If the tick cycle is not specified, the default value ratio < 1 > will be used, that is, the default tick cycle is 1 second.

The duration class provides three constructors: one is the default constructor, and the other constructor receives a value representing the number of ticks as a parameter; The third constructor takes another duration as an argument. The latter can be used to convert one duration to another, such as converting minutes to seconds. An example is given later in this section.

Duration supports arithmetic operations, such as +, -, *, /,%, + +, –, + =, - =, * =, / = and% =, as well as comparison operators. The duration class contains multiple methods, as shown in table 20-1.

methodexplain
Rep count() constReturns the duration value in ticks. The return type is the type parameter specified in the duration template
static duration zero()Returns a duration with a duration value equal to 0
static duration min() static duration max()Returns the duration value of the minimum / maximum duration represented by the type parameter specified in the duration template

C++17 adds foor(), ceil(), round() and abs() operations for duration, which behave similar to those used for numerical data. Let's take a look at how to use duration in actual code. The duration with each tick cycle of 1 second is defined as follows:

duration<long> d1;

Since ratio < 1 > is the default tick cycle, this line of code is equivalent to:

duration<long,ratio<1>> d1;

The following code specifies a duration with a tick cycle of 1 minute (60 seconds):

duration<double,ratio<60>> d2;

The following code defines the duration of each tick cycle of 1 / 60 second;

duration<double,ration<1,60>> d3;

As described earlier in this chapter, the header file defines some SI rational constants. These predefined constants are very convenient when defining the tick cycle. For example, the following line of code defines a duration of 1 millisecond for each tick cycle:

duration<long long,milli> d4;

The following example shows several aspects of duration. It shows how to define a duration, how to perform arithmetic operations on a duration, and how to convert one duration to another with a different tick cycle:

#include <iostream>
#include <string>
#include <regex>
#include <algorithm>
#include <ratio>
#include <chrono>
using namespace std;
using namespace std::chrono;

int main()
{
    //Specify a duration where each tick is 60 seconds
    duration<long,ratio<60>> d1(123);
    cout<<d1.count()<<endl;
    // Specify a duration represented by a double with each tick
    // equal to 1 second and assign the largest possible duration to it.
    duration<double> d2;
    d2 = d2.max();
    cout<<d2.count()<<endl;

    // Define 2 durations:
    // For the first duration, each tick is 1 minute
    // For the second duration, each tick is 1 second
    duration<long,ratio<60>> d3(10);  // = 10 minutes
    duration<long,ratio<1>> d4(14);  // = 14 seconds
    // Compare both durations
    if(d3 > d4)
        cout<<"d3 > d4"<<endl;
    else
        cout<<"d3 <= d4"<<endl;
    // Increment d4 with 1 resulting in 15 seconds
    ++d4;
    // Multiply d4 by 2 resulting in 30 seconds
    d4 *= 2;

    // Add both durations and store as minutes
    duration<double,ratio<60>> d5 = d3 + d4;

    //Add both durations and store as seconds
    duration<long,ratio<1>> d6 = d3 + d4;
    cout << d3.count() << " minutes + " << d4.count() << " seconds = "
        << d5.count() << " minutes or "
        << d6.count() << " seconds" << endl;
    // Create a duration of 30 seconds
    duration<long> d7(30);
    // Convert the seconds of d7 to minutes
    duration<double,ratio<60>> d8(d7);
    cout<<d7.count()<<" seconds = "<<d8.count() <<" minutes"<<endl;
    return 0;
}

output

xz@xiaqiu:~/study/test/test$ ./test1231.79769e+308d3 > d410 minutes + 30 seconds = 10.5 minutes or 630 seconds30 seconds = 0.5 minutesxz@xiaqiu:~/study/test/test$ 

be careful

The second line in the above output represents the maximum duration that the type double can represent. Specific values vary by compiler. Pay special attention to the following two lines;

duration<double,ratio<60>> d5 = d3 + d4;duration<long,ratio<1>> d6 = d3 + d4;

Both lines calculate d3+d4, but the first line saves the result in a floating-point value representing minutes and the second line saves the result in an integer representing secrets. Minute to second conversion (or second to minute conversion) is performed automatically. The following two lines in the above example show how to perform explicit conversion in different time units,

duration<long> d7(30); //secondsduration<double,ratio<60>> d8(d7); //minutes 

The first line defines a duration with a ticking period of 30 seconds. The second line converts this 30 seconds to 0.5 minutes. Conversion in this direction may result in non integer values, so it is required to use the duration represented by floating-point number type, otherwise some strange compilation errors will be obtained. For example, the following code cannot be compiled successfully because d8 uses a long type instead of a floating-point type:

duration<long>d7(30); //secondsduration<long,ratio<60>> d8(d7); //minutes //Error!

But you can use duration_cast() to cast,

duration<long> d7(30); //secondsauto d8 = duration_cast<duration<long,ratio<60>>>(d7); //minutes

Here, the tick period of d8 is 0 minutes because integer division is used to convert 30 seconds to minutes. In the conversion in the other direction, if the source data is of integer type, conversion to floating-point type is not required. Because if you convert from an integer value, you always get an integer value. For example, the following code converts 10 minutes into seconds, both of which are represented by integer type long,

duration<long,ratio<60>> d9(10); //minutesduration<long> d10(d9); //seconds

The chrono library also provides the following standard duration types in the std::chrono namespace:

using nanoseconds = duration<X 64 bits, nano>;using microseconds = duration<X 55 bits, micro>;using milliseconds = duration<X 45 bits, milli>;using seconds = duration<X 35 bits>;using minutes = duration<X 29 bits, ratio<60>>;using hours = duration<X 23 bits, ratio<3600>>;

The specific type of the area depends on the compiler, but the C + + standard requires that the type of the core be an integer of at least the specified size. The type aliases listed above use the predefined SI ratio type aliases described earlier in this chapter. Use these predefined types instead of writing:

duration<long,ratio<60>> d9(10); //minutes

It is written;

minutes d9(10); //minutes

The following is an example that explains how to use these predefined durations. This code first defines a variable t, which stores the result of 1 hour + 23 minutes + 45 seconds. The auto keyword is used here to let the compiler automatically deduce the exact type of T. The second line uses the predefined seconds duration constructor to convert the value of T into seconds and output the result to the console:

auto t = hours(1) + minutes(23)+seconds(45);cout<<seconds(t).count()<<" seconds"<<endl;

Since the C + + standard requires the predefined duration to use the integer type, if a non integer value is obtained after conversion, a compilation error will occur. Although integer division is usually truncated, when using duration implemented through the ratio type, the compiler declares all calculations that may cause non-zero remainder as compile time errors. For example, the following code cannot be compiled because after 90 seconds of conversion, the result is 1.5 minutes:

seconds s(90);minutes m(s);

The following code will not compile successfully, even if 60 seconds is exactly 1 minute. This code will also generate compilation errors, because the conversion from seconds to minutes may produce non integer values:

seconds s(60);minutes m(s);

The conversion in the other direction can be completed normally, because minutes duration is an integer value. Converting it to seconds can always get an integer value;

minutes m(2);seconds s(m);

You can create a duration using standard user-defined literals "h" "min" "s" "ms" "us" and "ns". From a technical point of view, these definitions are in STD:: literals:: Chrono_ In the literals namespace, but it can also be accessed through using namespace std::chrono. Here is an example:

using namespace std::chrono;//..auto myDuration = 43min;//42 minutes

Clock

The clock class consists of time_point and duration. time_ The point class is discussed in detail in section 20.2.3, but these details are not required to understand how clock works. But because of time_point itself depends on clock, so you should first understand how clock works in detail. The C + + standard defines three clocks. The first is called system_clock, which represents the real time from the system real-time clock. The second is called steady_clock is one that can guarantee its time_point never decrements the clock. system_clock cannot guarantee this because the system clock can be adjusted at any time. The third is called high_resolution_clock, the tick cycle of this clock has reached the minimum value. high_resolution_clock could be stead_clock or system_ Alias of clock, depending on the compiler. Each clock has a static now() method that uses the current time as the time_point. system_clock defines two static helper functions for time_point and C style time representation method time_ Mutual conversion between T. The first auxiliary function is to_time_t(), which will give time_ Convert point to time_t. The second auxiliary function is from_time_t(), which returns the time_ Tinitialized time_point. time_ The T type is defined in the < CTime. H > header file.

The following example shows a complete program that obtains the current time from the system and then outputs the time to the console in a format that can be read by the user. The localtime() function returns time_t is converted to the local time expressed in tm, which is defined in the header file. C + + put_ The time() stream operator is defined in the header file. See Chapter 13 for details.

//Get current time as a time_pointsystem_clock::time_point tpoint = system_clock::now();//Convert to a time_ttime_t tt = system_clock::to_time_t(tpoint);//Convert to local timetm* t = localtime(&tt);//Write the time to the consolecout<<put_time(t,"%H:%M:%S")<<endl;

If you want to convert time to a string, you can use std::stringstream or the C-style strfime() function, which are defined in.

//Get current time as a time point
system_clock::time_point tpoint = system_clock::now();
//Convert to a time_t 
time_t tt = system_clock::to_time_t(tpoint);
//Convert to local time
tm* t = localtime(&tt);
//Convert to readable format
char buffer[80] = {0};
strftime(buffer,sizeof(buffer),"%H:%M:%S",t);
//Write the time to the console
cout<<buffer<<endl;

Through the chrono library, you can also calculate the time spent on the execution of a piece of code. The following example shows this process. The actual types of variables start and end are system_ clock::time_ The actual type of point and diff enterprises is duration:

//Get the start time
auto start = high_resolution_clock::now();
//Execute code that you want to time
double d = 0;
for(int i = 0;i < 1000000; ++i)
{
	d+=sqrt(sin(i) * cos(i));
}
//Get the end time and calculate the difference
auto end = high_resolution_clock::now();
auto diff = end - start;
//Convert the difference into milliseconds and output to the console
cout<<duration<double,milli>(diff).count()<<"ms"<<endl;

output

xz@xiaqiu:~/study/test/test$ ./test88.7702msxz@xiaqiu:~/study/test/test$ ./test90.009msxz@xiaqiu:~/study/test/test$ ./test93.3066msxz@xiaqiu:~/study/test/test$ ./test88.8442msxz@xiaqiu:~/study/test/test$ ./test88.889msxz@xiaqiu:~/study/test/test$ 

The loop in this example performs some arithmetic operations, such as sqrt(), sin(), and cos(), to ensure that the loop does not end too soon. If you get a very small millisecond difference on the system, these values will not be very accurate. You should increase the number of friends of the loop to make the loop execution time longer. Small timing values are not very accurate, because although the accuracy of the timer is in the order of milliseconds, on most operating systems, the update frequency of the timer is not high, such as every 10 milliseconds or 15 milliseconds. This will lead to a phenomenon called gating error, that is, any event with a duration less than 1 timer tick only takes 0 time unit, and any event with a duration between 1 and 2 timer ticks takes 1 time unit. For example, on a system with a timer update frequency of 15 milliseconds, a cycle running for 44 milliseconds seems to take only 30 milliseconds. When using this type of timer to determine the calculation time, it is important to ensure that the whole calculation consumes a large number of basic timer tick units, so that the error can be minimized.

point of time

time_ The point class represents a time point in time and is stored as a duration relative to an epoch. time_point is always associated with a specific clock, and the era is the origin of the associated clock. For example, the time era of the classic UNIX/Linux is January 1, 1970, and the duration is measured in seconds. The era of Windows is January 1, 1601, and duration is measured in 100 nanoseconds. Other operating systems have different era, date and duration units. time_ The point class contains time_ since_epoch() function, which returns a duration representing the time between the era of the associated clock and the saved time point. C + + supports reasonable time_point and duration arithmetic operations. These operations are listed below. tp stands for time point and d for duration.

tp + d = tp tp – d = tpd + tp = tp tp – tp = dtp += d tp -= d

Examples of operations not supported by C + + are tp+tp. C + + supports the use of comparison operators to compare two time points and provides two static methods: min() returns the smallest time point and max() returns the largest time point. time_ The point class has three constructors. e time_point(): construct a time_point, which is initialized through duration::zero(). Time obtained_ Point represents the era of the associated clock.

time_ Point (const duration & C): construct a time_ Point, which is initialized through the given duration. Time obtained_ Point indicates era + d. template time_ Point (const time_point < clock, duration2 > & B: construct a time_point and initialize it through time_since_epoch(). Each time_point is associated with a clock. When creating a time_point, specify clock as the template parameter:

time_point<steady_clock> tp1;

Each clock knows its own time_point type, so you can write the following code;

steady_clock::time_point tp1;
//Create a time_point representing the epoch//of the associated steady clocktime_point<steady_clock> tp1;//Add 10 minutes to the time_pointtp1 += minites(10);//Store the duration between epoch and time_pointauto d1 = tp1.time_since_epoch();//Convert the duration to seconds and output to the consoleduration<double> d2(d1);cout<<d2.count()<<" second"<<endl;

output

xz@xiaqiu:~/study/test/test$ ./test600 secondxz@xiaqiu:~/study/test/test$ ./test600 secondxz@xiaqiu:~/study/test/test$ ./test600 secondxz@xiaqiu:~/study/test/test$ 

The time_point can be converted either implicitly or explicitly, which is similar to the duration conversion. The following is an example of an implicit conversion with an output of 42000 ms.

time_point<steady_clock,seconds> tpSeconds(42s);//Convert seconds to milliseconds implicitytime_point<steady_clock,milliseconds> tpMilliseconds(tpSeconds);cout<<ms.count()<<" ms"<<endl;

Generate random number

Generating random numbers that meet the requirements in software is a complex topic. Before C++11, the only way to generate random numbers is to use the C style srand() and rand() functions. srand() functions need to be called once in the application, which initializes the random number generator, also known as the set seed (seeding). Usually the current system time should be used as seed.

Warning;

In software based random number generators, high-quality seeds must be used. If the random number generator is initialized with the same seed each time, the random number sequence generated each time is the same. This is why the current system time is usually used as the seed.

After initializing the random number generator, the random number is generated through rand(). The following example shows how to use srand() and rand(). time(nullpt) calls return

srand(static_cast<unsigned int>(time(nullptr)));
cout<<rand()<<endl;

Random numbers in a specific range can be generated by the following functions:

The random number generated by the old C-style rand() function is between 0 and RAND_MAX. according to the standard, RAND_MAX should be at least 32767. However, the low order of rand() is usually not very random, that is, the range of random numbers generated by getRandom() above is small (for example, between 1 and 6), and the randomness is not very good.

be careful:

The random number generator based on software can never generate real random numbers, but generate random effects according to mathematical formulas, so it is called pseudo-random number generator.

The old srand() and rand() functions do not provide much flexibility. For example, the distribution of generated random numbers cannot be changed. C++11 adds a very powerful library that can generate random numbers according to different algorithms and distributions. This library is defined in the header file. This library has three main components: random number engine, random number engine adapter and distribution (distribution). The random number engine is responsible for generating the actual random number and saving the state required to generate subsequent random numbers. Distribution determines the range of generated random numbers and the mathematical distribution of random numbers within this range. The random number engine adapter modifies the results generated by the associated random number engine. It is strongly recommended not to use srand() and rand() And the class in use.

Random number engine

The following random number engines are available:

random_device

linear_congruential_engine

mersenne_twister_engine

Subtract_with_carry_engine

The random_device engine is not a software based random number generator; it is a special engine that requires computers to connect to hardware that can really generate uncertain random numbers, such as through physical principles. A classical mechanism is to measure the decay of radioisotopes by calculating the number of alpha particles in each time interval or similar methods, but there are many other types A physical principle based generator, including the method of measuring the "noise" of the reverse bias diode (there is no need to worry about the radioactive source in the computer) Due to space constraints, this book does not explain the details of these mechanisms. According to the specification of random_device, if the computer is not connected to such hardware devices, the library can choose a software algorithm. The choice of algorithm depends on the designer of the library.

The quality of the random number generator is determined by the entropy of the random number. If the random_device class uses a pseudo-random number generator based on software, the value returned by the entropy() method of this class is 0.0; if a hardware device is connected, it returns a non-zero value. This non-zero value is an estimate of the entropy of the connected device. The use of the random_device engine is very simple;

random_device rnd;
cout<<"Entropy: "<<rnd.entropy()<<endl;
cout<<"Min value: "<<rnd.min()
	<<", Max value: "<<rnd.max()<<endl;
cout<<"Random number: "<<rnd()<<endl;

The output of this program is as follows;

xz@xiaqiu:~/study/test/test$ ./test
Entropy: 0
Min value: 0, Max value: 4294967295
Random number: 3393511293

Random_device is usually slower than pseudo-random number engine. Therefore, if you need to generate a large number of random numbers, it is recommended to use pseudo-random number engine and use random_device to generate seeds for random number engine. In addition to random_device random number engine, there are three pseudo-random number engines, linear congruent engine The minimum amount of memory required to save the state. The state is an integer containing the last generated random number. If the random number has not been generated, the initial seed is saved. The cycle of this engine depends on the parameters of the algorithm, up to 2 ", but usually not so high. Therefore, if you need to use high-quality random number sequences, you should not use the linear congruence engine.

Among the three pseudo-random number engines, the random number quality generated by Mason rotation algorithm is the highest. The period of Mason rotation algorithm depends on the algorithm parameters, but it is much longer than that of linear congruence engine. The amount of memory required by Mason rotation algorithm to save the state also depends on the algorithm parameters, but it is much higher than the integer state of linear congruence engine. For example, the predefined Mason rotation algorithm mt19937 has a period of 2 ^ 19937 - 1, and the state contains 625 integers, about 2.5SKB. It is one of the fastest random number engines. The subtract with carry engine requires approximately 100 bytes of state to be saved. However, the quality of random numbers generated by this random number engine is not as good as Mason rotation algorithm. The mathematical principles of these random number engines are beyond the scope of this book, and the definition of random number quality requires a certain mathematical background. For a deeper understanding of this topic, refer to the references listed in the "random numbers" section of Appendix B. random_ The device random number engine is easy to use and does not require any parameters. However, when creating instances of these three pseudo-random number generators, you need to specify some mathematical parameters, which can be complex. The choice of parameters will greatly affect the quality of the generated random number. For example, Mersenne_ twister_ The definition of engine class is as follows:

template<class UIntType, size_t w, size_t n, size_t m, size_t r,UIntType a, size_t u, UIntType d, size_t s,UIntType b, size_t t, UIntType c, size_t l, UIntType f>class mersenne_twister_engine {...}

This definition requires 14 parameters. linear_congruential_engine class and subtract_ with_ carry_ The engine class also needs some of these mathematical parameters. Therefore, the C + + standard defines some predefined random number engines, such as mt19937 Mason rotation algorithm, which are defined as follows:

using mt19937 = mersenne_twister_engine<uint_fast32_t, 32, 624, 397, 31,0x9908b0df, 11, 0xffffffff, 7, 0x9d2c5680, 15, 0xefc60000, 18,1812433253>;

Unless you understand the details of Mason rotation algorithm, you will feel that these parameters are magic numbers. Generally, you don't need to modify any parameters unless you are a mathematical expert in pseudo-random number generator. It is strongly recommended to use these predefined type aliases, such as mt19937. Section 20.3.2 will fully list all predefined random number engines.

The random number engine adapter modifies the results generated by the associated random number engine, which is called base engine. This is an example of the adapter pattern. C + + defines the following three adapter templates:

template<class Engine, size_t p, size_t r> class
	discard_block_engine {...}
template<class Engine, size_t w, class UIntType> class
	independent_bits_engine {...}
template<class Engine, size_t k> class
	shuffle_order_engine {...}

discard_ block_ The engine adapter discards some values generated by the base engine to generate random numbers. The adapter template requires three parameters: the engine to be adapted, the block size P, and the block size r used. The base engine is used to generate P random numbers. The adapter discards the p - r number and returns the remaining R number.

independent_ bits_ The engine adapter combines some random numbers generated by the base engine to generate a random number with a given location number w.

shuflle_ order_ The random number generated by the engine adapter is consistent with the random number generated by the base engine, but the order is different. The specific working principle of these adapters is related to mathematical knowledge, which is beyond the scope of this book. The C + + standard includes some predefined random number engine adapters. Section 20.3.3 lists predefined random number engines and engine adapters. 20.3.3 "predefined random number engine and engine adapter

Predefined random number engine and engine adapter

As mentioned earlier, it is recommended not to specify the parameters of the pseudo-random number engine and engine adapter, but to use those standard random number engines. C + + defines the predefined engines and engine adapters shown in table 20-2, which are defined in the header file. They all have complex template parameters, but you can use them even if you don't understand them.

[external link picture transfer failed. The source station may have anti-theft chain mechanism. It is recommended to save the picture and upload it directly (img-aCwg1K6W-1633182167484)(/home/xz / picture / selection _999(119)].png)

default_random_engine is compiler related. An example of using these predefined engines is given in section 20.3.4.

Generate random number

Before generating random numbers, first create an engine instance. If you use a software based engine, you also need to define the distribution. Distribution is a mathematical formula that describes the distribution of numbers in a specific range. The recommended way to create an engine is to use the predefined engine discussed earlier. The following example uses a predefined Mason rotation algorithm named mt19937. This is a software based generator. Like the old rand() generator, software based engines need to be initialized with seeds. The seed of srand() is often the current time. In modern C + +, it is recommended to use random instead of any time-based seed (random_device does not own classes)_ Device generation seed:

random_device seeder;const auto seed = seeder.entropy()?seeder():time(nullptr);

Next, the distribution is defined. This example uses a uniform integer distribution in the range 1 ~ 99. In this example, uniform distribution is easy to use:

uniform_int_distribution<int> dist(1,99);

After defining the engine and distribution, you can generate random numbers by calling the distributed function call operator and passing in the engine as a parameter. In this example, it is written as dist(eng):

cout<<dis(eng)<<endl;

As you can see from this example, in order to generate random numbers through a software based engine, you always need to specify the engine and distribution. Using the std::bind() utility defined in the header file introduced in Chapter 18 can avoid specifying the distribution and engine when generating random numbers. The following example, like the previous example, uses the mt19937 engine and uniform distribution. This example defines gen() by binding eng to the first parameter of dist() through std::bind(). In this way, no parameters are required when calling Gen () to generate a new random number. Then this example demonstrates how to use the gen () and generate() algorithms together to fill a 10 element vector with random numbers. Chapter 18 discusses the generate() algorithm, which is defined in.

random_device seeder;const auto_seed = seeder.entropy()?seeder():time(nullptr);mt1993 eng(static_cast<mt19937::restulty_type>(seed));uniform_int_distribution<int> dist(1,99);auto gen = std::bind(dist,eng);vector<int> vec(10);generate(begin(vec),end(vec),gen);for(auto i : vec){cout<<i<<" ";}

be careful;

Remember that the generate() algorithm overwrites existing elements and does not insert new elements. This means that you first need to set vector to large enough to hold the required number of elements, and then call the generate() algorithm. The previous example specified the size of the vector as the parameter to construct the number of grandchildren. Although you don't know what the specific type of gen() is, you can pass gen() into another function that needs to use a generator. You have two options: use an argument of type std::function < int(), or use a function template. In the previous example, you can generate random numbers in the fllVector() function instead. Here is the implementation using std::function:

void fillVector(vector<int>& vec,const std::function<int()>& generator){	generate(begin(vec),end(vec),generator);}

The following is the function template version:

template<typename T>void fillVector(vector<int>& vec,const T& generator){	generate(begin(vec),end(vec),generator);}

Use this function as follows:

#include <iostream>#include <random>#include <ctime>#include <functional>#include <algorithm>using namespace std;template<typename T>void fillVector(vector<int>& vec,const T& generator){    generate(begin(vec),end(vec),generator);}int main(){    random_device seeder;    const auto seed = seeder.entropy()?seeder():time(nullptr);    mt19937 eng(static_cast<mt19937::result_type>(seed));    uniform_int_distribution<int> dist(1,99);    auto gen = std::bind(dist,eng);    vector<int> vec(10);    fillVector(vec, gen);    for(auto i : vec) { cout<<i<<" ";}    return 0;}

output

xz@xiaqiu:~/study/test/test$ ./test9 76 95 64 57 41 29 43 6 53 x

Random number distribution

Distribution is a mathematical formula that describes the distribution of numbers in a specific range. The distribution provided by the random number generator library can be used in combination with the pseudo-random number engine to define the distribution of the generated random numbers. This is a compressed representation. The first line of each distribution is the name of the class and the class template parameters, if any. The next line is the constructor for this distribution. Each distribution lists only one constructor to help readers understand this class. The standard library reference resources (see Appendix B) list all constructors and methods for each distribution.

uniform distribution

template<class IntType = int> class uniform_int_distribution
uniform_int_distribution(IntType a = 0,IntType b = numeric_limits<IntType>::max());
template<class RealType = double> class uniform_real_distribution
uniform_real_distribution(RealType a = 0.0, RealType b = 1.0);

Bernoulli distribution:

class bernoulli_distributionbernoulli_distribution(double p = 0.5);template<class IntType = int> class binomial_distributionbinomial_distribution(IntType t = 1, double p = 0.5);template<class IntType = int> class geometric_distributiongeometric_distribution(double p = 0.5);template<class IntType = int> class negative_binomial_distributionnegative_binomial_distribution(IntType k = 1, double p = 0.5);

Poisson distribution

template<class IntType = int> class poisson_distributionpoisson_distribution(double mean = 1.0);template<class RealType = double> class exponential_distributionexponential_distribution(RealType lambda = 1.0);template<class RealType = double> class gamma_distributiongamma_distribution(RealType alpha = 1.0, RealType beta = 1.0);template<class RealType = double> class weibull_distributionweibull_distribution(RealType a = 1.0, RealType b = 1.0);template<class RealType = double> class extreme_value_distributionextreme_value_distribution(RealType a = 0.0, RealType b = 1.0);

Normal distribution;

template<class RealType = double> class normal_distributionnormal_distribution(RealType mean = 0.0, RealType stddev = 1.0);template<class RealType = double> class lognormal_distributionlognormal_distribution(RealType m = 0.0, RealType s = 1.0);template<class RealType = double> class chi_squared_distributionchi_squared_distribution(RealType n = 1);template<class RealType = double> class cauchy_distributioncauchy_distribution(RealType a = 0.0, RealType b = 1.0);template<class RealType = double> class fisher_f_distributionfisher_f_distribution(RealType m = 1, RealType n = 1);template<class RealType = double> class student_t_distributionstudent_t_distribution(RealType n = 1);

Sampling distribution

template<class IntType = int> class discrete_distributiondiscrete_distribution(initializer_list<double> wl);template<class RealType = double> class piecewise_constant_distributiontemplate<class UnaryOperation>piecewise_constant_distribution(initializer_list<RealType> bl,UnaryOperation fw);template<class RealType = double> class piecewise_linear_distributiontemplate<class UnaryOperation>piecewise_linear_distribution(initializer_list<RealType> bl,UnaryOperation fw);

Each distribution requires a set of parameters. The detailed explanation of these mathematical parameters is beyond the scope of this book. The rest of this section will give some examples to explain the impact of distribution on the generated random numbers. Observing the graph is the easiest way to understand the distribution. For example, the following code generates 1 million random numbers between 1 and 99, and then calculates the number of times a number between 1 and 99 is randomly selected. Save the results in a map. The key of the map is a number between 1 and 99, and the value associated with the key is the number of times the key is randomly selected. After the loop, the results are written to a CSV (HI separated values) file, which can then be opened in the spreadsheet application

const unsigned int kStart = 1;const unsigned int kEnd = 99;const unsigned int kIteration = 1'000'000;//Uniform Mersenne Twisterrandom_device seeder;const auto seed = seeder.entropy()?seeder():time(nullptr);mt19937 eng(static_cast<mt19973::result_type>(seed));uniform_int_distribution<int> dist(kStart,kEnd);auto gen = bind(dist,eng);map<int,int> m;for(unsigned int i = 0;i<kIterations;++i){	// Search map for a key = rnd. If found, add 1 to the value associated	// with that key. If not found, add the key to the map with value 1.	++(m(rnd));}//write to a CSV fileofstream of("res.scv");for(unsigned int i = kStart;i <= kEnd;++i){    of<<i<<","<<m[i]<<endl;}

optional

std::optional is defined in to save a specific type of value or nothing. If you want the value to be optional, you can use it as an argument to the function. If a function can return some values or nothing, it can also be used as the return type of the function. This eliminates the need to return special values from functions such as nullptr, end(), - 1, and EOF. In this way, it is not necessary to make the written function return Boolean values, and store the actual values in the reference output parameters, such as bool GetData (T & dataoub). In the following example, the function returns optional;

optiona<int> getData(bool giveIt){	if(giveIt)	{		return 42;		}    return nullopt; //or simply return {}}

This function can be called as follows:

auto data1 = getData(true);auto data2 = getData(false);

To determine if optional has a value, use has_value() method, or use optional in the let statement;

cout<<"data1.has_value = "<<data1.has_value()<<endl;if(data2){	cout<<"data2 has a value."<<endl;}

If optional has a value, you can receive it using value() or use the following backreference operator,

cout<<"data1.value = "<<data1.value()<<endl;cout<<"data1.value = "<<*data1<<endl;

If value() is called on an empty optional, bad will be thrown_ optional_ Access exception.

Value can be used_ Or() returns an optional value, or another value if optional is empty:

cout<<"data2.value = "<<data2.value_or(0)<<endl;

Note that references cannot be stored in optional, so optional < T & > is not feasible. Instead, use optional < T * >, optional < reference_ Wrapper > or optional < reference_ wrapper>. As mentioned in Chapter 17, you can use std::ref() or cref() to create STD:: reference respectively_ Wrapper or reference_ wrapper .

variant

std::variant is defined in and can be used to hold a value of a given type collection. When defining a variant, you must specify the types it may contain. For example, the following code defines that a variant can contain an integer, string, or floating-point value at a time;

variant<int,string,float>v;

Here, the variant of this default construct contains the default construct value of the first type (int in this case). To construct a variant by default, make sure that the first type of the variant is constructable by default. For example, the following code cannot be compiled because Foo is not constructable by default.

class Foo{public:Foo() = delete;F(int){}};
class Bar{public:Bar() = delete;Bar(int){}};

In fact, neither Foo nor Bar is constructable by default. If you still need to construct variant by default, you can use STD:: monostate (an empty alternative) as the first type of variant:

variant<monostate,Foo,Bar> v;

You can use the assignment operator to store the contents in the variant:

variant<int,string,float> v;v = 12;v = 12.5f;v = "An std::string"s;

Variant can contain only one value at any given time. Therefore, for these three lines of code, first store the integer 12 in the variant, then change the variant to include floating-point values, and finally change the variant to include strings. Determine whether the variant currently contains a specific type of value:

cout<<"Type index: "<<v.index()<<endl;cout<<"Contains an int: "<<holds_alternative<int>(v)<<endl;

The output is as follows:

Type index: 1Contains an int: 0

Use std::get() or std::get() to retrieve values from variant. These functions throw bad if you use an index of type, or if you use a type that does not match the current value of variant_ variant_ Access exception:

cout<<std::get<string>(v)<<endl;
try
{
	cout<<std::get<0>(v)<<endl;
}
catch(const bad_variant_access& ex)
{
	cout<<"Exception: "<<ex.what()<<endl;  
}

The output is as follows

An std::string
Exception: bad variant access

To avoid exceptions, use std::get_if() or std::get_if() helper function. These functions receive a pointer to the variant, return a pointer to the requested value, and return nullptr if an error is encountered.

string* theString = std::get_if<string>(&v);int* theInt = std::get_if<int>(&v);cout<<"retrieved string: "<<(theString?*theString:"null")<<endl;cout<<"retrieved int: "<<(theInt?*theInt:0)<<endl;

The output is as follows

retrieved string: An std::stringretrieved string: 0

You can use the std::visit() helper function to apply the visitor pattern to the variant. Suppose the following classes define multiple overloaded function call operators, one for each possible type in the variant:

class MyVisitor{	public:		void operator()(int i){cout<<"int "<<i<<endl;}		void operator()(const string& s){cout<<"string "<<s<<endl;}		void operator()(float f){cout<<"float "<<f<<endl;}};

You can use it with std::visit(), as follows:

visit(MyVisitor(),v);

This will call the appropriate overloaded function call operator based on the value currently stored in the variant. The output of this example is as follows:

string An std::string

As with optional, you cannot store references in a variant. Pointer and reference can be stored_ Wrapper or reference_ An instance of the wrapper.

any

std::any, defined in, is a class that can contain values of any type. Once built, you can confirm whether the any instance contains values and the type of values contained. To access the contained values, you need to use any_cast(), if it fails, bad will be thrown_ any_ Exception of type cast. Here is an example,

any empty;
any anInt(3);
any aString("An std::string "s);
cout<<"empty.has_value = "<<empty.has_value()<<endl;
cout<<"anInt.has_value = "<<anInt.has_value()<<endl<<endl;

cout<<"anInt wrapped type = "<<anInt.type().name()<<endl;
cout<<"aString wapped type = "<<aString.type().name()<<endl<<endl;

int theInt = any_cast<int>(anInt);
cout<<theInt<<endl;
try
{
    int test = any_cast<int>(aString);
    cout<<test<<endl;
}
catch(const bad_any_cast& ex)
{
    cout<<"Exception: "<<ex.what()<<endl;
}

The output is shown below. Note that the wrapper type of aString is compiler dependent.

ampty.has_value = 0;anInt.has_value = 1;anInt wrapped type = int;aString wrapped type = class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char>>    3    Exception: Bad any_cast

You can assign new values to any instances, or even new values of different types

any something(3); //Now it contains an integersomething = "An std::string"s; //Now the same instance contains a string

An instance of any can be stored in a standard library container. This allows heterogeneous data to be stored in a single container. The only drawback to this is that you can only explicitly execute any_cast to retrieve a specific value, as follows:

vector<any> v;v.push_back(any(42));v.push_back(any("An std::string"s));cout<<any_cast<string>(v[1])<<endl;

Like optional and variant, references to any instances cannot be stored. Pointer and reference can be stored_ Wrapper or reference_ An instance of the wrapper.

tuple

The std::pair class introduced in Chapter 17 and defined in can hold two values, each of which has a specific type. The type of each value should be determined at compile time. Here is a simple example,

pair<int,string> p1(16,"Hello world");pair<bool,float> p2(true,0.1234f);cout << "p1 = (" << p1.first << ", " << p1.second << ")" << endl;cout << "p2 = (" << p2.first << ", " << p2.second << ")" << endl;

The output is shown below

p1 = (16,Hello world)p2 = (1,0.124)

There is also the std::tuple class, which is defined in the header file. Tuple (tuple) is a generalization of pair, which allows any number of values to be stored, and each value has its own specific type. Like pair, the size and value type of tuple are determined at compile time and are fixed.

Tuple can be created by tuple constructor. You need to specify the template type and actual value. For example, the following code creates a tuple. The first element is an integer, the second element is a string, and the last element is a Boolean value:

using MyTuple = tuple<int,string,bool>;MyTuple t1(16,"Test",true);

std::get() gets the ith element from tuple, i is the index starting from 0;, Therefore, < 0 > represents the first element of the tuple, < 1 > represents the second element of the tuple, and so on. The type of the return value is the correct type for that index position in the tuple

cout<<"t1 = (" <<get<0>(t1)<<","<<get<1>(t1)<<", "<<get<2>(t1)<<")"<<endl;//Output: t1 = (16,Test,1)

You can check whether get() returns the correct type through typeid() in the header file. The output of the following code shows that the value returned by get < 1 > (T1) is indeed std::string:

const <<"Type of get<1>(t1) = "<<typeid(get<1>(t1)).name()<<endl;//Outputs:Type of get<1>(t1) = class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char>>

The exact string returned by typeid() is compiler dependent. The output in the above example is from Visual C++ 2017. You can also use std::get() to extract elements from the tuple according to the type, where is the type of the element to be extracted (not the index). If the tuple has several elements of the required type, the compiler will generate an error. For example, you can extract string elements from tl:

cout<<"String = "<<get<string>(t1)<<endl;//Outputs:String = Test	

Unfortunately, iterating over tuple values is not straightforward. You cannot write a simple loop or call get(mytuple), etc., because the value of i must be known at compile time. One possible solution is to use template meta programming, as detailed in Chapter 22, which gives an example of printing tuple values.

cout<<"Tuple size = "<<tuple_size<decltype(t1)>::value<<endl;//Outputs:Tuple Size = 3

In C++17, the template parameter derivation rules of constructor are provided. When constructing a tuple, you can ignore the template type parameters and let the compiler deduce automatically according to the argument types passed to the constructor. For example, the same tuple is defined below, which contains an integer, a string, and a Boolean value. Note that "Tests" must be specified to ensure that it is std::string.

std::tuple t1(16,"Test"s,true);

Due to the automatic derivation of types, references cannot be specified through &. If you need to generate a tuple containing references or constant references through the template parameter derivation of the constructor, you need to use ref() and cref() respectively. Ref() and cref() Auxiliary functions are defined in the header file. For ex amp le, the following construction will generate a tuple of type tuple < int double &, const double &, string & >

double d = 3.14;
string str1 = "Test";
std::tuple t2(16,ref(d),ref(str1));

To test the double reference in the tuple package, the following code first writes the value of the double variable to the console. Then calling get<1> (T2), this function actually returns a reference to D, because the second tuple (index 1) element uses ref(d). . the second line modifies the value of the referenced variable, and the last line shows that the value of D has indeed been modified by the reference saved in the tuple. Note that the third line failed to compile because cref() is used for the third tuple element, that is, it is a constant reference of D.

cout<<"d = "<<d<<endl;
get<1>(t2) *= 2;
//get<2>*(t2) *= 2; //ERROR because of cref()
cout<<"d = "<<d<<endl;
//Outputs: d = 3.14
//		   d = 6.28

If you do not use the template parameter derivation method of the constructor, you can use the std:make_tuple() tool function to create a tuple. With this auxiliary function template, you can create a tuple by specifying only the actual value. The type is automatically derived at compile time, such as

auto t2 = std::make_tuple(16,ref(d),cref(d),ref(str1));

Decomposition tuple

There are two ways to decompose a tuple into separate elements: structured binding (C++17) and std::tie().

1. Structured binding

C++17 introduces structured binding, which allows people to easily decompose a tuple into multiple variables. For example, the following code defines a tuple, which includes an integer, a string and a Boolean value. After that, the tuple is divided into three independent variables using structured binding:

tuple t1(16,"Test"s,true);auto[i,str,b] = t1;cout<<"Decomposed: i= "	<<i<<",str = \""<<str<<"\",b= "<<b<<endl;

Using structured binding, you cannot ignore specific elements when decomposing. If a tuple contains three elements, the structured binding requires three variables. If you want to ignore elements, you must use tie(). The result of tie(). Since the result of tie() is a reference tuple, the assignment actually changes the value of three independent variables.

tuple<int,string,bool> t1(16,"Test",true);int i = 0;string str;bool b = false;cout<<"Before: i = "<<i<<",str = \""<<str<<"\",b= "<<b<<endl;tie(i,str,b) = t1;cout<<"After: i= "<<i<<",str = \""str<<"\",b= "<<b<<endl;

give the result as follows

Before: i = 0,str = "",b = 0After: i = 16,str = "Test",b = 1

With tie(), you can ignore some elements that you don't want to decompose. Instead of using the variable name of the decomposition value, you use the special std::ignore value. For the previous example, the string element of tuple is ignored when calling tie():

tuple<int,string,bool> t1(16,"Test",true);int i = 0;bool b = false;cout<<"Before: i = "<<i<<", b = "<<b<<endl;

Here is the new output;

Before: i = 0, b = 0After: i = 16,b = 1

series connection

Two tuples can be concatenated into one tuple through std::tuple_cat(). In the following example, the type of t is tuple < int, string, bool, double, string >

tuple<int,string,bool>t1(16,"Test",true);tuple<double,string>t2(3.14,"string 2");auto t3 = tuple_cat(t1,t2);auto [i,str,b,d,s] = t3;cout<<i<<" "<<str<<" "<<b<<" "<<d<<" "<<s<<endl;

output

xz@xiaqiu:~/study/test/test$ ./test16 Test 1 3.14 string 2xz@xiaqiu:~/study/test/test$ 

compare

Tuple also supports the following comparison operators: = =,! =, <, >, < = and > =. In order to use these comparison operators, the element types stored in tuple should also support these operations. For example:

tuple<int,string> t1(123,"def");tuple<int,string> t2(123,"abc");if(t1 < t2){	cout<<"t1 < t2"<<endl;}else{	cout<<"t1 > t2"<<endl;}

The output is as follows:;

t1 >= t2

For custom types that contain multiple data members, tuple comparison can be used to easily implement the dictionary comparison operators of these types. For example, the following simple structure contains three data members:

struct Foo{	int mInt;	string mStr;	bool mBool;};

Of course, in production environment level code, private data members should have public getter methods and possible public setter methods. In order to keep the code simple and grasp the key points, this example uses public struct. It is not easy to implement the correct operator < of Foo! However, it can be done simply by using std::tie() and tuple comparison;

bool operator<(const Foo& f1,const Foo& f2){	return tie(f1.mInt,f1.mStr,f1.mBool)<tie(f2.mInt,f2.mStr,f2.mBool);}

The following is a usage example;

Foo f1(42,"Hello",0);Foo f2(32,"World",0);cout<<(f1 < f2)<<endl;cout<<(f2 < f1)<<endl;

make_from_tuple()

Use std::make_from_tuple() to build an object of type I, and pass the element of the given tuple as a parameter to the constructor of T. for example, suppose it has the following classes:

class Foo{	public:		Foo(string str,int i):mStr(str),mInt(i){}	private:    	string mStr;    	int mInt;};

make_from_tuple() can be used as follows:

auto myTuple = make_tuple("Hello world ",42);auto foo = make_from_tuple<Foo>(myTuple);

The argument provided to make_from_tuple() may not be a tuple, but it must support STD:: get < > and std::tuple_size. std::array and std::pair also meet these requirements. In daily work, this function is not practical, but it can be convenient if you want to write generic code using templates or metaprogramming templates.

apply()

std::apply() calls the given function, lambda expression and function object, and passes the element of the given tuple as an argument. The following is an example:

int add(int a,int b){ return a + b;}...cout<<apply(add,std::make_tuple(39,3))<<endl;

Like make_from_tuple(), this function is not practical in daily work, but it is convenient if you want to write generic code using templates or do template metaprogramming.

File system support library

C++17 introduces file system support libraries, which are all defined in header files and located in the std::filesystem namespace. It allows you to write portable code for the file system. Using it, you can distinguish between directories and files, iterate the contents of directories, manipulate paths, and retrieve file information (such as size, extension, creation time, etc.) . here are the two most important aspects of the Library: path and directory_entry.

path

The basic component of this library is path. Path can be absolute path or relative path, including file name or not. For example, the following code defines some paths. Note that the literal of the original string is used to avoid escaping the backslash.

path p1(LR"(D:\Foo\Bar)");
path p2(L"D:/Foo/Bar");
path p3(L"D:/Foo/Bar/MyFile.txt");
path p4(LR"(..\SomeFolder)");
path p5(L"/usr/lib/X11");

When you convert a path to a string (such as using the c_str() method) or insert a stream, it is converted to a local format in the system on which the code is running. For example:

#include <tuple>#include <iostream>#include <filesystem>using namespace std;int main(){    filesystem::path p1(LR"(D:\Foo\Bar)");     filesystem::path p2(L"D:/Foo/Bar");     cout << p1 << endl;     cout << p2 << endl;     return 0;}

The output is shown below

xz@xiaqiu:~/study/test/test$ ./test"D:\\Foo\\Bar""D:/Foo/Bar"xz@xiaqiu:~/study/test/test$ 

You can append a component to a path using the append() method or operator =. A separator is automatically added to the path. For example:

path p(L"D:\\Foo");p.append("Bar");p /= "Bar";cout<<p<<endl;

output

"D:\\Foo/Bar/Bar"

You can use concat (method or operator =) to connect a string to an existing path. At this time, the path will not add a separator. For example:

path p(L"D:\\Foo");p.concat("Bar");p+="Bar";cout<<p<<endl;

output

xz@xiaqiu:~/study/test/test$ ./test"D:\\FooBarBar"xz@xiaqiu:~/study/test/test$ 

Warning:

append() and operator / = automatically add path separators, while concat() and operator + = do not.

path supports the use of iterators to help generate different components. The following is an example:

#include <iostream>#include <filesystem>using namespace std;int main(){    filesystem::path p(LR"(./Foo/Bar)");    for(const auto& component : p)    {         cout<<component<<endl;    }    return 0;}

The output is as follows:

xz@xiaqiu:~/study/test/test$ ./test".""Foo""Bar"xz@xiaqiu:~/study/test/test$ 

The path interface supports operations such as remove_filename(), replace_filename(), replace_extension(), root_name(), parent_path(), extension(), has_extension(), is_absolute() and is_relative(). Please refer to the standard library reference resources (see Appendix B) for a complete list of all available functions.

directory_entry

Path only represents the directory or file of the file system. Path may refer to a directory or file that does not exist. If you want to query the actual directory or file in the file system, you need to build a directory_entry from path. If the given directory or file does not exist, the structure will fail. The directory_entry interface supports is_directory(), is_regular_file(), is_socket(), is_symlink() , file_size() and last_write_time().

The following example constructs a directory_entry from path to query the file size:

#include <tuple>#include <iostream>#include <filesystem>using namespace std;int main(){    filesystem::path myPath(L"/home/xz/tool/test");    filesystem::directory_entry dirEntry(myPath);    if(dirEntry.exists() && dirEntry.is_regular_file())    {        cout<<"File size: "<<dirEntry.file_size()<<endl;    }    return 0;}

output

xz@xiaqiu:~/study/test/test$ ./testFile size: 8xz@xiaqiu:~/study/test/test$ 

auxiliary function

There is a complete set of auxiliary functions available. For example, copy() can be used to copy a file or directory, create_directory() can be used to create a new directory in the file system, exists() can be used to query whether a given directory or file exists, file_size() can be used to obtain the file size, last_write_time() can be used to obtain the last modification time of the file, and remove() can be used Delete files, use temp_directory_path() to get the directory suitable for saving temporary files, use space() to query the available space in the file system, etc. see the standard library reference resources (see Appendix B) for a complete list.

The following example prints the capacity of the file system and the current free space:

#include <tuple>#include <iostream>#include <filesystem>using namespace std;int main(){    filesystem::space_info s = filesystem::space("/home/xz/");    cout<<"Capacity: "<<s.capacity<<endl;    cout<<"Free: "<<s.free<<endl;    return 0;}

output

xz@xiaqiu:~/study/test/test$ ./testCapacity: 489639649280Free: 317362716672xz@xiaqiu:~/study/test/test$ 

Catalog and iteration

If you want to recursively and iterate over all files and subdirectories in a given directory, use the following recursive _directoryiterator:

void processPath(const path& p){	if(!exists(p))        return;    auto begin = recursive_directory_iterator(p);    auto end recursive_directory_iterator();    for(auto iter = begin;iter!=end;++iter)    {        const string spacer(iter.depth()*2,' ');                auto& entry = *iter;        if(is_regular_file(entry))        {            cout<<spacer<<"File: "<<entry;            cout<<" ("<<file_size(entry) <<" bytes)"<<endl;        }        else        {            std::cout<<spacer<<"Dir: "<<entry<<endl;        }    }}

This function can be called as follows

path p(LR"(/home/xz/study/test/test)");processPath(p);

output

xz@xiaqiu:~/study/test/test$ ./test
File: "/home/xz/study/test/test/res.csv" (866 bytes)
File: "/home/xz/study/test/test/test.go" (1935 bytes)
File: "/home/xz/study/test/test/test2.cpp" (46 bytes)
File: "/home/xz/study/test/test/test.cpp" (775 bytes)
File: "/home/xz/study/test/test/test1.cpp" (101 bytes)
File: "/home/xz/study/test/test/CMakeLists.txt" (916 bytes)
File: "/home/xz/study/test/test/test" (68096 bytes)
Dir: "/home/xz/study/test/test/build"

You can also use directory_iterator and the contents of the under generation directory, and implement recursion by yourself. The following example has the same function as the above example, but uses directory_iterator instead of recursive _directory_iterator:

void processPath(const path& p, size_t level = 0)
{
    if (!exists(p)) 
    {
    	return;
    }
    const string spacer(level * 2, ' ');
    if (is_regular_file(p)) 
    {
        cout << spacer << "File: " << p;
        cout << " (" << file_size(p) << " bytes)" << endl;
    } 
    else if (is_directory(p)) 
    {
        std::cout << spacer << "Dir: " << p << endl;
        for (auto& entry : directory_iterator(p)) 
        {
            processPath(entry, level + 1);
        }
    }
}

Master the advanced features of C + +

Chapter 16-18 mentioned that the standard library is a collection of powerful general containers and algorithms. The information you currently know should be enough to meet the needs of most applications. However, the previous chapter only introduces the basic functions of the library. You can customize and expand the standard library in any way you need. For example, you can apply iterators to input and output streams and write your own Containers, algorithms and iterators; you can even specify that containers use custom memory allocation schemes. This chapter explains these advanced features through the development process of container hash_map.

Warning;

It is rarely necessary to customize and extend the standard library. If you are not clear about the basic standard library containers and algorithms explained in the previous chapter, you can skip this chapter. However, if you want to understand the standard library and are not satisfied with using the standard library, this chapter is worth reading. You should be familiar with the operator overloading explained in Chapter 15. Because this chapter uses a lot of templates, you will continue to You should also be familiar with the template explained in Chapter 12 before continuing to read this chapter.

distributor

Each standard library container receives the Allocator type as a template parameter. In most cases, the default value is sufficient. For example, the definition of the vector template is as follows;

template <class T, class Allocator = allocator<T>> class vector;

The container constructor also allows you to specify objects of type allocator. These additional parameters allow you to customize how the container allocates memory. Each memory allocation performed by the container is performed by calling the allocate() method of the allocator object. On the contrary, each memory release is performed by calling deallocate() of the allocator object Method. The standard library provides a default allocator class called allocator, which implements these methods by wrapping operator new and operator delete. If the container in the program needs to use a custom memory allocation and release scheme, you can write your own allocator class. There are several reasons for using a custom allocator. For example , if the performance of the underlying allocator is unacceptable, but a CAI changed allocator can be built; or if the memory fragmentation problem (a large number of different allocation and release operations lead to many unavailable small holes in memory) is serious, some type of "object pool" can be created This method is called memory pool. If you have to allocate space for specific functions of the operating system, such as shared memory segments, you can use standard library containers in shared memory segments through custom allocators. The use of custom allocators is very complex and may cause many problems if you are not careful, so you should not use custom allocators carelessly. Alloc is provided for any ate(),deallocate() And other classes requiring method and type aliases can be replaced by the default allocator class. C++17 introduces the concept of polymorphic memory allocator. For allocators of containers specified as template type parameters, the problem is that two containers are very similar but have different allocator types. For example, two vector containers with different allocator template type parameters are The polymorphic memory allocator defined in the std::pmr namespace helps to solve this problem. std::pmr::polymorphicallocator is the appropriate allocator class because it meets various requirements, such as having allocate() and deallocate() Method. The allocation behavior of polymer_allocator depends on the memory_resource during construction, not on the template type parameters. Therefore, when allocating and freeing memory, although they have the same type, different polymer_allocators behave differently. The C + + standard provides some built-in memory resources for initializing polymorphic memory allocators: synchronized_pool_re Source, un synchronized_pool_resource and monotonic_buffer_resource. However, according to experience, these custom allocators and polymorphic memory allocators are quite advanced and rarely used. Therefore, these details are omitted in this book. For details, please refer to the relevant books of C + + standard library listed in Appendix B.

Stream adapter

The standard library provides four stream adapters. They are class templates similar to iterators, allowing input and output streams to be treated as input iterators and output iterators. Through these iterators, the input stream and output stream can be adapted, and they can be used as the source and target in different standard library algorithms. The available stream iterators are listed below:

ostream _ An iterator is an output stream and a path generator

istream_ An iterator is an input stream and a slave generator

And ostrambuf_ Iterator and istreambuf_iterator, but these are rarely used and will not be discussed in detail here.

Output stream and iterator

ostream_iterator is an output stream and iterator. It is a class template that receives element types as type parameters. The arguments received by the constructor of this class include an output stream and a delimiter string to be written to the stream after each element is written. ostream_ The iterator writes elements through the operator < < operator. For example, ostream can be used_ The iterator and copy() algorithms print out all the elements in the container in one line of code. The first parameter of copy() is the first iterator of the range to be copied, the second parameter is the tail and iterator of the range, and the third parameter is the target iterator.

#include <iostream>#include <iterator>#include <vector>#include <numeric>using namespace std;int main(){    vector<int> myVector(10);    iota(begin(myVector),end(myVector),1); //Fill vector with 1,2,3,...,10    //print the contents of the vector    copy(cbegin(myVector),cend(myVector),ostream_iterator<int>(cout," "));    return 0;}

output

xz@xiaqiu:~/study/test/test$ ./test1 2 3 4 5 6 7 8 9 10

Input stream iterator

You can also use the input stream and iterator istream_ The iterator reads values from the input stream through iterator abstraction. This is a class template, which takes the element type as a type parameter and reads the element through the operator > > operator. istream_iterator can be used as a source of algorithms and container methods. For example, the following code snippet reads integers from the console up to the end of the stream. In Windows, press Ctrl+Z and enter to reach the end of the stream. In Linux, press Ctrl+D and enter to reach the end of the stream. The calculate () algorithm is used to calculate the sum of all integers. Note that istream can be used_ The default constructor for iterator creates a tail iterator.

cout<<"Enter numbers seperated by white space."<<endl;cout<<"Press Ctr+Z followed by Enter to stop "<<endl;istream_iterator<int> numbersIter(cin);istream_iterator<int> endIter;int sum = accumulate(numbersIter,endIter,0);cout<<"Sum: "<<sum<<endl;

Consider these codes. If you delete all the output statements and variable declarations, all that remains is the accumulate() call. Since the algorithm and input stream antecedent are used and no explicit loop is used, this line of code reads any number of integers from the console and adds them together.

iterator adaptor

The standard library provides three iterator adapters, which are special iterators built on other iterators. The three and iterator adapters are defined in the header file. You can also write your own iterator adapter, but these are not discussed in this book. For more details, please refer to the relevant books of the standard library listed in Appendix B.

reverse iterator

STD:: reverse is provided in the standard library_ Iterator class templates to reverse iterate over bidirectional iterators or random access and iterators. All reverse iteratable containers in the standard library (every container in the C + + standard, except forward_list and unordered associated containers) provide the type alias reverse_iterator and rbegin0 and rend() methods. These reverse_ The type of iterator type alias is std::reverse_iterator, T equals the iterator type alias of the container. The rbegin() method returns a reverse that points to the last element in the container_ The iterator, rend() method also returns a reverse_iterator, which points to the element before the first element in the container. Yes, reverse_ When the operator + + operator is applied to the iterator, the operator – operator will be called to the underlying container friend, and vice versa. For example, you can traverse a collection from beginning to end by:

for(auto iter = begin(collection);iter != end(collection);++iter){}

To traverse the elements of this collection from end to end, you can call rbegin() and rend() to use reverse_iterator. Note that + + iter is still used here:

for(auto iter = rbegin(collection);iter != end(collection);++iter){}

std::reverse_iterator is mainly used when there is no equivalent algorithm in the standard library that can run in reverse. For example, the basic find() algorithm searches for the first element in a sequence. If you want to search for the last element in the sequence, you can use reverse instead_ iterator. Note that through reverse_ When the iterator calls an algorithm such as find(), the algorithm returns a reverse_iterator. By calling reverse_ The iterator's base() method always gets this reverse_ The underlying iterator in the iterator. However, considering reverse_ In the concrete implementation of iterator, the iterator returned by base() always refers to the called reverse_ The last element of the element referenced by the iterator. In order to get the same element, you must subtract 1. Here is a combination of find() and reverse_ Example of iterator:

#include <iostream>
#include <iterator>
#include <vector>
#include <numeric>
#include <algorithm>
using namespace std;

template<typename Container>
void populateContainer(Container& cont)
{
    int num;
    while(true)
    {
        cout<<"Enter a number (0 to quit):";
        cin>>num;
        if(num == 0)
        {
            break;
        }
        cont.push_back(num);
    }
}

int main()
{
//The implementation of populateContainer() is identical to that show in
//Chapter 18,so it is omitted here
    vector<int> myVector;
    populateContainer(myVector);

    int num;
    cout << "Enter a number to find: ";
    cin >> num;
    auto it1 = find(begin(myVector), end(myVector), num);
    auto it2 = find(rbegin(myVector), rend(myVector), num);
    if (it1 != end(myVector))
    {
        cout << "Found " << num << " at position " << it1 - begin(myVector) << " going forward " << endl;
        cout << "Found " << num << " at position " << it2.base() - 1 - begin(myVector) << " going backward " << endl;
    }
    else
    {
        cout << "Failed to find " << num << endl;
    }
    return 0;
}

Output

xz@xiaqiu:~/study/test/test$ ./test
Enter a number (0 to quit):1
Enter a number (0 to quit):2
Enter a number (0 to quit):3
Enter a number (0 to quit):3
Enter a number (0 to quit):3
Enter a number (0 to quit):3
Enter a number (0 to quit):3
Enter a number (0 to quit):4
Enter a number (0 to quit):4
Enter a number (0 to quit):4
Enter a number (0 to quit):0
Enter a number to find: 3
Found 3 at position 2 going forward 
Found 3 at position 6 going backward 
xz@xiaqiu:~/study/test/test$ 

insert iterator

According to the description in Chapter 18, algorithms such as copy() do not insert elements into the container, but replace old elements in a certain range with new elements. In order to make algorithms such as copy() more useful, the standard library provides three insert iterators to actually insert elements into the container: insert_iterator,back_insert_iterator and front_insert_iterator. Inserts and iterators are templated according to the container type and receive the actual container reference in the constructor. By providing the necessary iterator interfaces, these adapters can be used as target iterators for algorithms such as copy(). Instead of replacing elements in the container, these adapters actually insert new elements by calling the container. Basic insert_ The iterator calls the insert(position, element) method of the container, back_insert_iterator calls push_back(element) method, front_insert_iterator calls push_front(element) method. For example, combine back_insert_iterator and copy_if() algorithm can fill vectorTwo with all elements not equal to 100 from vectorOne:

#include <iostream>
#include <iterator>
#include <vector>
#include <numeric>
#include <algorithm>
using namespace std;

template<typename Container>
void populateContainer(Container& cont)
{
    int num;
    while(true)
    {
        cout<<"Enter a number (0 to quit):";
        cin>>num;
        if(num == 0)
        {
            break;
        }
        cont.push_back(num);
    }
}

int main()
{
    vector<int> vectorOne, vectorTwo;
    populateContainer(vectorOne);

    back_insert_iterator<vector<int>> inserter(vectorTwo);
    copy_if(cbegin(vectorOne), cend(vectorOne), inserter,
        [](int i){ return i != 100; });

    copy(cbegin(vectorTwo), cend(vectorTwo), ostream_iterator<int>(cout, " "));
    return 0;
}

Output

xz@xiaqiu:~/study/test/test$ ./test
Enter a number (0 to quit):10
Enter a number (0 to quit):1
Enter a number (0 to quit):1
Enter a number (0 to quit):1
Enter a number (0 to quit):1
Enter a number (0 to quit):20
Enter a number (0 to quit):20
Enter a number (0 to quit):20
Enter a number (0 to quit):20
Enter a number (0 to quit):100
Enter a number (0 to quit):100
Enter a number (0 to quit):1001
Enter a number (0 to quit):100
Enter a number (0 to quit):100
Enter a number (0 to quit):0
10 1 1 1 1 20 20 20 20 1001 

As you can see from this code, there is no need to resize the target container in advance when using the insert iterator. You can also use STD:: back_ The inserter() function creates a back_insert_iteritor. For example, in the previous example, delete the line that defines the inserter variable, and then copy_ Rewrite the if () call to the following code. The result is exactly the same as the previous implementation:

copy_if(cbegin(vectorOne), cend(vectorOne),	back_inserter(vectorTwo), [](int i){ return i != 100; });

Using the constructor template parameter derivation of C++17, it can be rewritten into the following form:

copy_if(cbegin(vectorOne), cend(vectorOne),
	back_insert_iterator(vectorTwo), [](int i) { return i != 100; });

front_insert_iterator and insert_iterator works in a similar way, except for insert_ The iterator also receives the initial iterator position as a parameter in the constructor and passes this position into the first insert(position, element) call. The subsequent iterator location prompt is generated by the return value of each insert() call. Using insert_ One of the great benefits of iterator is that it can use the association container as a target for modifying class algorithms. Chapter 18 explains the problem of associating containers, that is, it is not allowed to modify the elements of iterations. Through insert_iterator can insert elements. The correlation container actually supports an insert that takes the location of the receive iterator as a parameter and uses this location as a "hint", but this location can be ignored. Using insert on an associated container_ When iterator, you can pass in the container's begin() or end() and iterator as hints. insert_ After each call to insert(), the iterator modifies the undergeneration prompt transmitted to insert() to make it the position just after the element is inserted. The following is a modified version of the previous example. In this version, the target container is set instead of vector:

vector<int> vectorOne;set<int> setOne;populateContainer(vectorOne);insert_iterator<set<int>> inserter(setOne, begin(setOne));copy_if(cbegin(vectorOne), cend(vectorOne), inserter,	[](int i){ return i != 100; });copy(cbegin(setOne), cend(setOne), ostream_iterator<int>(cout, " "));

And back_ insert_ Similar to the iterator example, you can use the std::inserter() utility function to create an insert_iterator:

copy_if(cbegin(vectorOne), cend(vectorOne),inserter(setOne, begin(setOne)),[](int i){ return i != 100; });

You can also use the constructor template parameter derivation of C++17

copy_if(cbegin(vectorOne), cend(vectorOne),insert_iterator(setOne, begin(setOne)),[](int i) { return i != 100; });

Move iterator

Chapter 9 discusses mobile semantics. Assuming that the source object will be destroyed after assignment construction or copy construction, this semantics can avoid unnecessary copying. And iteration adapter STD:: move_ The dereference operator of the iterator automatically converts the value to an rvalue reference, that is, the value can be moved to a new destination without replication overhead. Before using mobile semantics, you need to ensure that objects support mobile semantics. The following MoveableClass supports mobile semantics. See Chapter 9 for more details.

class MoveableClass{	public:    	MoveableClass()        {            cout<<"Default constructor"<<endl;        }    	MoveableClass(const MoveableClass& src)        {            cout<<"Copy constructor"<<endl;        }    	Moveableclass(MoveableClass&& src) noexcept        {            cout<<"Move contructor"<<endl;        }    	MoveableClass& operator=(const MoveableClass& rhs)        {            cout<<"Copy assigment operator"<<endl;            return *this;        }    	MoveableClass& operator=(MoveableClass&& rhs) noexcept        {            cout<<"Move assignment operator "<<endl;        	return *this;        }};

The constructor and assignment operator here don't do anything, just print out a message to explain the content of the call. Now with this class, you can define a vector and save some MoveableClass instances, as shown below;

int main(){    vector<MoveableClass> vecSource;    MoveableClass mc;    vecSource.push_back(mc);    vecSource.push_back(mc);    return 0;}

The output is as follows

xz@xiaqiu:~/study/test/test$ ./testDefault constructor [1]Copy constructor	[2]Copy constructor	[3]Move contructor		[4]xz@xiaqiu:~/study/test/test$

The second line of this code creates a MoveableClass instance through the default constructor ([1]). First push_ The back () call triggers the copy constructor to copy mc to this vector ([2]). After this operation, the vector has an element space, that is, the first copy of mc. Note that the above discussion is based on the initial size and space growth strategy of vector implemented by Microsoft Visual C++ 2017. The C + + standard does not specify the initial capacity and space growth strategy of the vector, so the output varies with the compiler. Second push_ The back () call triggers the vector to resize itself and allocate space for the second element. This resizing operation will call the move constructor to move each element in the old vector to the new resized vector ([4]). After that, trigger the copy constructor to copy mc into this vector again ([3]). The move and copy order is not defined, so the order of [3] and [4] can be exchanged.

You can create a new vector named vecOne, which contains all the elements in vecSource. The code is as follows:

int main()
{
    vector<MoveableClass> vecSource;
    MoveableClass mc;
    vecSource.push_back(mc);
    vecSource.push_back(mc);
    std::cout<<"------------------------------"<<std::endl;
    vector<MoveableClass> vecOne(cbegin(vecSource),cend(vecSource));
    return 0;
}

If you do not use move_iterator, this code will trigger the copy constructor twice and trigger once for each element in vecSource:

xz@xiaqiu:~/study/test/test$ ./testDefault constructorCopy constructorCopy constructorMove contructor------------------------------Copy constructorCopy constructorxz@xiaqiu:~/study/test/test$ 

Through STD:: make_ move_ The iterator() function creates a move_iterator, which calls the move constructor of MoveableClass instead of the copy constructor:

int main(){    vector<MoveableClass> vecSource;    MoveableClass mc;    vecSource.push_back(mc);    vecSource.push_back(mc);    std::cout<<"------------------------------"<<std::endl;    vector<MoveableClass> vecTwo(make_move_iterator(begin(vecSource)),                                 make_move_iterator(end(vecSource)));    return 0;}

Output

xz@xiaqiu:~/study/test/test$ ./testDefault constructorCopy constructorCopy constructorMove contructor------------------------------Move contructorMove contructorxz@xiaqiu:~/study/test/test$ 

You can also use the constructor template parameter derivation of C++17;

vector<MoveableClass> vecTwo(move_iterator(begin(vecSource)),							 move_iterator(end(vecSource)));

Extended standard library

The standard library contains many useful containers, algorithms, and iterators that can be used in applications. However, no library can contain all the tools that all potential customers may need. Therefore, the library should be extensible; Allow customers to adapt and add functions based on basic functions, so as to obtain the accurate functions required by customers. The standard library is essentially extensible because its infrastructure separates data from algorithms that manipulate data. By providing an iterator that conforms to the standard library standard, you can write a container that can be used for the standard library algorithm. Similarly, you can write algorithms that manipulate standard container iterators. Note that you cannot put your own containers and algorithms in the std namespace.

Reasons for extending the standard library

When preparing to write algorithms or containers in C + +, you can follow or not follow the conventions of the standard library. For simple containers and algorithms, it may not be worth the extra effort to follow the standard library specification. However, for important code that you intend to reuse, these efforts are worth it. First, the code is easier to understand by other C + + programmers because it follows well-built interface specifications. Second, you can use your own containers and algorithms with other parts of the standard library (algorithms or containers) without providing special modifications or adapters. Finally, you can force compliance with the strict specifications required to develop solid code.

Write standard library algorithm

Chapter 18 describes a set of useful algorithms in the standard library, but sometimes you need to use new algorithms in your own programs. In this case, it is not difficult to write an algorithm that can manipulate the standard library iterator like the standard algorithm.

  1. find_all()

Suppose you need to find all the elements that satisfy a predicate within the specified range. find() and find_if() is the best candidate algorithm, but these algorithms return iterators that reference only one element. You can use copy_if() finds all elements that satisfy the predicate, but populates the output with a copy of the found element. If you want to avoid copying, use copy_if() and back insert_ Iterator (in vector < reference_wrapper >), but this does not give the location of the found element. In fact, it is impossible to use any standard algorithm to find iterators that satisfy all elements of predicates, but you can write your own version that can provide this function, called find_all().

The first task is to define the function prototype. Copy can be followed_ The model used by if (). This should be a templated function with three type parameters: input iterator type, output iterator type, and predicate type. The parameters of this function are the head and tail iterators of the input sequence, the head iterators of the output sequence, and the predicate object. And copy_ Like if (), the algorithm returns an iterator to the output sequence, pointing to the element after the last element stored in the output sequence. The following is the algorithm prototype:

template <typename InputIterator,typename OutputIterator,		  typename Predicate>OutputIterator find_all(InputIterator first,InputIterator last,                        OutputIterator dest,Predicate pred);

Another alternative is to ignore the output iterator and return an iterator to the input sequence to traverse all matching elements in the input sequence. However, this scheme requires writing a custom iterator class, as discussed later. The next task is to write the implementation of the algorithm. find_ The all () algorithm traverses all elements in the input sequence, calls predicates for each element, and stores the iterator of the matching element in the output sequence. The following is the implementation of the algorithm

template <typename InputIterator, typename OutputIterator, 
		  typename Predicate>
OutputIterator find_all(InputIterator first, InputIterator last,
						OutputIterator dest, Predicate pred)
{
    while (first != last) 
    {
        if (pred(*first)) 
       	{
            *dest = first;
            ++dest;
    	}
    	++first;
    }
    return dest;
}

And copy_ Similar to if (), the algorithm only covers the existing elements in the output sequence, so ensure that the output sequence is large enough to store the results, or use an iterative adapter, such as back in the following code_ insert_ iterator. After finding all the matching elements, the code calculates the elements found

int main()
{
    vector<int> vec{3, 4, 5, 4, 5, 6, 5, 8};
    vector<vector<int>::iterator> matches;
    find_all(begin(vec),end(vec),back_inserter(matches),
             [](int i){ return i == 5;});
    cout<<"Found "<<matches.size()<<" matching elements:"<<endl;
    for(const auto& it : matches)
    {
        cout<<*it<<" at position "<<(it - cbegin(vec))<<endl;
    }
    return 0;
}

output

xz@xiaqiu:~/study/test/test$ ./testFound 3 matching elements:5 at position 25 at position 45 at position 6xz@xiaqiu:~/study/test/test$ 

2.iterator_traits

The implementation of some algorithms requires additional information of iterators. For example, in order to save temporary values, the algorithm may need to know the type of element referenced by the iterator, and may also need to know whether the iterator is bidirectional or random. C + + provides a named iterator_traits class template to find this information. Instantiate the iterator with the iterator type to use_ Traits class template, and then you can access the following five type aliases: value_type,difference_type,iterator_category, pointer, and reference. For example, the following template function declares a temporary variable whose type is the type referenced by a friend of IteratorType. Note that in the iterator_ The traits line is preceded by the typename keyword. Typename must be explicitly specified when accessing a type based on one or more template parameters. In this example, the template parameter IteratorType is used to access value_type;

#include <iterator>template <typename IteratorType>void iteratorTraitsTest(IteratorType it){    typename std::iterator_traits<IteratorType>::value_type temp;    temp = *it;    cout<<temp<<endl;}

You can test this function with the following code:

int main(){    vector<int> v{ 5 };    iteratorTraitsTest(cbegin(v));    return 0;}

output

xz@xiaqiu:~/study/test/test$ ./test
5
xz@xiaqiu:~/study/test/test$ 

In this code, the temp variable in the iteratotraitstest() function is of type int. The output is 5. In this example, the above code can be simplified through the auto keyword, but this does not explain how to use the iterator_ Trails class template.

Authoring a standard library container

The C + + standard contains a list of requirements that a container should meet as a standard library container. In addition, if you want a container to be sequential (for example, vector), ordered associative (for example, map), or unordered associative (for example, unordered_map), the container must meet additional requirements. Our suggestion for writing a new container is: first, write a basic container that follows the rules of the general standard library, such as writing a class template, and don't care too much about the details of following the standard library. After the development and basic implementation, iterators and methods can be added to make the container work with the standard library framework. This chapter develops a hash in this way_ map.

be careful:

It is recommended to use the unordered Association container (also known as hash table) of standard C + + instead of implementing one yourself. These unordered associative containers explained in Chapter 17 include unordered_map,unordered_multimap,unordered_set and unordered_multiset. Hash in this chapter_ Map is used to demonstrate how to write a standard library container.

  1. Basic hash_map

C++11 began to support hash tables, as discussed in Chapter 17. However, previous versions of C + + did not support hash tables. Unlike map and set in the standard library, which provide insertion, lookup and deletion operations with logarithmic time complexity, hash table provides insertion, lookup and operation with constant time complexity in general and corresponding operation with linear time complexity in the worst case. Instead of saving elements in an orderly manner, a hash table hashes (or maps) each element into a hash bucket. As long as the number of saved elements is not significantly more than the number of buckets, and the hash function can evenly distribute the elements in all buckets, the insertion, deletion and lookup operations can be performed in constant time.

be careful:

This section assumes that you are familiar with the hash data structure. If you are not familiar with it, please refer to Chapter 17, which contains a discussion of hash tables, and any standard data structure literature listed in Appendix B.

This section implements a simple but comprehensive hash_map. Like map, hash_map stores key value pairs. In fact, hash_ The operations provided by map are almost the same as those provided by map, except for different performance. This hash_ The map implementation uses chained hashing (also known as open hashing), but does not provide the advanced function of re hashing. Section 17.5 "unordered associated container / hash table" explains the concept of chained hash.

hash function

Write hash_ The first decision a map needs to make is how to handle hash functions. There is a saying: good abstraction should allow simple situations to be handled easily, but also deal with complex situations. Good hash_ The map interface allows customers to specify their own hash functions and the number of hash buckets to customize the hash behavior to meet specific workloads. In addition, customers who do not need to customize or do not have the ability to write a good hash function and select the number of hash buckets should be able to ignore these when using the container. One solution is to allow customers to hash_ The map constructor provides the number of hash functions and hash buckets, as well as the default value. In this implementation, the hash function is a simple function object that contains only one function call operator. The function object is templated on the key type to be hashed to support templated hash_map container. Template exceptions can be used to write custom hash functions for certain types. The basic function objects are as follows:

template <typename T>class hash{	public:		size_t operator()(const T& key) const;};

Note, hash_ All the contents of the map implementation are placed in the ProCpp namespace so that the name does not conflict with the existing name. The implementation of hash function call operator is complex because it must be applicable to any type of key. The following implementation takes the key as a series of bytes and calculates the hash value of an integer;

//Calculate a hash by treating the key as a sequence//of bytes and summing the ASCII values of the bytestemplate <typename T>size_t hash<T>::operator()(const T& key) const{    const size_t bytes = sizeof(key);    size_t sum = 0;    for(size_t i = 0; i < bytes; ++i)    {        unsigned char b = *(reinterpret_cast<const unsigned char*>(&key) + i);        sum+= b;    }    return sum;}

Unfortunately, when the above hash method is applied to a string, this function calculates the hash of the entire string object rather than the hash of the actual text. The actual text may be on the heap, and the string object contains only the length and a pointer to the text on the heap. Pointers are different, even if they point to the same text. The result is that two string objects with the same text will generate different hash values. Therefore, it is best to provide a special version of the hash template specifically for strings and a generic template for any class that contains dynamically allocated memory. Chapter 12 discusses template specialization in more depth.

//A hash specialization for stringstemplate<>class hash<std::string>{    public:    	size_t operator()(const std::string& key) const;};//Calculate a hash by summing the ASCII values of all characterssize_t hash<std::string>::operator()(const std::string& key) const{    size_t sum = 0;    for(auto c : key)    {        sum += static_cast<unsigned char>(c);    }    return sum;}

If you want to use other pointer types or objects as keys, you must write custom hash specialization for these types.

warning

The hash function in this section is the basic hash_ An example of a map implementation. These hash functions do not guarantee uniform hashing of all keys. If you need to write a mathematically more rigorous hash function, or you don't know what "uniform hash" is, please refer to the algorithm reference listed in Appendix B.

hash_map interface

Hash_map supports three basic operations: insert, delete and find. Hash_map is also exchangeable. Of course, it also provides constructors. Copy and move constructors are explicitly set to default, and copy and move assignment operators are provided. The following is the common part of the hash_map class template:

template <typename Key, typename T,
          typename KeyEqual = std::equal_to<>,
          typename Hash = hash<Key>>
class hash_map
{
public:
    using key_type = Key;
    using mapped_type = T;
    using value_type = std::pair<const Key, T>;
    virtual ~hash_map() = default; //virtual destructor
    //Throws invalid_argument if the number of buckets is illegal
    explicit hash_map(const KeyEqual &equal = KeyEqual(),
                      size_t numBuckets = 101,
                      const Hash &hash = Hash());
    //Copy constructor
    hash_map(const hash_map<Key, T, KeyEqual, Hash> &src) = default;
    //Move constructor
    hash_map(hash_map<Key, T, KeyEqual, Hash> &&src) noexcept = default;

    //Copy assignment operator
    hash_map<Key, T, KeyEqual, Hash> &operator=(
        const hash_map<Key, T, KeyEqual, Hash> &rhs);
    //Move  assignment operator
    hash_map<Key, T, KeyEqual, Hash> &operator=(
        hash_map<Key, T, KeyEqual, Hash> &&rhs) noexcept;
    //Inserts the key/value pair x
    void insert(const value_type &x);

    //Removes the element with key k. if it exists
    void erase(const key_type &k);

    //Removes all elements
    void clear() noexcept;

    // Find returns a pointer to the element with key k.
    // Returns nullptr if no element with that key exists.
    value_type *find(const key_type &k);
    const value_type *find(const key_type &k) const;

    // operator[] finds the element with key k, or inserts an
    // element with that key if none exists yet. Returns a reference to
    // the value corresponding to that key.
    T &operator[](const key_type &k);

    // Swaps two hash maps
    void swap(hash_map<Key, T, KeyEqual, Hash> &other) noexcept;
private:
    //Implementation details not show yet
    using ListType = std::list<value_type>;

    // Returns a pair containing an iterator to the found element with
    // a given key, and the index of that element's bucket.
    std::pair<typename ListType::iterator, size_t> findElement(const key_type& k);

    std::vector<ListType>mBuckets;
    size_t mSize = 0;
    KeyEqual mEqual;
    Hash mHash;
};

It can be seen that, like the map of the standard library, the types of keys and values are template parameters. The actual elements saved by the hash_map in the container are pair < const key, t >. The insert(), erase(), find() and operator [] methods are very simple. However, there are several points in this interface that need to be explained in detail.

Template parameter KeyEqual

Like map, set and other standard containers, hash_map allows customers to specify comparison types in template parameters and pass specific comparison objects of corresponding types in the constructor. Unlike map and set, hash_map does not sort elements according to keys, but still needs to compare whether keys are equal. Therefore, hash_map does not use less as the default ratio The only purpose of comparing objects is to detect whether duplicate keys are inserted into the container.

Template parameters

It should be possible to modify the hash function to better adapt to the element types to be stored in the hash_map. Therefore, the hash_map template receives four template parameters: key type, value type, comparison type and hash type.

Type alias

The hash_map class template defines three type aliases:

using key_type = Key;using mapped_type = T;using value_type = std::pair<const Key,T>;

value_type is used to refer to complex pair < const key, t > types. As you will see later, these type aliases are also the requirements of the C + + standard for the standard library.

realization

After determining the hash_map interface, you need to select the implementation model. The basic hash table data structure is usually composed of a fixed number of hash buckets, and each hash bucket can store one or more elements. The hash bucket should be available through bucket_id (the result of hashing the key) Access in constant time. Therefore, vector is the most suitable container for storing hash buckets. Each bucket must store a list of elements, so the standard library list can be used as the bucket type. The final structure is: pair < const key, t > linked list vector of elements. The following are the private members in the hash_map class:

private:	using ListType = std::list<value_type>;	std::vector<ListType>mBuckets;	size_t mSize = 0;	KeyEqual mEqual;	Hash mHash;

If there is no type alias for value_type and ListType, the line declaring mBuckets can be written as:

std::vector<std::list<std::pair<const Key,T>>>mBuckets;

The mComp and mHash members save the comparison object and hash object respectively, and mSize saves the current number of elements in the container.

Constructor

Constructor initializes all fields and specifies the correct size for the hash bucket. Unfortunately, the template syntax is complex. If you are confused about the syntax, see Chapter 12 for details on writing class templates.

//Contruct mBuckets with the correct number of bucketstemplate<typename Key, typename T, typename KeyEqual, typename Hash>hash_map<Key, T, KeyEqual, Hash>::hash_map(    const KeyEqual &equal, size_t numBuckets,    const Hash &hash)    : mBuckets(numBuckets), mEqual(equal), mHash(hash){    if (numBuckets == 0)    {        throw std::invalid_argument("Number of buckets must be positive ");    }}

This implementation requires at least one bucket, so the constructor enforces this restriction.

Find element

The three main operations of hash_map (find, insert and delete) require the code to find elements according to the given key. Therefore, it is best to implement a private helper method that can perform this task. findElement() First, the hash of the key is calculated through the hash object, and the calculated value is modeled to limit the calculated hash value to the number of hash buckets. Then, find the element matching the given key in this bucket. The element is saved in the key value pair, so the actual comparison operation must be for the first field of the element. The comparison operation is performed through the comparison function object specified by the constructor.

template<typename Key, typename T, typename KeyEqual, typename Hash>std::pair<typename hash_map<Key, T, KeyEqual, Hash>::ListType::iterator, size_t>hash_map<Key, T, KeyEqual, Hash>::findElement(const key_type &k){    //Hash the key to get the bucket    size_t bucket = mHash(k) % mBuckets.size();    //Search for the key int the  bucket    auto iter = find_if(begin(mBuckets[bucket]),                                       end(mBuckets[bucket]),[this, &k](const auto & element){        return mEqual(element.first, k);    });    //Return a pair of the iterator and the bucket index    return std::make_pair(iter, bucket);}

Note that findElement() returns pair, which contains iterator and bucket index. Bucket index is the index of the bucket to which the given key is mapped, regardless of whether the given key is in the container. The returned iterator points to the element in bucket list, and list indicates the bucket to which the key is mapped. If an element is found, the human iterator points to the corresponding element, and otherwise, it is the tail iterator of list.

The syntax in the function header of this method is difficult to understand, especially the part using the typename keyword. When using types related to template parameters, the typename keyword must be used. Specifically, ListType::iterator type is list < pair < const Key, T > >:: iterator type, which is related to template parameters Key and T. The find() method can be implemented as fndElement() Simple packaging:

template<typename Key, typename T, typename KeyEqual, typename Hash>typename hash_map<Key, T, KeyEqual, Hash>::value_type *hash_map<Key, T, KeyEqual, Hash>::find(const key_type &k){    //Use the findElement() helper ,and c++17 structured binds    auto[it, bucket] = findElement(k);    if (it == end(mBuckets[bucket]))    {        //Element not found -- return nullptr        return nullptr;    }    //Elment found -- return a pointer to it    return &(*it);}

The const version of find() uses const_cast to pass the request to the non const version to avoid code duplication:

template<typename Key, typename T, typename KeyEqual, typename Hash>const typename hash_map<Key, T, KeyEqual, Hash>::value_type *hash_map<Key, T, KeyEqual, Hash>::find(const key_type &k) const{    return const_cast<hash_map<Key, T, KeyEqual, Hash>*>(this)->find(k);}

The operator [] implementation uses the findElement() method. If no element is found, insert the element:

template <typename Key, typename T, typename KeyEqual, typename Hash>T &hash_map<Key, T, KeyEqual, Hash>::operator[](const key_type &k){    //Try to find the element,if it doesn't exist.add a new element    auto[it, bucket] = findElement(k);    if (it == end(mBuckets[bucket]))    {        mSize++;        mBuckets[bucket].push_back(std::make_pair(k, T()));        return mBuckets[bucket].back().second;    }    else    {        return it->second;    }}

Insert element

insert() must first check whether the element with the key is already in the hash_map. If not, add the element to the list of the corresponding bucket. Note that findElement() returns the bucket to which the key is hashed by reference, even if the element corresponding to that key is not found:

template<typename Key, typename T, typename KeyEqual, typename Hash>void hash_map<Key, T, KeyEqual, Hash>::insert(const value_type &x){    //Try to find thle element    auto[it, bucket] = findElement(x.first);    if (it != end(mBuckets[bucket]))    {        //The element already exists        return;    }    else    {        //We didn't find the element,so insert a new one        mSize++;        mBuckets[bucket].push_back(x);    }}

Note that the implementation of insert() returns void, so the caller does not know whether the element has been inserted or the corresponding element has been in the hash_map. When the hash_map implementation and iterator are described later in this chapter, we will analyze how to overcome this disadvantage.

Delete element

erase() and insert() use the same pattern: first call findElement() to search for elements. If elements exist, delete them from the list of corresponding buckets. Otherwise, do nothing.

template<typename Key, typename T, typename KeyEqual, typename Hash>
void hash_map<Key, T, KeyEqual, Hash>::erase(const key_type &k)
{
    //First,try to find the element
    auto[it, bucket] = findElement(k);
    if (it != end(mBuckets[bucket]))
    {
        //The element exists -- erase it
        mBuckets[bucket].erase(it);
        mSize--;
    }
}

exchange

The swap() method uses std::swap() to exchange all data members:

void hash_map<Key, T, KeyEqual, Hash>::swap(
    hash_map<Key, T, KeyEqual, Hash> &other) noexcept
{
    using std::swap;
    swap(mBuckets, other.mBuckets);
    swap(mSize, other.mSize);
    swap(mEqual, other.mEqual);
    swap(mHash, other.mHash);
}

C + + also provides the following independent version of swap(), which is only forwarded to the swap() method:

template <typename Key, typename T, typename KeyEqual, typename Hash>void swap(hash_map<Key, T, KeyEqual, Hash> &first,                                 hash_map<Key, T, KeyEqual, Hash> &second) noexcept{    first.swap(second);}

Assignment Operators

The following is the implementation of copy and move assignment operators. See the "copy and exchange" idiomatic syntax discussed in Chapter 9.

template <typename Key, typename T, typename KeyEqual, typename Hash>hash_map<Key, T, KeyEqual, Hash>&   hash_map<Key, T, KeyEqual, Hash>::operator=(    const hash_map<Key, T, KeyEqual, Hash> &rhs){    //check for self-assignment    if (this == &rhs)    {        return *this;    }    //Copy-and-swap idiom    auto copy = rhs; //Do all the work in a temporary instace    swap(copy); //Commit the work with only non-throwing operations    return *this;}template <typename Key, typename T, typename KeyEqual, typename Hash>hash_map<Key, T, KeyEqual, Hash>&hash_map<Key, T, KeyEqual, Hash>::operator=(hash_map<Key, T, KeyEqual, Hash> &&rhs) noexcept{    swap(rhs);    return *this;}

Use basic hash_map

The following is a simple test program that demonstrates how to use the basic hash_map class template:

hash_map<int,int>myHash;
myHash.insert(make_pair(4,40));
myHash.insert(make_pair(6,60));

//x will have type hash_map<int,int>::value_type*
auto x2 = myHash.find(4);
if(x2 != nullptr)
{
	cout<<"4 maps to "<<x2->second<<endl;
}
else
{
	cout<<"cannot find 4 in map"<<endl;
}
myHash[4] = 35;
myHash[4] = 60;
auto x3 = myHash.find(4);
if(x3 != nullptr)
{
	cout<<"4 maps to"<<x3->second<<endl;
}
else
{
	cout<<"cannot find 4 in map"<<endl;
}
//Test std::swap()
hash_map<int,int> other(std::equal_to<>(),11);
swap(other,myHash);

//Test copy construction and copy assignment
hash_map<int,int> myHash2(other);
hash_map<int,int> myHash3;
myHash3 = myHash2;

//Test move construction and move assignment
hash_map<int,int>myHash4 (std::move(myHash3));
hash_map<int,int>myHash5;
myHash5 = std::move(myHash4);

code

#include <iostream>
#include <iterator>
#include <vector>
#include <numeric>
#include <list>
#include <utility>
#include <string>
#include <algorithm>
using namespace std;
namespace ProCpp
{
template <typename T>
class hash
{
public:
    size_t operator()(const T &key) const;
};


//Calculate a hash by treating the key as a sequence
//of bytes and summing the ASCII values of the bytes
template <typename T>
size_t hash<T>::operator()(const T &key) const
{
    const size_t bytes = sizeof(key);
    size_t sum = 0;
    for (size_t i = 0; i < bytes; ++i)
    {
        unsigned char b = *(reinterpret_cast<const unsigned char *>(&key) + i);
        sum += b;
    }
    return sum;
}


//A hash specialization for strings
template<>
class hash<std::string>
{
public:
    size_t operator()(const std::string &key) const;
};
//Calculate a hash by summing the ASCII values of all characters
size_t hash<std::string>::operator()(const std::string &key) const
{
    size_t sum = 0;
    for (auto c : key)
    {
        sum += static_cast<unsigned char>(c);
    }
    return sum;
}

template <typename Key, typename T,
          typename KeyEqual = std::equal_to<>,
          typename Hash = hash<Key>>
class hash_map
{
public:
    using key_type = Key;
    using mapped_type = T;
    using value_type = std::pair<const Key, T>;
    virtual ~hash_map() = default; //virtual destructor
    //Throws invalid_argument if the number of buckets is illegal
    explicit hash_map(const KeyEqual &equal = KeyEqual(),
                      size_t numBuckets = 101,
                      const Hash &hash = Hash());
    //Copy constructor
    hash_map(const hash_map<Key, T, KeyEqual, Hash> &src) = default;
    //Move constructor
    hash_map(hash_map<Key, T, KeyEqual, Hash> &&src) noexcept = default;

    //Copy assignment operator
    hash_map<Key, T, KeyEqual, Hash> &operator=(
        const hash_map<Key, T, KeyEqual, Hash> &rhs);
    //Move  assignment operator
    hash_map<Key, T, KeyEqual, Hash> &operator=(
        hash_map<Key, T, KeyEqual, Hash> &&rhs) noexcept;
    //Inserts the key/value pair x
    void insert(const value_type &x);

    //Removes the element with key k. if it exists
    void erase(const key_type &k);

    //Removes all elements
    void clear() noexcept;

    // Find returns a pointer to the element with key k.
    // Returns nullptr if no element with that key exists.
    value_type *find(const key_type &k);
    const value_type *find(const key_type &k) const;

    // operator[] finds the element with key k, or inserts an
    // element with that key if none exists yet. Returns a reference to
    // the value corresponding to that key.
    T &operator[](const key_type &k);

    // Swaps two hash maps
    void swap(hash_map<Key, T, KeyEqual, Hash> &other) noexcept;
private:
    //Implementation details not show yet
    using ListType = std::list<value_type>;

    // Returns a pair containing an iterator to the found element with
    // a given key, and the index of that element's bucket.
    std::pair<typename ListType::iterator, size_t> findElement(const key_type &k);

    std::vector<ListType>mBuckets;
    size_t mSize = 0;
    KeyEqual mEqual;
    Hash mHash;
};

//Contruct mBuckets with the correct number of buckets
template<typename Key, typename T, typename KeyEqual, typename Hash>
hash_map<Key, T, KeyEqual, Hash>::hash_map(
    const KeyEqual &equal, size_t numBuckets,
    const Hash &hash)
    : mBuckets(numBuckets), mEqual(equal), mHash(hash)
{
    if (numBuckets == 0)
    {
        throw std::invalid_argument("Number of buckets must be positive ");
    }
}

template<typename Key, typename T, typename KeyEqual, typename Hash>
std::pair<typename hash_map<Key, T, KeyEqual, Hash>::ListType::iterator, size_t>
hash_map<Key, T, KeyEqual, Hash>::findElement(const key_type &k)
{
    //Hash the key to get the bucket
    size_t bucket = mHash(k) % mBuckets.size();
    //Search for the key int the  bucket
    auto iter = find_if(begin(mBuckets[bucket]),                                       end(mBuckets[bucket]),
                        [this, &k](const auto & element)
    {
        return mEqual(element.first, k);
    });
    //Return a pair of the iterator and the bucket index
    return std::make_pair(iter, bucket);
}

template<typename Key, typename T, typename KeyEqual, typename Hash>
typename hash_map<Key, T, KeyEqual, Hash>::value_type *
hash_map<Key, T, KeyEqual, Hash>::find(const key_type &k)
{
    //Use the findElement() helper ,and c++17 structured binds
    auto[it, bucket] = findElement(k);
    if (it == end(mBuckets[bucket]))
    {
        //Element not found -- return nullptr
        return nullptr;
    }
    //Elment found -- return a pointer to it
    return &(*it);
}

template<typename Key, typename T, typename KeyEqual, typename Hash>
const typename hash_map<Key, T, KeyEqual, Hash>::value_type *
hash_map<Key, T, KeyEqual, Hash>::find(const key_type &k) const
{
    return const_cast<hash_map<Key, T, KeyEqual, Hash>*>(this)->find(k);
}


template <typename Key, typename T, typename KeyEqual, typename Hash>
T &hash_map<Key, T, KeyEqual, Hash>::operator[](const key_type &k)
{
    //Try to find the element,if it doesn't exist.add a new element
    auto[it, bucket] = findElement(k);
    if (it == end(mBuckets[bucket]))
    {
        mSize++;
        mBuckets[bucket].push_back(std::make_pair(k, T()));
        return mBuckets[bucket].back().second;
    }
    else
    {
        return it->second;
    }
}

template<typename Key, typename T, typename KeyEqual, typename Hash>
void hash_map<Key, T, KeyEqual, Hash>::insert(const value_type &x)
{
    //Try to find thle element
    auto[it, bucket] = findElement(x.first);
    if (it != end(mBuckets[bucket]))
    {
        //The element already exists
        return;
    }
    else
    {
        //We didn't find the element,so insert a new one
        mSize++;
        mBuckets[bucket].push_back(x);
    }
}

template<typename Key, typename T, typename KeyEqual, typename Hash>
void hash_map<Key, T, KeyEqual, Hash>::erase(const key_type &k)
{
    //First,try to find the element
    auto[it, bucket] = findElement(k);
    if (it != end(mBuckets[bucket]))
    {
        //The element exists -- erase it
        mBuckets[bucket].erase(it);
        mSize--;
    }
}

template <typename Key, typename T, typename KeyEqual, typename Hash>
void hash_map<Key, T, KeyEqual, Hash>::swap(
    hash_map<Key, T, KeyEqual, Hash> &other) noexcept
{
    using std::swap;
    swap(mBuckets, other.mBuckets);
    swap(mSize, other.mSize);
    swap(mEqual, other.mEqual);
    swap(mHash, other.mHash);
}


template <typename Key, typename T, typename KeyEqual, typename Hash>
void swap(hash_map<Key, T, KeyEqual, Hash> &first,                                 hash_map<Key, T, KeyEqual, Hash> &second) noexcept
{
    first.swap(second);
}


template <typename Key, typename T, typename KeyEqual, typename Hash>
hash_map<Key, T, KeyEqual, Hash> &
hash_map<Key, T, KeyEqual, Hash>::operator=(
    const hash_map<Key, T, KeyEqual, Hash> &rhs)
{
    //check for self-assignment
    if (this == &rhs)
    {
        return *this;
    }
    //Copy-and-swap idiom
    auto copy = rhs; //Do all the work in a temporary instace
    swap(copy); //Commit the work with only non-throwing operations
    return *this;
}

template <typename Key, typename T, typename KeyEqual, typename Hash>
hash_map<Key, T, KeyEqual, Hash> &
hash_map<Key, T, KeyEqual, Hash>::operator=
(hash_map<Key, T, KeyEqual, Hash> &&rhs) noexcept
{
    swap(rhs);
    return *this;
}
}

int main()
{
    ProCpp::hash_map<int, int>myHash;
    myHash.insert(make_pair(4, 40));
    myHash.insert(make_pair(6, 60));

//x will have type hash_map<int,int>::value_type*
    auto x2 = myHash.find(4);
    if (x2 != nullptr)
    {
        cout << "4 maps to " << x2->second << endl;
    }
    else
    {
        cout << "cannot find 4 in map" << endl;
    }
    myHash[4] = 35;
    myHash[4] = 60;
    auto x3 = myHash.find(4);
    if (x3 != nullptr)
    {
        cout << "4 maps to " << x3->second << endl;
    }
    else
    {
        cout << "cannot find 4 in map" << endl;
    }
//Test std::swap()
    ProCpp::hash_map<int, int> other(std::equal_to<>(), 11);
    swap(other, myHash);

//Test copy construction and copy assignment
    ProCpp::hash_map<int, int> myHash2(other);
    ProCpp::hash_map<int, int> myHash3;
    myHash3 = myHash2;

//Test move construction and move assignment
    ProCpp::hash_map<int, int>myHash4 (std::move(myHash3));
    ProCpp::hash_map<int, int>myHash5;
    myHash5 = std::move(myHash4);
    return 0;
}

output

xz@xiaqiu:~/study/test/test$ ./test4 maps to 404 maps to 60xz@xiaqiu:~/study/test/test$ 
  1. Implement hash_map as a standard library container

The basic hash_map discussed above follows the idea of the standard library, but does not follow the detailed specifications. In most cases, the above implementation is good enough. However, if you want to use the standard library algorithm on the hash_map, you need to do more work. The C + + standard specifies the data structure as the method and type alias that the standard library container must provide. The required type name

The C + + standard specifies that each standard library container should have the public type alias shown in table 21-1.

Type nameexplain
value_typeThe type of element saved in the container
referenceA reference to the element type saved in the container
const_referenceconst reference to the element type saved in the container
iteratorTraverses the types of elements in the container
const_iteratorAnother version of iterator traverses the const element in the container
size_typeIndicates the type of the number of elements in the container, usually size_ T (from)
difference_ typeRepresents the type of difference between two iterator s used for containers, usually ptrdiff t (from)

Here is the hash_map implements the class template definition of these type aliases, except for iterator and const_iterator. The way iterators are written will be explained in detail later. Note value_ Type (plus key_type and mapped_type to be discussed later) in the old version of hash_ It is already defined in the map. This implementation also adds the type alias hash_map_type, used to give hash_ Specify a short name for a specific template instance of map:

template<typename Key, typename T,typename KeyEqual = std::equal_to<>,typename Hash = hash<Key>>class hash_map{	public:		using key_type = Key;		using mapped_type = T;		using reference = value_type&;		using const_reference = const value_type&;		using size_type = size_t;		using difference_type = ptrdiff_t;		using hash_map_type = hash_map<Key, T, KeyEqual, Hash>;		//Remainder of class definition omitted for brevity};

Methods required from containers

In addition to type aliases, each container must provide the methods shown in table 21-2.

methodexplainWorst case complexity
Default constructor Construct an empty containerConstant time complexity
copy constructor Perform a deep copy of the containerLinear time complexity
move constructor Perform the move construction operationConstant time complexity
copy assignment operator Perform a deep copy of the containerLinear time complexity
Move assignment operatorPerform move assignmentConstant time complexity
DestructorDestroy the dynamically allocated memory and call the destructor on all the remaining elements in the containerLinear time complexity
iterator begin(); const_iterator begin() const;Returns an iterator that references the first element in the containerConstant time complexity
iterator end(); const_terator end() const;Returns the iterator that references the location after the last element in the containerConstant time complexity
const_iterator cbegin() const;Returns the first const iterator in the reference containerConstant time complexity
const_iterator cend() const;Returns the const iterator that references the location after the last element in the containerConstant time complexity
operator== operator!=Compares the comparison operators of two containers on an element by element basisLinear time complexity
void swap(Container&) noexceptThe contents of the container that is passed into the method as a parameter, and where it is calledConstant time complexity
size_type size() constReturns the number of elements in the containerConstant time complexity
size_type max_size() const;Returns the maximum number of elements that the container can holdConstant time complexity
bool empty() const;Specifies whether the container contains any elementsConstant time complexity

be careful:

In this hash map example, the comparison operator is ignored. Implementing these operators is a good exercise for the reader, but you must first consider two hashes_ The equality semantics of map. One possibility is that only when two hashes_ When the number of buckets in the map is exactly the same and the contents of the buckets are the same, they are equal. Similarly, you must consider a hash_map is smaller than another hash_ The meaning of map. One option is to define them as paired comparisons of elements.

The following code snippet shows the declarations of all the remaining methods, except begin(), end(), cbegin(), and cend(). These methods are discussed later.

template<typename Key,typename T,typename KeyEqual = std::equal_to<>,typename Hash = hash<Key>>class hash_map{	public:		//Type aliases omitted for brevity        //Size methods        bool empty() const;        size_type size() const;        size_type max_size() const;        //Other method omitted for brevity};

The implementation of size() and empty() is simple because of hash_ The implementation of map maintains its own size in the mSize data member. Note that size_type is a type alias defined in this class. Since it is a member of a class, such a return type must have the full name typename hash in its implementation_ map<Key,T,KeyEqual,Hash>

template<typename Key, typename T, typename KeyEqual, typename Hash>bool hash_map<Key, T, KeyEqual, Hash>::empty() const{	return mSize == 0;}template <typename Key, typename T, typename KeyEqual, typename Hash>typename hash_map<Key, T, KeyEqual, Hash>::size_type	hash_map<Key,T,KeyEqual,Hash>::size() const{	return mSize;}

max_ The implementation of size () is a little cumbersome. At first, you might think this hash_ The maximum size of the map container is the sum of the maximum sizes of all lists. In the worst case, however, all elements are hashed to the same bucket. Therefore, hash_ The maximum size that a map can declare to support should be the maximum size of a single list:

template <typename Key, typename T, typename KeyEqual, typename Hash>typename hash_map<Key, T, KeyEqual, Hash>::size_type	hash_map<Key,T,KeyEqual,Hash>::max_size() const{	return mBuckets[0].max_size();        }

Write iterator

The most important requirement for containers is to implement iterators. In order to be used in generic algorithms, each container must provide an iterator that can access the elements in the container. And iterators should generally provide overloaded operator * and operator - > operators, plus other operations that depend on specific behavior. As long as the iterator provides the basic iteration operation, there will be no problem. The first decision you need to make about iterators is to choose the type of iterator: forward access, bidirectional access, or random access iterators. Random access iterators have little meaning to the associated container, so hash_ Logically, a map iterator should be a two-way access and iterator. This means that you must provide operator + +, operator –, operator = = and operator I =. See Chapter 17 for specific requirements for different iterators. The second decision is how to sort the elements of the container. hash_ Maps are unordered, so performing ordered iterations can be a little difficult. The actual situation is that you can traverse all buckets, starting from the first bucket to the last bucket. From the customer's point of view, this order is random, but has - consistency and repeatability. The third decision is to choose the internal representation of the iterator. This implementation is usually closely related to the internal implementation of the container. The main function of iterators is to reference elements in containers. In hash_ In the map, each element is in the standard library list, so hash_ A map iterator can be a wrapper around a list iterator that references related elements. However, a two-way access iterator also has the function of allowing customers to advance from the current element to the next element or back to the previous element. In order to advance from one bucket to the next, you also need to track the current bucket and the hash referenced by the iterator_ Map object. Once the implementation is selected, a consistent representation must be determined for the tail and iterator. Recall that the tail iterator should actually be a "past the last element" tag, that is, the iterator obtained by applying the + + operator to the iterator of the last element in the container. hash_ The map iterator can hash_ The tail iterator of the last bucket list in the map is used as a hash_ Tail iterator for map. The container needs to provide const iterators and non const iterators. Non const iterators must be able to convert to const iterators. This implementation uses a derived hash_map_ The iterator defines const_hash_map_iterator.

const_hash_map_iterator class

Based on the previous decision, let's start defining const_hash_map_iterator class. The first thing to note is that each const_hash_map_iterator objects are hashes_ Iterator for an instance of the map class. To provide this one-to-one mapping, const_ hash_map_ The iterator must also be a class template and hash_ The map type is called HashMap as a template parameter.

The main problem in this class definition is how to meet the requirements of two-way access iterators. Any object that behaves like an iterator is an iterator. The custom class does not need to be a subclass of another class, and can also meet the requirements of two-way access iterators. However, if you want the iterator to be applicable to functions of generic algorithms, you must specify an iterator_traits. As explained earlier in this chapter, iterator traits is a class template that defines five type aliases for each iterator type: value_type,difference_type, iterator_category, pointer, and reference. If necessary, the iterator_ The traits class template can be partially specialized to meet the new iterator types. In addition, iterator_ The default implementation of the traits class template gets five type aliases from the iterator class itself. Therefore, you can define these type aliases directly in your iterator class. const_hash_map_iterator is a two-way access and iterator, so it will be bidirectional_iterator_tag is specified as the iterator category. The other legal iterator class is input_iterator_tag,output_iterator_tag, forward_iterator_tag and random_access_iterator_tag, for const_hash_map_iterator, the element type is typename_HashMap::value_type.

Here is the basic const_hash_map_iterator class definition

template <typename HashMap>
class const_hash_map_iterator
{
	public:
    	using value_type = typename HashMap::value_type;
    	using difference_type = ptrdiff_t;
    	using iterator_category = std::bidirectional_iterator_tag;
    	using pointer = value_type*;
    	using reference = value_type&;
    	using list_iterator_type = typename HashMap::ListType::const_iterator;
    	
    	//Bidirectional iterators must supply a default constructor
    	//Using an iterator constructed with the default constructor
    	//is undefined,so it doesn't matter how it's initialized
    	const_hash_map_iterator() = default;
    	
    	const_hash_map_iterator(size_t bucket, list_iterator_type listIt, const HashMap* hashmap);
    	//Don't need to define a copy constructor or operator= because the default behavior is what we want
    	//Don't need to destructor because the default behavior
    	//(not deleting mHashmap) is what we want
    	const value_type& operator*() const;
    	//Return type must be something to whitch -> can be applied
    	//Return a pointer to a pair<const Key,T>,to which the compiler
    	//will apply->again
    	const value_type* operator->() const;
    	const_hash_map_iterator<HashMap>& operator++();
    	const_hash_map_iterator<HashMap> operator++(int);
    
    	const_hash_map_iterator<HashMap>& operator--();
    	const_hash_map_iterator<HashMap> operator--(int);
    
    	//The following are ok as member functions because we don't
    	//support comparisions of different types to this one
    	bool operator==(const const_hash_map_iterator<HashMap>& rhs) const;
    	bool operator!=(const const_hash_map_iterator<HashMap>& rhs) const;
    protected:
    	size_t mBucketIndex = 0;
    	list_iterator_type mListIterator;
    	const HashMap* mHashmap =  nullptr;
    	
    	//Helper methods for operator++ and operator--
    	void increment();
    	void decrement();
};

If you find the definition and implementation of overloaded operators difficult to understand, please refer to Chapter 15 for details on operator overloading.

const_ hash_ map_ Method implementation of iterator

const_ hash_ map_ The iterator constructor initializes three member variables:

template<typename HashMap>const_hash_map_iterator<HashMap>::const_hash_map_iterator(size_t bucket,list_iterator_type listIt,const HashMap* hashmap)	:mBucketIndex(bucket),mListIterator(listIt),mHashmap(hashmap)    {            }

The only purpose of the default constructor is to allow clients to declare uninitialized const_hash_map_iterator variable. The iterator constructed by the default constructor can not reference any value, and any operation on the iterator can produce a defined behavior

The implementation of dereference operator is very concise, but it is also difficult to understand. Chapter 15 mentions that the operator * and operator - > operators are asymmetric; The operator * operator returns a reference to the underlying actual value, in this example, the element referenced by the iterator; The operator - > operator returns an object to which the arrow operator can be applied again. Therefore, a pointer to the element is returned. The compiler applies the - > operator to this pointer to access the fields in the element:

//Return a reference to the actual elementtemplate<typename HashMap>const typename const_hash_map_iterator<HashMap>::value_type& 	const_hash_map_iterator<HashMap>::operator*() const	{		return *mListIterator;	}//Return  a pointer to the actual element,so the compiler can//apply->to it to access the actual desired fieldtemplate<typename HashMap>const typename const_hash_map_iterator<HashMap>::value_type*    const_hash_map_iterator<HashMap>::operator->() const    {        return &(*mListIterator);    }

The implementations of the increment and decrement operators are as follows. These two implementations push the actual increment and decrement operations to the private increment() and decrement() auxiliary methods:

//Defer the details to the increment() helper;template<typename HashMap>const_hash_map_iterator<HashMap>&	const_hash_map_iterator<HashMap>::operator++(){	increment();	return *this;}//Defer the details to the increment() helpertemplate<typename HashMap>const_hash_map_iterator<HashMap>	const_hash_map_iterator<HashMap>::operator++(int){	auto oldIt = *this;    increment();    return oldIt;}//Defer the default to the decrement() helpertemplate<typename HashMap>const_hash_map_iterator<HashMap>&const_hash_map_iterator<HashMap>::operator--(){    decrement();   	return *this;}//Defer the details to the decrement() helpertemplate<typename HashMap>const_hash_map_iterator<HashMap>	const_hash_map_iterator<HashMap>::operator--(int)    {        auto oldIt = *this;        decrement();        return oldIt;    }

Incremental const_hash_map_iterator means that this iterator references the "next" element in the container. This method first increments the list iterator and then checks whether it reaches the end of the bucket. If it arrives, look for the hash_ The next nonempty ellipse in the map and set the list iterator equal to the first element in that bucket. Note that you cannot simply move to the next bucket because there may be no elements in the next bucket. If there are no more non empty buckets, set mlistirator to hash according to the Convention selected in this example_ The tail slave of the last bucket in the map. This iterator is const_ hash_ The special "end" position of the map iterator. The undergeneration is not required to be more secure than ordinary pointers, so there is no need to perform error checking for operations such as "incrementing a friend that is already a tail friend".

//Behavior is underfined if mListIterator already refers to the past-the-end
//element,or is otherwise invalid
template<typename HashMap>
void const_hash_map_iterator<HashMap>::increment()
{
	//mListIterator is an iterator into a single bucket,Increment it,
    ++mListIterator;
    //if we're at the end of the current bucket
    //find the next bucket with element
    auto& buckets = mHashmap->mBuckets;
    if(mListIterator == end(buckets(mBucketIndex)))
    {
        for(size_t i = mBucketIndex + 1; i < buckets.size();i++)
        {
            if(!buckets[i].empty())
            {
            	//We found a non-empty-bucket
                //Make mListIterator refer to the first element in it
                mListIterator = begin(buckets[i]);
                mBucketIndex = i;
                return;
            }
            //No more non-empty buckets,Set mListIterator to refer to the
            //end iterator of last list
            mBucketIndex = buckets.size() -1;
            mListIterator = end(buckets[mBucketIndex]);
        }
    }
}

Decrement is the opposite of incrementing: it causes the iterator to reference the "previous" element in the container. However, there is asymmetry between decreasing and increasing because the start position and end position are represented differently: the start position represents the first element and the end position represents the "next position" > of the last element. The algorithm for decrement is to first check whether the underlying list iterator is at the starting position in the current bucket. If it is not in this position, the decrement operation is performed directly. And otherwise, the code needs to find the first non empty bucket before the current bucket. If you find one, you must set the list iterator to reference the last element in that bucket, that is, subtract 1 from the tail iterator of that bucket. If a non empty bucket cannot be found, the decrement operation is invalid and the code can do anything (the behavior is undefined). Note that the for loop needs to use a signed integer type as the loop variable instead of an unsigned type, such as size_t. Because – i is recycled;

//Behavior is undefined if mListIterator already refers to the first
//element. or is otherwise invalid
template<typename HashMap>
void const_hash_map_iterator<HashMap>::decrement()
{
    // mListIterator is an iterator into a single bucket.
    // If it's at the beginning of the current bucket, don't decrement it.
    // Instead, try to find a non-empty bucket before the current one.
    auto& buckets = mHashmap->mBuckets;
    if(mListIterator == begin(buckets(mBucketIndex)))
    {
        for(int i = mBucketIndex - 1;i >= 0;--i)
        {
            if(!buckets[i].empty())
            {
                mListIterator = --end(buckets[i]);
                mBucketIndex = -1;
                return;
            }
        }
        // No more non-empty buckets. This is an invalid decrement.
		// Set mListIterator to refer to the end iterator of the last list.
        mBucketIndex = buckets.size() - 1;
        mListIterator = end(buckets[mBucketIndex]);
    }
    else
    {
        // We're not at the beginning of the bucket, so just move down.
		--mListIterator;
    }
}

Note that both increment () and increment () access the hash_ private member of the map class. Therefore, hash_ The map class must convert const_hash_map_iterator is declared as a friend class. After defining increment() and increment(), operator = = and operator= The definition of is relatively simple. These operators only need to compare the three data members of the object:

template<typename HashMap>
bool const_hash_map_iterator<HashMap>::operator==(const const_hash_map_iterator<HashMap>& rhs) const
{
	// All fields, including the hash_map to which the iterators refer,
	// must be equal.
	return(mHashmap == rhs.mHashmap&&
           mBucketIndex == rhs.mBucketIndex&&
           mListIterator == rhs.mListIterator);
}
template<typename HashMap>
bool const_hash_map_iterator<HashMap>::operator!=(const const_hash_map_iterator<HashMap>& rhs) const
{
    return !(*this == rhs);
}

hash_map_iterator class

hash_ map_ The iterator class is derived from const_hash_map_iterator, and there is no need to rewrite operator = =, operator! = Increment() and increment(), because the base class version is sufficient:

template<typename HashMap>
class hash_map_iterator : public const_hash_map_iterator<HashMap>
{
	public:
		using value_type = typename const_hash_map_iterator<HashMap>::value_type;
    	using difference_type = ptrdiff_t;
    	using iterator_category = std::bidirectional_iterator_tag;
    	using pointer = value_type*;
    	using reference = value_type&;
    	using list_iterator_type = typename HashMap::ListType::iterator;
    
    	hash_map_iterator() = default;
    	hash_map_iterator(size_t bucket,list_iterator_type listIt,HashMap* hashmap);
    	value_type& operator*();
    	value_type* operator->();
    
    	hash_map_iterator<HashMap>& operator++();
    	hash_map_iterator<HashMap> operator++(int);
    	hash_map_iterator<HashMap>& operator--();
    	hash_map_iterator<HashMap> operator--(int);
};

hash_ map_ Implementation of iterator method

hash_ map_ The implementation of the iterator method is quite simple. The constructor only calls the base class constructor, and const is used for operator * and operator - > respectively_ Cast returns non const type. Operator + and operator - only use increment() and increment() of the base class, but return hash_map_iterator instead of const_hash_map_iterator. The C + + name lookup rule requires the explicit use of this pointer to point to data members and methods in the base class template:

template<typename HashMap>
hash_map_iterator<HashMap>::hash_map_iterator(
    size_t bucket,list_iterator_type listIt,HashMap* hashmap)
{
    
}

//Return a reference to the actual element
template<typename HashMap>
typename hash_map_iterator<HashMap>::value_type&
    hash_map_iterator<HashMap>::operator*()   
    {
        return const_cast<value_type&>(*this->mListIterator);
    }

// Return a pointer to the actual element, so the compiler can
// apply -> to it to access the actual desired field.
template<typename HashMap>
typename hash_map_iterator<HashMap>::value_type*
    hash_map_iterator<HashMap>::operator->()
    {
        return const_cast<value_type*>(&(*this->mListIterator));
    }
//Defer the details to the increment() helper in the base class
template <typename HashMap>
hash_map_iterator<HashMap>& hash_map_iterator<HashMap>::operator++()
{
    this->increment();
    return *this;
}

// Defer the details to the increment() helper in the base class.
template<typename HashMap>
hash_map_iterator<HashMap> hash_map_iterator<HashMap>::operator++(int)
{
    auto oldIt = *this;
    this->increment();
    return oldIt;
}
// Defer the details to the decrement() helper in the base class.
template<typename HashMap>
hash_map_iterator<HashMap>& hash_map_iterator<HashMap>::operator--()
{
    this->decrement();
    return *this;
}

//Defer the details to the decrement() helper in the base class 1
template<typename HashMap>
hash_map_iterator<HashMap> hash_map_iterator<HashMap>::operator--(int)
{
    auto oldIt = *this;
    this->decrement();
    return oldIt;
}

And iterator type aliases and access methods

hash_ The last part of map's iterator support is in hash_ Provide the necessary type aliases in the map class template, and write the begin(), end(), cbegin(), and cend() methods. Type aliases and method prototypes are as follows:

template<typename Key,typename T,	typename KeyEqual = std::equal_to<>,typename Hash = hash<Key>>class hash_map{    public:    	//Other type aliases omitted for brevity    	using iterator = hash_map_iterator<hash_map_type>;    	using const_iterator = const_hash_map_iterator<hash_map_type>;    	    	//Iterator methods    	iterator begin();    	iterator end();    	const_iterator begin() const;        const_iterator end() const;        const_iterator cbegin() const;        const_iterator cend() const;        // Remainder of class definition omitted for brevity};

The implementation of begin() includes optimization, in other words, if hash_ If there are no elements in the map, the tail generation will be returned. The code is as follows:

template<typename Key,typename T,typename KeyEqual,typename Hash>
typename hash_map<Key,T,KeyEqual,Hash>::iterator
    hash_map<Key,T,KeyEqual,Hash>::begin()
    {
        if(mSize == 0)
        {
            // Special case: there are no elements, so return the end iterator.
			return end();
        }
        // We know there is at least one element. Find the first element.
		for(size_t i = 0;i < mBuckets.size();++i)
        {
            if(!mBuckets[i].empty())
            {
                return hash_map_iterator<hash_map_type>(i,
std::begin(mBuckets[i]), this);
            }
        }
        // Should never reach here, but if we do, return the end iterator.
		return end();	
    }

end() creates a hash_ map_ The iterator references the tail iterator in the last bucket:

template <typename Key, typename T, typename KeyEqual, typename Hash>
typename hash_map<Key, T, KeyEqual, Hash>::iterator
hash_map<Key, T, KeyEqual, Hash>::end()
{
    // The end iterator is the end iterator of the list of the last bucket.
    size_t bucket = mBuckets.size() - 1;
    return hash_map_iterator<hash_map_type>(bucket,
    std::end(mBuckets[bucket]), this);
}

The begin() and end() implementations of the const version use const_ The non const version corresponding to the cast call. These non const versions return a hash_map_iterator, which can be converted to const_hash_map_iterator.

template<typename Key,typename T,typename KeyEqual, typename Hash>
typename hash_map<Key,T,KeyEqual,Hash>::const_iterator
    hash_map<Key,T,KeyEqual,Hash>::begin() const
    {
        return const_cast<hash_map_type*>(this)->begin();
    }
template<typename Key,typename T,typename KeyEqual,typename Hash>
typename hash_map<Key,T,KeyEqual,Hash>::const_iterator
	hash_map<Key, T, KeyEqual, Hash>::end() const
{
    return const_cast<hash_map_type*>(this)->end();
}

The cbegin() and cend() methods pass the request to the const version of begin() and end():

template <typename Key, typename T, typename KeyEqual, typename Hash>
typename hash_map<Key, T, KeyEqual, Hash>::const_iterator
	hash_map<Key, T, KeyEqual, Hash>::cbegin() const
{
	return begin();
}
template <typename Key, typename T, typename KeyEqual, typename Hash>
typename hash_map<Key, T, KeyEqual, Hash>::const_iterator
	hash_map<Key, T, KeyEqual, Hash>::cend() const
{
	return end();
}

Using hash_map_iterator

”Now hash_map supports iteration. You can iterate hash like any standard library container_ Map element, and hash_ The undergeneration of map is passed to methods and functions. Here are some examples:

hash_map<string, int> myHash;
myHash.insert(make_pair("KeyOne", 100));
myHash.insert(make_pair("KeyTwo", 200));
myHash.insert(make_pair("KeyThree", 300));

for (auto it = myHash.cbegin(); it != myHash.cend(); ++it) 
{
    // Use both -> and * to test the operations.
    cout << it->first << " maps to " << (*it).second << endl;
}

// Print elements using a range-based for loop
for (auto& p : myHash) 
{
	cout << p.first << " maps to " << p.second << endl;
}

// Print elements using a range-based for loop and C++17 structured bindings
for (auto&[key, value] : myHash) 
{
	cout << key << " maps to " << value << endl;
}

// Create an std::map with all the elements in the hash_map.
map<string, int> myMap(cbegin(myHash), cend(myHash));
for (auto& p : myMap) 
{
	cout << p.first << " maps to " << p.second << endl;
}

This code also shows that non member functions such as std::cbegin() and std::cend() work as expected.

  1. Supplementary instructions on distributor

As described earlier in this chapter, all standard library containers allow you to specify custom memory allocators. hash_ The implementation of map should be the same. However, since these have deviated from the main line of this chapter and custom allocators are rarely used, they are omitted.

  1. Supplementary instructions for reversible containers

If a container provides two-way access or random access iterators, the container can be considered reversible. A reversible container should provide two additional type aliases as shown in table 21-3.

Type nameexplain
reverse_iteratorBackward traversal of the types of elements in the container
const_reverse_iteratorAnother version of reverse_iterator, which traverses the const element in the container in reverse

In addition, the container should also provide rbegin() and rend() corresponding to begin() and end(); You should also provide crbegin() and crend(), which correspond to cend() with cbegin(). The general implementation uses STD:: reverse described earlier in this chapter_ The iterator adapter is OK. These implementations are left to the reader as an exercise.

  1. hash_map is implemented as an unordered Association container

In addition to the basic container requirements already shown, containers can also meet the requirements of orderly related containers, disorderly related containers or sequential containers. This section modifies the hash_map class template to meet the requirements of other unordered associated containers.

Type alias requirements for unordered associative containers

Unordered Association containers require the type aliases shown in table 21-4.

Type nameexplain
key_typeThe key type selected when instantiating the container
mapped_typeThe element type used when instantiating the container
value_typepair<const Key, T>
hasherThe hash type used when instantiating the container
key_equalThe equality predicate used when instantiating the container
local_iteratorThe iterator type used when selecting a single bucket cannot iterate across buckets
const_local_iteratorAnd const iterator type used when iterating over a single bucket, which cannot span buckets and under generations
node_typeIndicates the type of node. This section will not be discussed in detail. Please refer to the discussion of nodes in Chapter 17

The following is the hash that updated the type alias collection_ Map definition. Note that the definition of ListType has been moved because the definition of local iterators requires ListType.

template<typename Key,typename T,typename KeyEqual = std::equal_to<>,typename Hash = hash<Key>>
class hash_map
{
    public:
    	using key_type = Key;
    	using mapped_type = T;
    	using value_type = std::pair<const Key, T>;
		using hasher = Hash;
		using key_equal = KeyEqual;
	private:
    	using ListType = std::list<value_type>;
	public:
    	using local_iterator = typename ListType::iterator;
		using const_local_iterator = typename ListType::const_iterator;
		// Remainder of hash_map class definition omitted for brevity
}

Method requirements for unordered associated containers the C + + standard specifies some additional methods that unordered associated containers need to implement, as shown in table 21-5. The beast in the last column is the name of the element in the container

methodexplainComplexity
Constructor that takes an iterator range as a parameterConstruct the container and insert elements within the scope of the iterator. The antecedent scope is not required to reference other containers of the same type. Note that the constructors of all unordered associated containers must receive an equality predicate. The constructor should provide a default constructed object as the default valueAverage complexity, O(n) worst case complexity: O(n^2)
Receive initializer_ Constructor with list < value - type > as parameterReplace all elements in the container with elements in the initialization listAverage complexity: O(n) worst case complexity: O(n^2)
On the right is the assignment operator of initializer listReplace all elements in the container with elements in the initialization listAverage complexity: O(n) worst case complexity: O(n^2)
hasher hash_function() const;Return hash functionConstant time complexity
key_equal key_eq() const;Returns the equality predicate for the comparison keyConstant time complexity
pair<iterator, bool> insert(value_type&c);iterator insert(const_iterator hint, value_type&);Two different forms of insert() hint can be ignored by the implementation. Containers that allow duplicate keys only return iterator in the first form, because insert() will always succeedAverage complexity: O(1) bad case complexity: O(n)
void insert(Inputterator start, Inputlterator end);Insert an element range, which may not come from containers of the same typeAverage complexity: (m)m is the number of inserted elements. The most complex case is O(m*n+m)
void insert(initializer_list<value_type>);Inserts an element from the initialization list into the containerAverage complexity: (m)m is the number of inserted elements. The most complex case is O(m*n+m)
pair<iterator,bool> emplace(Args&&...);iterator emplace_hint(const_iterator hint,Args&&...);It realizes the placement operation and constructs the object in placeAverage complexity: O(1) most japonica case complexity: O(n)
size_type erase(key_type&); iterator erase(iterator position);iterator erase (iterator start, iterator end);There are three different forms of erase(). The first form returns the number of deleted values (0 or 1 in containers that do not allow duplicate keys). The second and third forms delete the element in position or the element from start to end, and the returned iterator points to the element after the last deleted elementMaximum case complexity: O(n)
void clear()Except all elementsO(n)
Iterator find(key_type&); const_iterator find(key_type&)Finds an element that matches the specified keyAverage complexity: O(1) most japonica case complexity: O(n)
size_type count(key_type&) const;Returns the number of elements that match the specified key (0 or 1 in containers that do not allow duplicate keys)Average complexity: O(1) most japonica case complexity: O(n)
pair<iterator,iterator>equal_range(key_type&); pair<const_iterator, const_iterator>equal_range(key_type&) const;Returns an iterator that references the first element that matches the specified key and the position after the last element that matches the specified keyMaximum case complexity: O(n)

Note, hash_map does not allow duplicate keys, so equal_range() always returns a pair of identical iterators. C++17 adds extract() and merge() methods to the requirements list. These two methods are related to the processing node (see Chapter 17), this hash_ The map implementation ignores it. Here is the complete hash_map class definition. The prototypes of insert(), erase() and find() are slightly changed compared with the previous versions, because the original version does not meet the requirements of unordered Association containers for return types;

template <typename Key, typename T, typename KeyEqual = std::equal_to<>,
typename Hash = hash<Key>>
class hash_map
{
public:
    using key_type = Key;
    using mapped_type = T;
    using value_type = std::pair<const Key, T>;
    using hasher = Hash;
    using key_equal = KeyEqual;
    using reference = value_type&;
    using const_reference = const value_type&;
    using size_type = size_t;
    using difference_type = ptrdiff_t;
    using hash_map_type = hash_map<Key, T, KeyEqual, Hash>;
    using iterator = hash_map_iterator<hash_map_type>;
    using const_iterator = const_hash_map_iterator<hash_map_type>;
private:
	using ListType = std::list<value_type>;
public:
    using local_iterator = typename ListType::iterator;
    using const_local_iterator = typename ListType::const_iterator;
    // The iterator classes need access to all members of the hash_map
    friend class hash_map_iterator<hash_map_type>;
    friend class const_hash_map_iterator<hash_map_type>;
    virtual ~hash_map() = default;
     // Virtual destructor
    // Throws invalid_argument if the number of buckets is illegal.
    explicit hash_map(const KeyEqual& equal = KeyEqual(),
    size_type numBuckets = 101, const Hash& hash = Hash());
    // Throws invalid_argument if the number of buckets is illegal.
    template <typename InputIterator>
    hash_map(InputIterator first, InputIterator last,
    const KeyEqual& equal = KeyEqual(),
    size_type numBuckets = 101, const Hash& hash = Hash());
    // Initializer list constructor
    // Throws invalid_argument if the number of buckets is illegal.
    explicit hash_map(std::initializer_list<value_type> il,
    const KeyEqual& equal = KeyEqual(), size_type numBuckets = 101,
    const Hash& hash = Hash());
    // Copy constructor
    hash_map(const hash_map_type& src) = default;
    // Move constructor
    hash_map(hash_map_type&& src) noexcept = default;
    // Copy assignment operator
    hash_map_type& operator=(const hash_map_type& rhs);
    // Move assignment operator
    hash_map_type& operator=(hash_map_type&& rhs) noexcept;
    // Initializer list assignment operator
    hash_map_type& operator=(std::initializer_list<value_type> il);
    // Iterator methods
    iterator begin();
    iterator end();
    const_iterator begin() const;
    const_iterator end() const;
    const_iterator cbegin() const;
    const_iterator cend() const;
    // Size methods
    bool empty() const;
    size_type size() const;
    size_type max_size() const;
    // Element insert methods
    T& operator[](const key_type& k);
    std::pair<iterator, bool> insert(const value_type& x);
    iterator insert(const_iterator hint, const value_type& x);
    template <typename InputIterator>
    void insert(InputIterator first, InputIterator last);
    void insert(std::initializer_list<value_type> il);
    // Element delete methods
    size_type erase(const key_type& k);
    iterator erase(iterator position);
    iterator erase(iterator first, iterator last);
    // Other modifying utilities
    void swap(hash_map_type& other) noexcept;
    void clear() noexcept;
    // Access methods for Standard Library conformity
    key_equal key_eq() const;
    hasher hash_function() const;
    // Lookup methods
    iterator find(const key_type& k);
    const_iterator find(const key_type& k) const;
    std::pair<iterator, iterator> equal_range(const key_type& k);
    std::pair<const_iterator, const_iterator>
    equal_range(const key_type& k) const;
    size_type count(const key_type& k) const;
private:
    // Returns a pair containing an iterator to the found element with
    // a given key, and the index of that element's bucket.
    std::pair<typename ListType::iterator, size_t> findElement(
    const key_type& k);
    std::vector<ListType> mBuckets;
    size_type mSize = 0;
    KeyEqual mEqual;
    Hash mHash;
};

hash_map iterator scope constructor

The constructor that receives iterator ranges is a method template that can receive iterator ranges from any container, not just from other hashes_ Iterator range of the map. If this is not a method template, you need to explicitly specify the Inputterator type as hash_map_iterator, which is limited to receiving only from hash_map undergeneration. No matter how complex the syntax is, it is not difficult to implement: it will construct delegated delegate to explicit constructor to initialize all data members, then call insert() to insert all elements in the specified range.

// Make a call to insert() to actually insert the elements.
template <typename Key, typename T, typename KeyEqual, typename Hash>
template <typename InputIterator>
hash_map<Key, T, KeyEqual, Hash>::hash_map(
InputIterator first, InputIterator last, const KeyEqual& equal,
size_type numBuckets, const Hash& hash)
: hash_map(equal, numBuckets, hash)
{
	insert(first, last);
}

hash_map initializes the list constructor

Chapter 1 discusses initialization lists. The following is a hash that receives an initialization list as a parameter_ The implementation of the map constructor is very similar to the implementation of the constructor that receives the iterator range:

template <typename Key, typename T, typename KeyEqual, typename Hash>
hash_map<Key, T, KeyEqual, Hash>::hash_map(
std::initializer_list<value_type> il,
const KeyEqual& equal, size_type numBuckets, const Hash& hash)
: hash_map(equal, numBuckets, hash)
{
	insert(std::begin(il), std::end(il));
}

With this initialization list constructor, you can construct a hash as follows_ map:

hash_map<string, int> myHash {
{ "KeyOne", 100 },
{ "KeyTwo", 200 },
{ "KeyThree", 300 } };

hash_map initialization list assignment operator

The assignment operator can also receive an initialization list on the right. Here is the hash_map initializes the implementation of the list assignment operator. It uses algorithms based on "copy and exchange" idiomatic syntax to ensure strong exception security.

template <typename Key, typename T, typename KeyEqual, typename Hash>
hash_map<Key, T, KeyEqual, Hash>& 
hash_map<Key, T, KeyEqual, Hash>::operator=(std::initializer_list<value_type> il)
{
    // Do all the work in a temporary instance
    hash_map_type newHashMap(il, mEqual, mBuckets.size(), mHash);
    swap(newHashMap); // Commit the work with only non-throwing operations
    return *this;
}

With this assignment operator, you can write the following code

myHash = {{ "KeyOne", 100 },{ "KeyTwo", 200 },{ "KeyThree", 300 } };

hash_map insert operation

The basic hash is discussed earlier in this chapter_ The map section provides a simple insert() method. In this version, four insert() versions with additional functions are provided: a simple insert() operation returns a pair < iterator, bool >, indicating where the element is inserted and whether a new element is created. The insert() version that receives a hint as a parameter is in the hash_map does not work, but it also needs to be provided in order to be symmetrical with other types of collections. This hint is ignored, and then only the first version is called. The third form of insert() is a method template, so it can be used in hash_ Insert the element range of any container into the map. The last form of insert() receives an initializer_ list<value_ type>. Note that from a technical point of view, the following insert() versions are also available, which receive right value references.

std::pair<iterator, bool> insert(value_type&& x);iterator insert(const_iterator hint, value_type&& x);

hash_map does not provide these. In addition, there are two versions of insert() related to processing nodes. Chapter 17 discusses nodes. hash_ The implementation codes of the first two forms of insert() method of map are as follows:

template <typename Key, typename T, typename KeyEqual, typename Hash>
std::pair<typename hash_map<Key, T, KeyEqual, Hash>::iterator, bool>
	hash_map<Key, T, KeyEqual, Hash>::insert(const value_type &x)
{
    // Try to find the element.
    auto[it, bucket] = findElement(x.first);
    bool inserted = false;
    if (it == std::end(mBuckets[bucket]))
    {
        // We didn't find the element, so insert a new one.
        it = mBuckets[bucket].insert(it, x);
        inserted = true;
        mSize++;
    }
    return std::make_pair(hash_map_iterator<hash_map_type>(bucket, it, this), inserted);
}
template <typename Key, typename T, typename KeyEqual, typename Hash>
typename hash_map<Key, T, KeyEqual, Hash>::iterator
hash_map<Key, T, KeyEqual, Hash>::insert(const_iterator /*hint*/, 
                                         const value_type &x)
{
    // Completely ignore position.
    return insert(x).first;
}

The third form of insert() is a method template for the same reason as the constructor shown earlier: it should be possible to insert elements using and iterators from any type of container. The actual implementation uses an insert_iterator, see the discussion earlier in this chapter;

template <typename Key, typename T, typename KeyEqual, typename Hash>template <typename InputIterator>void hash_map<Key, T, KeyEqual, Hash>::insert(InputIterator first, 													  InputIterator last){    // Copy each element in the range by using an insert_iterator adaptor.    // Give begin() as a dummy position -- insert ignores it anyway.    std::insert_iterator<hash_map_type> inserter(*this, begin());	std::copy(first, last, inserter);}

The last form of insert0 receives the initialization list, hash_ This implementation of map simply hands over the work to the insert() method that receives an iterator scope.

template <typename Key, typename T, typename KeyEqual, typename Hash>void hash_map<Key, T, KeyEqual, Hash>::insert(std::initializer_list<value_type> il){	insert(std::begin(il), std::end(il));}

With this insert() method, you can write the following code:

myHash.insert({{ "KeyFour", 400 },{ "KeyFive", 500 } });

hash_map placement operation

You can construct objects in place using placement operations, which are discussed in Chapter 17. hash_ The map is placed as follows:

template <typename... Args>std::pair<iterator, bool> emplace(Args&&... args);template <typename... Args>iterator emplace_hint(const_iterator hint, Args&&... args);

The "..." in these lines of code is not an input error. They are the so-called variable template, that is, the template has a variable number of template type parameters and function parameters. Chapter 22 discusses variable parameter templates. hash_ The map implementation ignores the placement operation.

hash_map delete operation

The basic hash is discussed earlier in this chapter_ The erase () version mentioned in the map does not meet the requirements of the standard library. You need to implement the following version of erase():

A version receives a key_type and returns a size_ The type value indicates the number of elements deleted from the collection (for hash_map, there are only two possible return values: (0 and 1)).

Another version deletes the element at the specified iterator position and returns an iterator pointing to the element after the deleted element.

The third version deletes the elements in the range specified by the two iterators and returns an iterator pointing to the element after the last deleted element

The implementation of the first version of erase() is as follows:

template<typename Key, typename T, typename KeyEqual,typename Hash>
typename hash_map<Key, T, KeyEqual, Hash>::size_type
    hash_map<Key,T,KeyEqual,Hash>::erase(const key_type & k)
    {
        // First, try to find the element.
		auto[it, bucket] = findElement(k);
		if(it != std::end(mBuckets[bucket]))
        {
            //The element exists --erase it
            mBuckets[bucket].erase(it);
            mSize--;
            return 1;
        }
        else
        {
            return 0;
        }
    }

The second version of erase() must remove the element used for the specified iterator location. The given iterator is of course a hash_map_iterator. Therefore, hash_map should have some ability to pass hash_map_ The iterator gets the underlying bucket and the list iterator. The method we take is to hash_ The map class is defined as hash_map_iterator's friend (not shown in the previous class definition). The following is the implementation of this erase() version:

template<typename Key,typename T,typename KeyEqual,typename Hash>
typename hash_map<Key, T, KeyEqual, Hash>::iterator
	hash_map<Key, T, KeyEqual, Hash>::erase(iterator position)
	{
		iterator next = position;
        ++next;
        //Erase the element from its bucket
        mBuckets[position.mBucketIndex].erase(position.mListIterator);
        mSize--;
        return next;
	}

The last version of erase() deletes elements in a range. From the first iteration to the last iteration, call erase (0) for each element, and let the previous version of erase() complete all the work:

template <typename Key, typename T, typename KeyEqual, typename Hash>typename hash_map<Key, T, KeyEqual, Hash>::iteratorhash_map<Key, T, KeyEqual, Hash>::erase(iterator first, iterator last){    // Erase all the elements in the range.    for (iterator next = first; next != last;)     {    	next = erase(next);    }    return last;}

hash_map accessor operation

The C + + standard requires the use of key_eq() and hash_ The function() method retrieves the equality predicate and hash function respectively:

template <typename Key, typename T, typename KeyEqual, typename Hash>
typename hash_map<Key, T, KeyEqual, Hash>::key_equal
	hash_map<Key, T, KeyEqual, Hash>::key_eq() const
{
	return mEqual;
}

template <typename Key, typename T, typename KeyEqual, typename Hash>
typename hash_map<Key, T, KeyEqual, Hash>::hasher
hash_map<Key, T, KeyEqual, Hash>::hash_function() const
{
	return mHash;
}

This fnd() method is the same as the previous basic hash_ The find() method of map is similar, but the return code is different. Instead of returning a pointer to an element, this find() version constructs a hash that references the element_ map_ iterator:

template <typename Key, typename T, typename KeyEqual, typename Hash>
typename hash_map<Key, T, KeyEqual, Hash>::iterator
hash_map<Key, T, KeyEqual, Hash>::find(const key_type& k)
{
    // Use the findElement() helper, and C++17 structured bindings.
    auto[it, bucket] = findElement(k);
    if (it == std::end(mBuckets[bucket])) 
    {
        // Element not found -- return the end iterator.
        return end();
    }
    // Element found -- convert the bucket/iterator to a hash_map_iterator.
    return hash_map_iterator<hash_map_type>(bucket, it, this);
}

The const version of find() returns const_hash_map_iterator and use const_cast calls the non const version of find() to avoid code duplication. Note that the non const version of find() returns hash_map_iterator,hash_ map_ The iterator is converted to const_hash_map_iterator.

template <typename Key, typename T, typename KeyEqual, typename Hash>typename hash_map<Key, T, KeyEqual, Hash>::const_iteratorhash_map<Key, T, KeyEqual, Hash>::find(const key_type& k) const{	return const_cast<hash_map_type*>(this)->find(k);}

The equal range() implementations of both versions are the same, but one of them returns a pair of hashes_ map_ Iterator, and the other returns a pair of consts_ hash_map_iterator. They just pass the request to find(). hash_map cannot contain elements with duplicate keys, so. Equal of hash map_ The range () implementation always returns a pair of identical iterators.

template <typename Key, typename T, typename KeyEqual, typename Hash>
std::pair<
typename hash_map<Key, T, KeyEqual, Hash>::iterator,
typename hash_map<Key, T, KeyEqual, Hash>::iterator>
hash_map<Key, T, KeyEqual, Hash>::equal_range(const key_type& k)
{
    auto it = find(k);
    return std::make_pair(it, it);
}

hash_map does not allow duplicate keys. count() can only return 1 or 0: if an element is found, it returns 1, and otherwise it returns 0. The implementation simply wraps the find() call. If the find() method does not find an element, it returns a trailing generation. count() calls end() to get the tail iterator for comparison.

template <typename Key, typename T, typename KeyEqual, typename Hash>
typename hash_map<Key, T, KeyEqual, Hash>::size_type
hash_map<Key, T, KeyEqual, Hash>::count(const key_type& k) const
{
    // There are either 1 or 0 elements matching key k.
    // If we can find a match, return 1, otherwise return 0.
    if (find(k) == end()) 
    {
        return 0;
    } 
    else 
    {
        return 1;
    }
}

The last method is operator [], which is not required by the C + + standard, but it can provide convenience for programmers and corresponds to the method of std::map. The prototype and implementation of this method is equivalent to the operator [] in the standard library:: map. The comments in the following code explain single line implementation code that may be difficult to understand

template <typename Key, typename T, typename KeyEqual, typename Hash>
T& hash_map<Key, T, KeyEqual, Hash>::operator[] (const key_type& k)
{
    // It's a bit cryptic, but it basically attempts to insert
    // a new key/value pair of k and a zero-initialized value. Regardless
    // of whether the insert succeeds or fails, insert() returns a pair of
    // an iterator/bool. The iterator refers to a key/value pair, the
    // second element of which is the value we want to return.
    return ((insert(std::make_pair(k, T()))).first)->second;
}

hash_map bucket operation

Unordered associative containers also provide multiple bucket related methods:

bucket_count() returns the number of buckets in the container.

max_bucket_count() returns the maximum number of buckets supported.

bucket(key) returns the index of the bucket to which the given key is mapped.

bucket_size(n) returns the number of elements in the bucket with the given index.

begin(n), end(n), cbegin(m), and cend(m) return the local head and tail generators of the bucket with the given index. Here is the hash_map implementation:

template <typename Key, typename T, typename KeyEqual, typename Hash>
typename hash_map<Key, T, KeyEqual, Hash>::size_type
hash_map<Key, T, KeyEqual, Hash>::bucket_count() const
{
	return mBuckets.size();
}
template <typename Key, typename T, typename KeyEqual, typename Hash>
typename hash_map<Key, T, KeyEqual, Hash>::size_type
hash_map<Key, T, KeyEqual, Hash>::max_bucket_count() const
{
	return mBuckets.max_size();
}
template <typename Key, typename T, typename KeyEqual, typename Hash>
typename hash_map<Key, T, KeyEqual, Hash>::size_type
hash_map<Key, T, KeyEqual, Hash>::bucket(const Key& k) const
{
	return const_cast<hash_map_type*>(this)->findElement(k).second;
}
template <typename Key, typename T, typename KeyEqual, typename Hash>
typename hash_map<Key, T, KeyEqual, Hash>::size_type
hash_map<Key, T, KeyEqual, Hash>::bucket_size(size_type n) const
{
	return mBuckets[n].size();
}
template <typename Key, typename T, typename KeyEqual, typename Hash>
typename hash_map<Key, T, KeyEqual, Hash>::local_iterator
hash_map<Key, T, KeyEqual, Hash>::begin(size_type n)
{
	return mBuckets[n].begin();
}

The implementation of other begin(m), end(n), cbegin() and cend() methods is similar. They simply forward the call to the correct bucket list based on the given index. Finally, the unordered Association container should provide load and packages ctor(), max_ load_ The factor (), rehash() and reserve() methods. hash_map ignores these methods.

  1. Supplementary instructions for sequential containers

Previously developed hash_map is an unordered Association container. Of course, you can also write sequential containers or ordered associative containers, but follow different requirements. These requirements are not listed here. A simpler way is to point out that the deque container almost perfectly meets the requirements of the specified sequential container. The only difference is that deque provides an additional resize() method (which is not required by the C + + standard). An example of an ordered Association container is map. You can build your own ordered Association container based on map.

Posted by mechew on Sat, 02 Oct 2021 13:12:17 -0700