Templates raise two classes of challenges when it comes to
debugging them. One set of challenges is definitely a problem for writers of
templates: How can we ensure that the templates we write will function for any template arguments that satisfy the conditions we
document? The other class of problems is almost exactly the opposite: How can a
user of a template find out which of the template parameter requirements it
violated when the template does not behave as documented?
Before we discuss these issues in depth, it is useful to
contemplate the kinds of constraints that may be imposed on template parameters.
In this section we deal mostly with the constraints that lead to compilation
errors when violated, and we call these constraints syntactic constraints. Syntactic constraints can
include the need for a certain kind of constructor to exist, for a particular
function call to be unambiguous, and so forth. The other kind of constraint we
call semantic constraints. These constraints are
much harder to verify mechanically. In the general case, it may not even be
practical to do so. For example, we may require that there be a <
operator defined on a template type parameter (which is a syntactic constraint),
but usually we'll also require that the operator actually defines some sort of
ordering on its domain (which is a semantic constraint).
The term concept is often used
to denote a set of constraints that is repeatedly required in a template
library. For example, the C++ standard library relies on such concepts as random access iterator and default constructible. Concepts can form hierarchies in
the sense that one concept can be a refinement of another. The more refined
concept includes all the constraints of the other concept but adds a few more.
For example, the concept random access iterator
refines the concept bidirectional iterator in the
C++ standard library. With this terminology in place, we can say that debugging
template code includes a significant amount of determining how concepts are
violated in the template implementation and in their use.
Decoding the Error Novel
Ordinary compilation errors are normally quite succinct and to
the point. For example, when a compiler says "class X has no member
'fun'," it usually isn't too hard to figure out what is wrong in our code
(for example, we might have mistyped run as fun). Not so with
templates. Consider the following relatively simple code excerpt using the C++
standard library. It contains a fairly small mistake:
list<string> is used, but we are searching using a
greater<int> function object, which should have been a
greater<string> object:
std::list<std::string> coll;
This sort of mistake commonly happens when cutting and pasting
some code and forgetting to adapt parts of it.
A version of the popular GNU C++ compiler reports the following
error:
/local/include/stl/_algo.h: In function 'struct _STL::_List_iterator<_STL::basic
_string<char,_STL::char_traits<char>,_STL::allocator<char> >,_STL::_Nonconst_tra
its<_STL::basic_string<char,_STL::char_traits<char>,_STL::allocator<char> > > >
_STL::find_if<_STL::_List_iterator<_STL::basic_string<char,_STL::char_traits<cha
r>,_STL::allocator<char> >,_STL::_Nonconst_traits<_STL::basic_string<char,_STL::
char_traits<char>,_STL::allocator<char> > > >, _STL::binder2nd<_STL::greater<int
> > >(_STL::_List_iterator<_STL::basic_string<char,_STL::char_traits<char>,_STL:
:allocator<char> >,_STL::_Nonconst_traits<_STL::basic_string<char,_STL::char_tra
its<char>,_STL::allocator<char> > > >, _STL::_List_iterator<_STL::basic_string<c
har,_STL::char_traits<char>,_STL::allocator<char> >,_STL::_Nonconst_traits<_STL:
:basic_string<char,_STL::char_traits<char>,_STL::allocator<char> > > >, _STL::bi
nder2nd<_STL::greater<int> >, _STL::input_iterator_tag)':
/local/include/stl/_algo.h:115: instantiated from '_STL::find_if<_STL::_List_i
terator<_STL::basic_string<char,_STL::char_traits<char>,_STL::allocator<char> >,
_STL::_Nonconst_traits<_STL::basic_string<char,_STL::char_traits<char>,_STL::all
ocator<char> > > >, _STL::binder2nd<_STL::greater<int> > >(_STL::_List_iterator<
_STL::basic_string<char,_STL::char_traits<char>,_STL::allocator<char> >,_STL::_N
onconst_traits<_STL::basic_string<char,_STL::char_traits<char>,_STL::allocator<c
har> > > >, _STL::_List_iterator<_STL::basic_string<char,_STL::char_traits<char>
,_STL::allocator<char> >,_STL::_Nonconst_traits<_STL::basic_string<char,_STL::ch
ar_traits<char>,_STL::allocator<char> > > >, _STL::binder2nd<_STL::greater<int>
>)'
testprog.cpp:18: instantiated from here
/local/include/stl/_algo.h:78: no match for call to '(_STL::binder2nd<_STL::grea
ter<int> >) (_STL::basic_string<char,_STL::char_traits<char>,_STL::allocator<cha
r> > &)'
/local/include/stl/_function.h:261: candidates are: bool _STL::binder2nd<_STL::g
reater<int> >::operator ()(const int &) const
A message like this starts looking more like a novel than a
diagnostic. It can also be overwhelming to the point of discouraging novice
template users. However, with some practice, messages like this become
manageable, and the errors are relatively easily located.
The first part of this error message says that an error
occurred in a function template instance (with a horribly long name) deep inside
the /local/include/stl/_algo.h header. Next, the compiler reports why
it instantiated that particular instance. In this case it all started on line 18
of testprog.cpp (which is the file containing our example code), which
caused the instantiation of a find_if template on line 115 of the
_algo.h header. The compiler reports all this in case we simply were
not expecting all these templates to be instantiated. It allows us to determine
the chain of events that caused the instantiations.
However, in our example we're willing to believe that all kinds
of templates needed to be instantiated, and we just wonder why it didn't work.
This information comes in the last part of the message: The part that says
"no match for call" implies that a function call could not be resolved
because the types of the arguments and the parameter types didn't match.
Furthermore, just after this, the line containing "candidates are"
explains that there was a single candidate type expecting an integer type
(parameter type const int&). Looking back at line 18 of the
program, we see std::bind2nd(std::greater<int>(),"A"), which does
indeed contain an integer type (<int>) that is not compatible
with the string type objects for which we're looking in our example. Replacing
<int> with <std::string> makes the problem go
away.
There is no doubt that the error message could be better
structured. The actual problem could be omitted before the history of the
instantiation, and instead of using fully expanded template instantiation names
like MyTemplate<YourTemplate<int> >, decomposing the
instance as in MyTemplate<T> with T=YourTemplate<int> can
reduce the overwhelming length of names. However, it is also true that all the
information in this diagnostic could be useful in some situations. It is
therefore not surprising that other compilers provide similar information
(although some use the structuring techniques mentioned).
Shallow Instantiation
Diagnostics such as those discussed earlier arise when errors
are found after a long chain of instantiations. To illustrate this, consider the
following somewhat contrived code:
This example illustrates the typical layering of software
development: High-level function templates like shell() rely on
components like middle(), which themselves make use of basic facilities
like core(). When we instantiate shell(), all the layers below
it also need to be instantiated. In this example, a problem is revealed in the
deepest layer: core() is instantiated with type int (from the
use of Client::Index in middle()) and attempts to dereference
a value of that type, which is an error. A good generic diagnostic includes a
trace of all the layers that led to the problems, but we observe that so much
information can appear unwieldy.
An excellent discussion of the core ideas surrounding this
problem can be found in [StroustrupDnE], in which
Bjarne Stroustrup identifies two classes of approaches to determine earlier
whether template arguments satisfy a set of constraints: through a language
extension or through earlier parameter use. The latter alternative consists of forcing any errors in shallow instantiations. This is achieved by inserting
unused code with no other purpose than to trigger an error if that code is
instantiated with template arguments that do not meet the requirements of deeper
levels of templates.
In our previous example we could add code in shell()
that attempts to dereference a value of type T::Index. For example:
If T is a type such that T::Index cannot be
dereferenced, an error is now diagnosed on the local class
ShallowChecks. Note that because the local class is not actually used,
the added code does not impact the running time of the shell()
function. Unfortunately, many compilers will warn about the fact that
ShallowChecks is not used (and neither are its members). Tricks such as
the use of the ignore() template can be used to inhibit such warnings,
but they add to the complexity of the code.
Clearly, the development of the dummy code in our example can
become as complex as the code that implements the actual functionality of the
template. To control this complexity it is natural to attempt to collect various
snippets of dummy code in some sort of library. For example, such a library
could contain macros that expand to code that triggers the appropriate error
when a template parameter substitution violates the concept underlying that
particular parameter. The most popular such library is the Concept Check Library, which is part of the Boost
distribution.
Unfortunately, the technique isn't particularly portable (the
way errors are diagnosed differs considerably from one compiler to another) and
sometimes masks issues that cannot be captured at a higher level.
Long Symbols
There is an another problem
of templates: Instantiated template code can result in very long symbols. For
example, in the implementation used earlier std::string is expanded
to
Some programs that use the C++ standard library produce symbols
that contain more than 10,000 characters. These very long symbols can also cause
errors or warnings in compilers, linkers, and debuggers. Modern compilers use
compression techniques to reduce this problem, but in error messages this is not
apparent.
Tracers
So far we have discussed bugs that arise when compiling or
linking programs that contain templates. However, the most challenging task of
ensuring that a program behaves correctly at run time often follows a successful build. Templates can sometimes
make this task a little more difficult because the behavior of generic code
represented by a template depends uniquely on the client of that template
(certainly much more so than ordinary classes and functions). A tracer is a
software device that can alleviate that aspect of debugging by detecting
problems in template definitions early in the development cycle.
A tracer is a user-defined class that can be used as an
argument for a template to be tested. Often, it is written just to meet the
requirements of the template and no more than those requirements. More
important, however, a tracer should generate a trace of the operations that are invoked on it. This
allows, for example, to verify experimentally the efficiency of algorithms as
well as the sequence of operations.
Here is an example of a tracer that might be used to test a
sorting algorithm:
In addition to the value to sort, value, the tracer
provides several members to trace an actual sort: generation traces for
each object how many copies it is from the original. The other static members
trace the number of creations (constructor calls), destructions, assignment
comparisons, and the maximum number of objects that ever existed.
The static members are defined in a separate dot-C file:
This particular tracer allows us to track the pattern or entity
creation and destruction as well as assignments and comparisons performed by a
given template. The following test program illustrates this for the
std::sort algorithm of the C++ standard library:
Running this program creates a considerable amount of output,
but much can be concluded from the "final report." For one implementation of the
std::sort() function, we find the following:
For example, we see that although 15 temporary tracers were
created in our program while sorting, at most two additional tracers existed at
any one time.
Our tracer thus fulfills two roles: It proves that the standard
sort() algorithm requires no more functionality than our tracer (for
example, operators == and > were not needed), and it gives
us a sense of the cost of the algorithm. It does not, however, reveal much about
the correctness of the sorting template.
Oracles
Tracers are relatively simple and effective, but they allow us
to trace the execution of templates only for specific input data and for a
specific behavior of its related functionality. We may wonder, for example, what
conditions must be met by the comparison operator for the sorting algorithm to
be meaningful (or correct), but in our example we have only tested a comparison
operator that behaves exactly like less-than for integers.
An extension of tracers is known in some circles as oracles (or run-time analysis
oracles). They are tracers that are connected to a so-called inference engine—a program that can remember assertions
and reasons about them to infer certain conclusions. One such system that was
applied to certain parts of a standard library implementation is called MELAS and is discussed in [MusserWangDynaVeri].
One author, David Musser, was also a key figure in the development of the C++ standard library. Among other things, he designed and implemented the first associative containers.
Oracles allow us, in some cases, to verify template algorithms
dynamically without fully specifying the substituting template arguments (the
oracles are the arguments) or the input data (the inference engine may request
some sort of input assumption when it gets stuck). However, the complexity of
the algorithms that can be analyzed in this way is still modest (because of the
limitations of the inference engines), and the amount of work is considerable.
For these reasons, we do not delve into the development of oracles, but the
interested reader should examine the publication mentioned earlier (and the
references contained therein).
Archetypes
We mentioned earlier that tracers often provide an interface
that is the minimal requirement of the template they trace. When such a minimal
tracer does not generate run-time output, it is sometimes called an archetype. An archetype allows us to verify that a
template implementation does not require more syntactic constraints than
intended. Typically, a template implementer will want to develop an archetype
for every concept identified in the template library.
-----------------------------------------------------------------
See Also:
-----------------------------------------------------------------
See Also:
-----------------------------------------------------------------
- Complete Tutorial of C++ Template's
- Standard Template Library Tutorial
- Inter Process Communication Tutorial
- Advance Programming in C & C++
-----------------------------------------------------------------
No comments:
Post a Comment