Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
B.Eckel - Thinking in C++, Vol.2, 2nd edition.pdf
Скачиваний:
58
Добавлен:
08.05.2013
Размер:
2.09 Mб
Скачать

When using an istreambuf_iterator, you create one to attach to the istream object, and one with the default constructor as the past-the-end marker. Both of these are used to create the TokenIterator that will actually produce the tokens; the default constructor produces the faux TokenIterator past-the-end sentinel (this is just a placeholder, and as mentioned previously is actually ignored). The TokenIterator produces strings that are inserted into a container which must, naturally, be a container of string – here a vector<string> is used in all cases except the last (you could also concatenate the results onto a string). Other than that, a TokenIterator works like any other input iterator.

stack

The stack, along with the queue and priority_queue, are classified as adapters, which means they are implemented using one of the basic sequence containers: vector, list or deque. This, in my opinion, is an unfortunate case of confusing what something does with the details of its underlying implementation – the fact that these are called “adapters” is of primary value only to the creator of the library. When you use them, you generally don’t care that they’re adapters, but instead that they solve your problem. Admittedly there are times when it’s useful to know that you can choose an alternate implementation or build an adapter from an existing container object, but that’s generally one level removed from the adapter’s behavior. So, while you may see it emphasized elsewhere that a particular container is an adapter, I shall only point out that fact when it’s useful. Note that each type of adapter has a default container that it’s built upon, and this default is the most sensible implementation, so in most cases you won’t need to concern yourself with the underlying implementation.

The following example shows stack<string> implemented in the three possible ways: the default (which uses deque), with a vector and with a list:

//: C04:Stack1.cpp

//Demonstrates the STL stack #include "../require.h" #include <iostream>

#include <fstream> #include <stack> #include <list> #include <vector> #include <string> using namespace std;

//Default: deque<string>: typedef stack<string> Stack1;

//Use a vector<string>:

typedef stack<string, vector<string> > Stack2; // Use a list<string>:

typedef stack<string, list<string> > Stack3;

Chapter 15: Multiple Inheritance

208

int main(int argc, char* argv[]) { requireArgs(argc, 1); // File name is argument ifstream in(argv[1]);

assure(in, argv[1]);

Stack1 textlines; // Try the different versions

//Read file and store lines in the stack: string line;

while(getline(in, line)) textlines.push(line + "\n");

//Print lines from the stack and pop them: while(!textlines.empty()) {

cout << textlines.top(); textlines.pop();

}

}///:~

The top( ) and pop( ) operations will probably seem non-intuitive if you’ve used other stack classes. When you call pop( ) it returns void rather than the top element that you might have expected. If you want the top element, you get a reference to it with top( ). It turns out this is more efficient, since a traditional pop( ) would have to return a value rather than a reference, and thus invoke the copy-constructor. When you’re using a stack (or a priority_queue, described later) you can efficiently refer to top( ) as many times as you want, then discard the top element explicitly using pop( ) (perhaps if some other term than the familiar “pop” had been used, this would have been a bit clearer).

The stack template has a very simple interface, essentially the member functions you see above. It doesn’t have sophisticated forms of initialization or access, but if you need that you can use the underlying container that the stack is implemented upon. For example, suppose you have a function that expects a stack interface but in the rest of your program you need the objects stored in a list. The following program stores each line of a file along with the leading number of spaces in that line (you might imagine it as a starting point for performing some kinds of source-code reformatting):

//: C04:Stack2.cpp

//Converting a list to a stack #include "../require.h" #include <iostream>

#include <fstream> #include <stack> #include <list> #include <string> using namespace std;

//Expects a stack:

Chapter 15: Multiple Inheritance

209

template<class Stk>

void stackOut(Stk& s, ostream& os = cout) { while(!s.empty()) {

os << s.top() << "\n"; s.pop();

}

}

class Line {

string line; // Without leading spaces int lspaces; // Number of leading spaces

public:

Line(string s) : line(s) {

lspaces = line.find_first_not_of(' '); if(lspaces == string::npos)

lspaces = 0;

line = line.substr(lspaces);

}

friend ostream&

operator<<(ostream& os, const Line& l) { for(int i = 0; i < l.lspaces; i++)

os << ' ';

return os << l.line;

}

// Other functions here...

};

int main(int argc, char* argv[]) { requireArgs(argc, 1); // File name is argument ifstream in(argv[1]);

assure(in, argv[1]); list<Line> lines;

//Read file and store lines in the list: string s;

while(getline(in, s)) lines.push_front(s);

//Turn the list into a stack for printing: stack<Line, list<Line> > stk(lines); stackOut(stk);

}///:~

