The most interesting part of the classes derived from Query_base
is how they are represented. The WordQuery
class is most straightforward. Its job is to hold the search word.
The other classes operate on one or two operands. A NotQuery
has a single operand, and AndQuery
and OrQuery
have two operands. In each of these classes, the operand(s) can be an object of any of the concrete classes derived from Query_base
: A NotQuery
can be applied to a WordQuery
, an AndQuery
, an OrQuery
, or another NotQuery
. To allow this flexibility, the operands must be stored as pointers to Query_base
. That way we can bind the pointer to whichever concrete class we need.
However, rather than storing a Query_base
pointer, our classes will themselves use a Query
object. Just as user code is simplified by using the interface class, we can simplify our own class code by using the same class.
Now that we know the design for these classes, we can implement them.
WordQuery
ClassA WordQuery
looks for a given string
. It is the only operation that actually performs a query on the given TextQuery
object:
class WordQuery: public Query_base {
friend class Query; // Query uses the WordQuery constructor
WordQuery(const std::string &s): query_word(s) { }
// concrete class: WordQuery defines all inherited pure virtual functions
QueryResult eval(const TextQuery &t) const
{ return t.query(query_word); }
std::string rep() const { return query_word; }
std::string query_word; // word for which to search
};
Like Query_base
, WordQuery
has no public
members; WordQuery
must make Query
a friend in order to allow Query
to access the WordQuery
constructor.
Each of the concrete query classes must define the inherited pure virtual functions, eval
and rep
. We defined both operations inside the WordQuery
class body: eval
calls the query
member of its given TextQuery
parameter, which does the actual search in the file; rep
returns the string
that this WordQuery
represents (i.e., query_word
).
Having defined the WordQuery
class, we can now define the Query
constructor that takes a string
:
inline
Query::Query(const std::string &s): q(new WordQuery(s)) { }
This constructor allocates a WordQuery
and initializes its pointer member to point to that newly allocated object.
NotQuery
Class and the ~
OperatorThe ~
operator generates a NotQuery
, which holds a Query
, which it negates:
class NotQuery: public Query_base {
friend Query operator~(const Query &);
NotQuery(const Query &q): query(q) { }
// concrete class: NotQuery defines all inherited pure virtual functions
std::string rep() const {return "~(" + query.rep() + ")";}
QueryResult eval(const TextQuery&) const;
Query query;
};
inline Query operator~(const Query &operand)
{
return std::shared_ptr<Query_base>(new NotQuery(operand));
}
Because the members of NotQuery
are all private
, we start by making the ~
operator a friend. To rep
a NotQuery
, we concatenate the ~
symbol to the representation of the underlying Query
. We parenthesize the output to ensure that precedence is clear to the reader.
It is worth noting that the call to rep
in NotQuery
’s own rep
member ultimately makes a virtual call to rep
: query.rep()
is a nonvirtual call to the rep
member of the Query
class. Query::rep
in turn calls q->rep()
, which is a virtual call through its Query_base
pointer.
The ~
operator dynamically allocates a new NotQuery
object. The return (implicitly) uses the Query
constructor that takes a shared_ptr<Query_base>
. That is, the return
statement is equivalent to
// allocate a new NotQuery object
// bind the resulting NotQuery pointer to a shared_ptr<Query_base
shared_ptr<Query_base> tmp(new NotQuery(expr));
return Query(tmp); // use the Query constructor that takes a shared_ptr
The eval
member is complicated enough that we will implement it outside the class body. We’ll define the eval
functions in §15.9.4 (p. 647).
BinaryQuery
ClassThe BinaryQuery
class is an abstract base class that holds the data needed by the query types that operate on two operands:
class BinaryQuery: public Query_base {
protected:
BinaryQuery(const Query &l, const Query &r, std::string s):
lhs(l), rhs(r), opSym(s) { }
// abstract class: BinaryQuery doesn't define eval
std::string rep() const { return "(" + lhs.rep() + " "
+ opSym + " "
+ rhs.rep() + ")"; }
Query lhs, rhs; // right- and left-hand operands
std::string opSym; // name of the operator
};
The data in a BinaryQuery
are the two Query
operands and the corresponding operator symbol. The constructor takes the two operands and the operator symbol, each of which it stores in the corresponding data members.
To rep
a BinaryOperator
, we generate the parenthesized expression consisting of the representation of the left-hand operand, followed by the operator, followed by the representation of the right-hand operand. As when we displayed a NotQuery
, the calls to rep
ultimately make virtual calls to the rep
function of the Query_base
objects to which lhs
and rhs
point.
The BinaryQuery
class does not define the eval
function and so inherits a pure virtual. Thus, BinaryQuery
is also an abstract base class, and we cannot create objects of BinaryQuery
type.
AndQuery
and OrQuery
Classes and Associated OperatorsThe AndQuery
and OrQuery
classes, and their corresponding operators, are quite similar to one another:
class AndQuery: public BinaryQuery {
friend Query operator& (const Query&, const Query&);
AndQuery(const Query &left, const Query &right):
BinaryQuery(left, right, "&") { }
// concrete class: AndQuery inherits rep and defines the remaining pure virtual
QueryResult eval(const TextQuery&) const;
};
inline Query operator&(const Query &lhs, const Query &rhs)
{
return std::shared_ptr<Query_base>(new AndQuery(lhs, rhs));
}
class OrQuery: public BinaryQuery {
friend Query operator|(const Query&, const Query&);
OrQuery(const Query &left, const Query &right):
BinaryQuery(left, right, "|") { }
QueryResult eval(const TextQuery&) const;
};
inline Query operator|(const Query &lhs, const Query &rhs)
{
return std::shared_ptr<Query_base>(new OrQuery(lhs, rhs));
}
These classes make the respective operator a friend and define a constructor to create their BinaryQuery
base part with the appropriate operator. They inherit the BinaryQuery
definition of rep
, but each overrides the eval
function.
Like the ~
operator, the &
and |
operators return a shared_ptr
bound to a newly allocated object of the corresponding type. That shared_ptr
gets converted to Query
as part of the return statement in each of these operators.
Exercise 15.34: For the expression built in Figure 15.3 (p. 638):
(a) List the constructors executed in processing that expression.
(b) List the calls to rep
that are made from cout << q
.
(c) List the calls to eval
made from q.eval()
.
Exercise 15.35: Implement the Query
and Query_base
classes, including a definition of rep
but omitting the definition of eval
.
Exercise 15.36: Put print statements in the constructors and rep
members and run your code to check your answers to (a)
and (b)
from the first exercise.
Exercise 15.37: What changes would your classes need if the derived classes had members of type shared_ptr<Query_base>
rather than of type Query
?
Exercise 15.38: Are the following declarations legal? If not, why not? If so, explain what the declarations mean.
BinaryQuery a = Query("fiery") & Query("bird");
AndQuery b = Query("fiery") & Query("bird");
OrQuery c = Query("fiery") & Query("bird");