The function that requires the stack interface just sends each top( ) object to an ostream and then removes it by calling pop( ). The Line class determines the number of leading spaces, then stores the contents of the line without the leading spaces. The ostream operator<< re-

Chapter 15: Multiple Inheritance

210

inserts the leading spaces so the line prints properly, but you can easily change the number of spaces by changing the value of lspaces (the member functions to do this are not shown here).

In main( ), the input file is read into a list<Line>, then a stack is wrapped around this list so it can be sent to stackOut( ).

You cannot iterate through a stack; this emphasizes that you only want to perform stack operations when you create a stack. You can get equivalent “stack” functionality using a vector and its back( ), push_back( ) and pop_back( ) methods, and then you have all the additional functionality of the vector. Stack1.cpp can be rewritten to show this:

//: C04:Stack3.cpp

// Using a vector as a stack; modified Stack1.cpp #include "../require.h"

#include <iostream> #include <fstream> #include <vector> #include <string> using namespace std;

int main(int argc, char* argv[]) { requireArgs(argc, 1);

ifstream in(argv[1]); assure(in, argv[1]); vector<string> textlines; string line; while(getline(in, line))

textlines.push_back(line + "\n"); while(!textlines.empty()) {

cout << textlines.back(); textlines.pop_back();

}

} ///:~

You’ll see this produces the same output as Stack1.cpp, but you can now perform vector operations as well. Of course, list has the additional ability to push things at the front, but it’s generally less efficient than using push_back( ) with vector. (In addition, deque is usually more efficient than list for pushing things at the front).

queue

The queue is a restricted form of a deque – you can only enter elements at one end, and pull them off the other end. Functionally, you could use a deque anywhere you need a queue, and you would then also have the additional functionality of the deque. The only reason you need

Chapter 15: Multiple Inheritance

211

to use a queue rather than a deque, then, is if you want to emphasize that you will only be performing queue-like behavior.

The queue is an adapter class like stack, in that it is built on top of another sequence container. As you might guess, the ideal implementation for a queue is a deque, and that is the default template argument for the queue; you’ll rarely need a different implementation.

Queues are often used when modeling systems where some elements of the system are waiting to be served by other elements in the system. A classic example of this is the “bankteller problem,” where you have customers arriving at random intervals, getting into a line, and then being served by a set of tellers. Since the customers arrive randomly and each take a random amount of time to be served, there’s no way to deterministically know how long the line will be at any time. However, it’s possible to simulate the situation and see what happens.

A problem in performing this simulation is the fact that, in effect, each customer and teller should be run by a separate process. What we’d like is a multithreaded environment, then each customer or teller would have their own thread. However, Standard C++ has no model for multithreading so there is no standard solution to this problem. On the other hand, with a little adjustment to the code it’s possible to simulate enough multithreading to provide a satisfactory solution to our problem.

Multithreading means you have multiple threads of control running at once, in the same address space (this differs from multitasking, where you have different processes each running in their own address space). The trick is that you have fewer CPUs than you do threads (and very often only one CPU) so to give the illusion that each thread has its own CPU there is a time-slicing mechanism that says “OK, current thread – you’ve had enough time. I’m going to stop you and go give time to some other thread.” This automatic stopping and starting of threads is called pre-emptive and it means you don’t need to manage the threading process at all.

An alternative approach is for each thread to voluntarily yield the CPU to the scheduler, which then goes and finds another thread that needs running. This is easier to synthesize, but it still requires a method of “swapping” out one thread and swapping in another (this usually involves saving the stack frame and using the standard C library functions setjmp( ) and longjmp( ); see my article in the (XX) issue of Computer Language magazine for an example). So instead, we’ll build the time-slicing into the classes in the system. In this case, it will be the tellers that represent the “threads,” (the customers will be passive) so each teller will have an infinite-looping run( ) method that will execute for a certain number of “time units,” and then simply return. By using the ordinary return mechanism, we eliminate the need for any swapping. The resulting program, although small, provides a remarkably reasonable simulation:

//: C04:BankTeller.cpp

//Using a queue and simulated multithreading

//To model a bank teller system

#include <iostream> #include <queue>

Chapter 15: Multiple Inheritance

212

#include <list> #include <cstdlib> #include <ctime> using namespace std;

class Customer { int serviceTime;

public:

Customer() : serviceTime(0) {} Customer(int tm) : serviceTime(tm) {} int getTime() { return serviceTime; } void setTime(int newtime) {

serviceTime = newtime;

}

friend ostream&

operator<<(ostream& os, const Customer& c) { return os << '[' << c.serviceTime << ']';

}

};

class Teller { queue<Customer>& customers; Customer current;

static const int slice = 5;

int ttime; // Time left in slice

bool busy; // Is teller serving a customer? public:

Teller(queue<Customer>& cq)

: customers(cq), ttime(0), busy(false) {} Teller& operator=(const Teller& rv) {

customers = rv.customers; current = rv.current; ttime = rv.ttime;

busy = rv.busy; return *this;

}

bool isBusy() { return busy; } void run(bool recursion = false) {

if(!recursion) ttime = slice;

int servtime = current.getTime(); if(servtime > ttime) {

servtime -= ttime;

Chapter 15: Multiple Inheritance

213

current.setTime(servtime);

busy = true; // Still working on current return;

}

if(servtime < ttime) { ttime -= servtime; if(!customers.empty()) {

current = customers.front(); customers.pop(); // Remove it busy = true;

run(true); // Recurse

}

return;

}

if(servtime == ttime) {

// Done with current, set to empty: current = Customer(0);

busy = false;

return; // No more time in this slice

}

}

};

// Inherit to access protected implementation: class CustomerQ : public queue<Customer> { public:

friend ostream&

operator<<(ostream& os, const CustomerQ& cd) { copy(cd.c.begin(), cd.c.end(),

ostream_iterator<Customer>(os, "")); return os;

}

};

int main() {

CustomerQ customers; list<Teller> tellers;

typedef list<Teller>::iterator TellIt; tellers.push_back(Teller(customers)); srand(time(0)); // Seed random number generator while(true) {

//Add a random number of customers to the

//queue, with random service times:

Chapter 15: Multiple Inheritance

214

for(int i = 0; i < rand() % 5; i++) customers.push(Customer(rand() % 15 + 1));

cout << '{' << tellers.size() << '}'

<<customers << endl;

//Have the tellers service the queue: for(TellIt i = tellers.begin();

i != tellers.end(); i++) (*i).run();

cout << '{' << tellers.size() << '}'

<<customers << endl;

//If line is too long, add another teller: if(customers.size() / tellers.size() > 2)

tellers.push_back(Teller(customers));

//If line is short enough, remove a teller: if(tellers.size() > 1 &&

customers.size() / tellers.size() < 2) for(TellIt i = tellers.begin();

i!= tellers.end(); i++)

if(!(*i).isBusy()) { tellers.erase(i);

break; // Out of for loop

}

}

} ///:~

Each customer requires a certain amount of service time, which is the number of time units that a teller must spend on the customer in order to serve that customer’s needs. Of course, the amount of service time will be different for each customer, and will be determined randomly. In addition, you won’t know how many customers will be arriving in each interval, so this will also be determined randomly.

The Customer objects are kept in a queue<Customer>, and each Teller object keeps a reference to that queue. When a Teller object is finished with its current Customer object, that Teller will get another Customer from the queue and begin working on the new Customer, reducing the Customer’s service time during each time slice that the Teller is allotted. All this logic is in the run( ) member function, which is basically a three-way if statement based on whether the amount of time necessary to serve the customer is less than, greater than or equal to the amount of time left in the teller’s current time slice. Notice that if the Teller has more time after finishing with a Customer, it gets a new customer and recurses into itself.

Just as with a stack, when you use a queue, it’s only a queue and doesn’t have any of the other functionality of the basic sequence containers. This includes the ability to get an iterator in order to step through the stack. However, the underlying sequence container (that the queue is built upon) is held as a protected member inside the queue, and the identifier for

Chapter 15: Multiple Inheritance

215

Соседние файлы в предмете Численные методы
  • #
    08.05.20133.99 Mб25A.Menezes, P.van Oorschot,S.Vanstone - HANDBOOK OF APPLIED CRYPTOGRAPHY.djvu
  • #
  • #
    08.05.20135.91 Mб28B.Eckel - Thinking in Java, 3rd edition (beta).pdf
  • #
  • #
    08.05.20136.09 Mб20D.MacKay - Information Theory, Inference, and Learning Algorithms.djvu
  • #
    08.05.20133.85 Mб19DIGITAL Visual Fortran ver.5.0 - Programmers Guide to Fortran.djvu
  • #
    08.05.20131.84 Mб16E.A.Lee, P.Varaiya - Structure and Interpretation of Signals and Systems.djvu