WIP on functiontemplates

git-svn-id: https://cppannotations.svn.sourceforge.net/svnroot/cppannotations/trunk@519 f6dd340e-d3f9-0310-b409-bdd246841980
This commit is contained in:
Frank B. Brokken 2011-02-25 10:39:57 +00:00
parent 2ec3a82712
commit 8da6f113a1
15 changed files with 479 additions and 443 deletions

View file

@ -6,6 +6,9 @@ includefile(functiontemplates/intro)
lsect(TEMPFUNDEF)(Defining function templates)
includefile(functiontemplates/definitions)
subsect(Considerations regarding template parameters)
includefile(functiontemplates/considerations)
subsect(Late-specified return type (C++0x))
includefile(functiontemplates/alternate)
@ -39,12 +42,24 @@ includefile(functiontemplates/declarations)
lsect(TEMPFUNINST)(Instantiating function templates)
includefile(functiontemplates/instantiations)
subsect(Instantiations: no `code bloat')
includefile(functiontemplates/inocodebloat)
lsect(TEMPFUNEXPLICIT)(Using explicit template types)
includefile(functiontemplates/explicit)
sect(Overloading function templates)
includefile(functiontemplates/overloading)
subsect(An example using overloaded function templates)
includefile(functiontemplates/example)
subsect(Ambiguities when overloading function templates)
includefile(functiontemplates/ambiguities)
subsect(Declaring overloaded function templates)
includefile(functiontemplates/declaring)
lsect(SPECIALIZING)(Specializing templates for deviating types)
includefile(functiontemplates/specialization)

View file

@ -0,0 +1,80 @@
Although it em(is) possible to define another function template tt(add) this
will introduce an ambiguity as the compiler won't be able to choose which of
the two overloaded versions defining two differently typed function parameters
to use. For example when defining:
verb(
#include "add.h"
template <typename T1, typename T2>
T1 add(T1 const &lvalue, T2 const &rvalue)
{
return lvalue + rvalue;
}
int main()
{
add(3, 4.5);
}
)
the compiler will report an ambiguity like the following:
verb(
error: call of overloaded `add(int, double)' is ambiguous
error: candidates are: Type add(const Container&, const Type&)
[with Container = int, Type = double]
error: T1 add(const T1&, const T2&)
[with T1 = int, T2 = double]
)
Now recall the overloaded function template accepting three arguments:
verb(
template <typename Type>
Type add(Type const &lvalue, Type const &mvalue, Type const &rvalue)
{
return lvalue + mvalue + rvalue;
}
)
It may be considered as a disadvantage that only equally typed arguments
are accepted by this function (three tt(int)s, three tt(double)s, etc.). To
remedy this we define yet another overloaded function template, this time
accepting arguments of any type. This function template can only be used if
tt(operator+) is defined between the function's actually used types, but apart
from that there appears to be no problem. Here is the overloaded version
accepting arguments of any type:
verb(
template <typename Type1, typename Type2, typename Type3>
Type1 add(Type1 const &lvalue, Type2 const &mvalue, Type3 const &rvalue)
{
return lvalue + mvalue + rvalue;
}
)
Now that we've defined the above two overloaded function templates
expecting three arguments let's call tt(add) as follows:
verb(
add(1, 2, 3);
)
Should we expect an ambiguity here? After all, the compiler might select
the former function, deducing that tt(Type == int), but it might also select
the latter function, deducing that tt(Type1 == int, Type2 == int) and tt(Type3
== int). Remarkably, the compiler reports no ambiguity.
No ambiguity is reported because of the following. If overloaded template
functions are defined using em(less) and em(more) specialized template type
parameters (e.g., less specialized: all types different vs. more specialized:
hi(function templates: specialized type parameters) all types equal)
then the compiler selects the more specialized function whenever possible.
As a i(rule of thumb): overloaded function templates must allow a unique
combination of template type arguments to be specified to prevent ambiguities
when selecting which overloaded function template to instantiate. The
em(ordering) of template type parameters in the function template's type
parameter list is not important. E.g., trying to instantiate one of the
following function templates results in an ambiguity:
verb(
template <typename T1, typename T2>
void binarg(T1 const &first, T2 const &second)
{}
template <typename T1, typename T2>
void binarg(T2 const &first, T1 const &second)
{}
)
This should not come as a surprise. After all, template type parameters
are just formal names. Their names (tt(T1), tt(T2) or tt(Whatever)) have no
concrete meanings.

View file

@ -1,7 +1,7 @@
Although the em(construction) of class templates is the topic of chapter
ref(TEMPCLASS) class templates have already extensively been em(used)
earlier. For example, abstract containers (cf. chapter ref(CONTAINERS)) are
defined as class templates. Class templates can, like
ref(TEMPCLASS), we've already extensively em(used) class templates before. For
example, abstract containers (cf. chapter ref(CONTAINERS)) are defined as
class templates. Class templates can, like
hi(ordinary class)hi(class: ordinary)
ordinary classes, participate in the construction of class hierarchies.
@ -52,7 +52,7 @@ Caseless) could be constructed as follows:
hi(transformation to a base class)
hi(class template: transformation to a base class)
em(transformation to a base class instantiated from a class template), the
function template tt(sortVectors()) may now also be used to sort tt(Vector)
function template tt(sortVector) may now also be used to sort tt(Vector)
objects. For example:
verb(
int main()

View file

@ -0,0 +1,193 @@
We've managed to design our first function template:
verbinclude(examples/add.h)
Look again at tt(add)'s parameters. By specifying tt(Type const &)
rather than tt(Type) superfluous copying is prevented, at the same time
allowing values of primitive types to be passed as arguments to the
function. So, when tt(add(3, 4)) is called, tt(int(4)) will be assigned to
tt(Type const &rvalue). In general, function parameters should be defined as
tt(Type const &) to prevent unnecessary copying. The compiler is smart enough
to handle `references to references' in this case, which is something the
language normally does not support. For example, consider the following
tt(main) function (here and in the following simple examples it is assumed
that the template and the required headers and namespace declarations have
been provided):
verb(
int main()
{
size_t const &var = size_t(4);
cout << add(var, var) << '\n';
}
)
Here tt(var) is a reference to a constant tt(size_t). It is passed as
argument to tt(add), thereby initializing tt(lvalue) and tt(rvalue) as tt(Type
const &) to tt(size_t const &) values. The compiler interprets tt(Type) as
tt(size_t). Alternatively, the parameters might have been specified using
tt(Type &), rather than tt(Type const &). The disadvantage of this (non-const)
specification being that temporary values cannot be passed to the function
anymore. The following therefore fails to compile:
verb(
int main()
{
cout << add(string("a"), string("b")) << '\n';
}
)
Here, a tt(string const &) cannot be used to initialize a tt(string &).
Had tt(add) define tt(Type &&) parameters then the above program would have
compiled just fine. In addition the following example correctly compiles as
the compiler decides that tt(Type) apparently is a tt(string const):
verb(
int main()
{
string const &s = string("a");
cout << add(s, s) << '\n';
}
)
What can we deduce from these examples?
itemization(
it() In general, function parameters should be specified as tt(Type const
&) parameters to prevent unnecessary copying.
it() The template mechanism is fairly flexible. It will interpret
formal types as plain types, const types, pointer types, etc., depending on
the actually provided types. The i(rule of thumb) is that the formal type is
used as a generic mask for the actual type, with the formal type name covering
whatever part of the actual type must be covered. Some examples, assuming the
parameter is defined as tt(Type const &):
center(
table(2)(ll)(
rowline()
row(
cell(bf(Provided argument:)) cell(bf(Actually used Type:))
)
rowline()
row(
cell(tt(size_t const)) cell(tt(size_t))
)
row(
cell(tt(size_t)) cell(tt(size_t))
)
row(
cell(tt(size_t *)) cell(tt(size_t *))
)
row(
cell(tt(size_t const *)) cell(tt(size_t const *))
)
rowline()
)
)
)
As a second example of a function template, consider the following
function template:
verb(
template <typename Type, size_t Size>
Type sum(Type const (&array)[Size])
{
Type tp = Type();
for (size_t idx = 0; idx < Size; idx++)
tp += array[idx];
return tp;
}
)
This template definition introduces the following new concepts and
features:
itemization(
it() The emi(template parameter list). This template parameter list has
two elements. The first element is a well-known template type parameter, but
the second element has a very specific type: a tt(size_t). Template parameters
of specific (i.e., non-formal) types used in template parameter lists are
called
hi(template non-type parameter)em(template non-type parameters).
A template non-type parameter defines the type of a
i(constant expression), which must be known by the time the template is
instantiated and which is specified in terms of existing types, such as a
tt(size_t).
it() Looking at the function's head, we see one parameter:
verb(
Type const (&array)[Size]
)
This parameter defines tt(array) as a reference to an array having
tt(Size) elements of type tt(Type) that may not be modified.
it() In the parameter definition, both tt(Type) and tt(Size) are
used. tt(Type) is of course the template's type parameter tt(Type), but
tt(Size) is also a template parameter. It is a tt(size_t), whose value must be
inferable by the compiler when it compiles an actual call of the tt(sum)
function template. Consequently, tt(Size) must be a tt(const) value. Such a
constant expression is called a em(template non-type parameter), and its type
is named in the template's parameter list.
it() When the function template is called, the compiler must be able to
infer not only tt(Type)'s concrete value, but also tt(Size)'s value. Since
the function tt(sum) only has one parameter, the compiler is only able to
infer tt(Size)'s value from the function's actual argument. It can do so if
the provided argument is an array (of known and fixed size) rather
than a pointer to tt(Type) elements. So, in the following tt(main) function
the first statement will compile correctly but the second statement
will not:
verb(
int main()
{
int values[5];
int *ip = values;
cout << sum(values) << '\n'; // compiles OK
cout << sum(ip) << '\n'; // won't compile
}
)
it() Inside the function's body the statement tt(Type tp = Type()) is used
to initialize tt(tp) to a default value. Note here that no fixed value (like
0) is used. Any type's default value may be obtained using its default
constructor
hi(template type: initialization)
rather than using a fixed numeric value. Of course, not every class
accepts a numeric value as an argument to one of its constructors. But all
types, even the primitive types, support default constructors (actually, some
classes do not implement a default constructor, or make it inaccessible; but
most do). The default constructor hi(constructor: primitive type) of primitive
types will initialize their variables to 0 (or tt(false)). Furthermore, the
statement tt(Type tp = Type()) is a true initialization: tt(tp) is initialized
by tt(Type)'s default constructor, rather than using tt(Type)'s copy
constructor to assign tt(Type)'s copy to tt(tp).
It's interesting to note here (although not directly related to the
current topic) that the syntactic construction tt(Type tp(Type())) em(cannot)
be used, even though it also looks like a proper initialization. Usually an
initializing argument can be provided to an object's definition, like
tt(string s("hello")). Why, then, is tt(Type tp = Type()) accepted, whereas
tt(Type tp(Type())) isn't? When tt(Type tp(Type())) is used it won't result
in an error message. So we don't immediately detect that it's em(not) a
tt(Type) object's default initialization. Instead, the compiler will start
generating error messages once tt(tp) is used. This is caused by the fact that
in bf(C++) (and in bf(C) alike) the compiler will do its best to recognize a
function or
hi(C++: function prevalence rule) function pointer whenever possible: the
emi(function prevalence rule). According to this rule tt(Type()) is (because
of the pair of parentheses) interpreted as a em(pointer to a function)
expecting no arguments; returning a tt(Type). It will do so unless the
compiler clearly is unable to do so. In the initialization tt(Type tp =
Type()) it em(can't) see a pointer to a function as a tt(Type) object cannot
be given the value of a function pointer (remember: tt(Type()) is interpreted
as tt(Type (*)()) whenever possible). But in tt(Type tp(Type())) it em(can)
use the pointer interpretation: tt(tp) is now em(declared) as a em(function)
expecting a pointer to a function returning a tt(Type), with tt(tp) itself
also returning a tt(Type). E.g., tt(tp) could have been defined as:
verb(
Type tp(Type (*funPtr)())
{
return (*funPtr)();
}
)
it() Comparable to the first function template, tt(sum) also assumes the
existence of certain public members in tt(Type)'s class. This time
tt(operator+=) and tt(Type)'s copy constructor.
)
Like class definitions, template definitions should not contain tt(using)
hi(templates vs. using)hi(using vs. templates)
directives or declarations: the template might be used in a situation
where such a directive overrides the programmer's intentions: ambiguities or
other conflicts may result from the template's author and the programmer using
different tt(using) directives (E.g, a tt(cout) variable defined in the
tt(std) namespace and in the programmer's own namespace). Instead, within
template definitions only hi(fully qualified name)hi(name: fully qualified)
fully qualified names, including all required namespace specifications should
be used.

View file

@ -12,18 +12,30 @@ deduced to be an tt(int), rather than an tt(int &&).
This is fairly intuitive. But what happens if the actual type is tt(int &)?
There is no such thing as an tt(int & &&param) and so the compiler contracts
the double reference by removing the rvalue reference, keeping the lvalue
reference. Here a simple rule is followed: Any reference to an lvalue
reference results in an lvalue reference and emi(vice versa): an lvalue to any
reference also results in an lvalue:
reference. Here the following rules are applied:
quote(
1. A template function parameter defined as an lvalue reference (e.g.,
tt(Type &)) receiving an lvalue reference argument results in a single
lvalue reference.
2. A template function parameter defined as an rvalue reference (e.g.,
tt(Type &&)) receiving any kind of reference argument uses the reference
type of the argument.
)
Examples:
itemization(
it() if tt(Type) is tt(Actual &) then tt(Type &) is tt(Actual &) and
tt(Type == Actual);
it() if tt(Type) is tt(Actual &) then tt(Type &&) still is tt(Actual &)
and tt(Type == Actual);
it() if tt(Type) is tt(Actual &&) then tt(Type &) again is tt(Actual &)
and tt(Type == Actual);
it() if tt(Type) is tt(Actual &&) then tt(Type &&) is tt(Actual &&)
and tt(Type == Actual);
it() When providing an tt(Actual &) argument then tt(Type &) becomes an
tt(Actual &) and tt(Type) is inferred as tt(Actual);
it() When providing an tt(Actual &) then tt(Type &&) becomes an
tt(Actual &) and tt(Type) is inferred as tt(Actual);
it() When providing an tt(Actual &&) then tt(Type &) also becomes
tt(Actual &) and tt(Type) is inferred as tt(Actual);
it() When providing an tt(Actual &&) then tt(Type &&) becomes tt(Actual
&&) and tt(Type) is inferred as tt(Actual);
)
Let's look at a concrete exampe where contraction occurs. Consider the
@ -38,18 +50,18 @@ references to some template type parameter:
)
In this situation, when tt(function) is called with an (lvalue) argument of
type tt(TP &) the template type parameter tt(Type) is deduced to be tt(Tp
&). Therefore, tt(param) is considered to be instantiated as tt(Tp &param) (as
tt(Tp & &&param)) is contracted as tt(Tp &param). But with an rvalue typed
argument (tt(Tp &&)) it results in tt(Tp &&param) (again using contraction).
&). Therefore, tt(Type &&param) is instantiated as tt(Tp &param), tt(Type)
becomes tt(Tp) and the rvalue reference is replaced by an lvalue reference.
Likewise, when tt(callee) is called using the tt(static_cast) the same
contraction occurs, so if tt(Type &&param) is interpreted as tt(Tp &param) the
static cast em(also) uses type tt(Tp &param). If (Type &&param) is interpreted
as tt(Tp &&param) the static cast em(also) uses type tt(Tp &&param).
contraction occurs, so tt(Type &&param) operates on tt(Tp &param). Therefore
(using contraction) the static cast em(also) uses type tt(Tp &param). If
tt(param) happened to be of type tt(Tp &&) then the static cast uses type
tt(Tp &&param).
This characteristics allows us to pass a function argument straight through to
This characteristic allows us to pass a function argument to
a nested function em(without) changing its type: lvalues remain lvalues,
rvalues remain rvalues. This characteristic is therefore also known as
emi(perfect forwarding) which will be discussed in greated detail in section
emi(perfect forwarding) which is discussed in greated detail in section
ref(PERFECT). Perfect forwarding prevents the template author from having to
define multiply overloaded versions of a function template.

View file

@ -18,7 +18,7 @@ computers keep getting faster and faster. What was a nuisance a few years ago
is hardly noticeable today.
it() Every time a function template is instantiated, its code appears in
the resulting object module. However, if multiple instantiations of a template
using the same actual types for its template parameter exist in multiple
using the same actual types for its template parameters exist in multiple
object files the emi(one definition rule) is lifted. The linker will weed out
superfluous instantiations (i.e., identical definitions of instantiated
templates). In the final program only one instantiation for a particular set

View file

@ -0,0 +1,21 @@
Like any function, overloaded functions may be declared, either using
plain declarations or instantiation declarations. Explicit template argument
types may also be used. Example:
itemization(
it() To declare a function template tt(add) accepting certain containers:
verb(
template <typename Container, typename Type>
Type add(Container const &container, Type const &init);
)
it() to use an instantiation declaration (in which case the compiler must
already have seen the template's definition):
verb(
template int add<std::vector<int>, int>
(std::vector<int> const &vect, int const &init);
)
it() to use explicit template type arguments:
verb(
std::vector<int> vi;
int sum = add<std::vector<int>, int>(vi, 0);
)
)

View file

@ -11,18 +11,17 @@ function tt(add) expects two tt(Type) arguments and returns their sum:
return lvalue + rvalue;
}
)
Note how closely the above function's definition follows its description:
it gets two arguments, and returns its sum. Now consider what would happen if
we would have defined this function for, e.g., tt(int) values. We would have
written:
Note how closely the above function's definition follows its description.
It receives two arguments, and returns its sum. Now consider what would happen
if we defined this function for, e.g., tt(int) values. We would write:
verb(
int add(int const &lvalue, int const &rvalue)
{
return lvalue + rvalue;
}
)
So far, so good. However, were we to add two doubles, we would have
overloaded this function:
So far, so good. However, were we to add two doubles, we would overload
this function:
verb(
double add(double const &lvalue, double const &rvalue)
{
@ -37,10 +36,11 @@ basically the same function are required because of the strongly typed nature
of bf(C++). Because of this, a truly generic function cannot be constructed
without resorting to the i(template mechanism).
Fortunately, we've already seen a large part of a template function. Our
initial function tt(add) actually is an implementation of such a function
although it isn't a full template definition yet. If we would give the first
tt(add) function to the compiler, it would produce an error message like:
Fortunately, we've already seen an important part of a template
function. Our initial function tt(add) actually is an implementation of such a
function although it isn't a full template definition yet. If we gave the
first tt(add) function to the compiler, it would produce an error message
like:
verb(
error: `Type' was not declared in this scope
error: parse error before `const'
@ -52,7 +52,7 @@ em(formal) typename. Comparing it to the alternate implementations, it will be
clear that we could have changed tt(Type) into tt(int) to get the first
implementation, and into tt(double) to get the second.
The full template definition allows for this formal character of the
The full template definition allows for this formal nature of the
tt(Type) typename. Using the keyword tt(template), we prefix one line to
our initial definition, obtaining the following function template
definition:
@ -124,191 +124,3 @@ available for streams.
Within the template definition's scope formal type names overrule identically
named identifiers of broader scopes.
Look again at the tt(add)'s parameters. By specifying tt(Type const &) rather
than tt(Type) superfluous copying is prevented, at the same time allowing
values of primitive types to be passed as arguments to the function. So, when
tt(add(3, 4)) is called, tt(int(4)) will be assigned to tt(Type const
&rvalue). In general, function parameters should be defined as tt(Type const
&) to prevent unnecessary copying. The compiler is smart enough to handle
`references to references' in this case, which is something the language
normally does not support. For example, consider the following tt(main)
function (here and in the following simple examples it is assumed that the
template and the required headers and namespace declarations have been
provided):
verb(
int main()
{
size_t const &var = size_t(4);
cout << add(var, var) << '\n';
}
)
Here tt(var) is a reference to a constant tt(size_t). It is passed as
argument to tt(add), thereby initializing tt(lvalue) and tt(rvalue) as tt(Type
const &) to tt(size_t const &) values. The compiler interpreting tt(Type) as
tt(size_t). Alternatively, the parameters might have been specified using
tt(Type &), rather than tt(Type const &). The disadvantage of this (non-const)
specification being that temporary values cannot be passed to the function
anymore. The following therefore fails to compile:
verb(
int main()
{
cout << add(string("a"), string("b")) << '\n';
}
)
Here, a tt(string const &) cannot be used to initialize a tt(string &).
If tt(add) would define tt(Type &&) parameters the above program em(would)
compile just fine. In addition the following will compile as the compiler
decides that tt(Type) apparently is a tt(string const):
verb(
int main()
{
string const &s = string("a");
cout << add(s, s) << '\n';
}
)
What can we deduce from these examples?
itemization(
it() In general, function parameters should be specified as tt(Type const
&) parameters to prevent unnecessary copying.
it() The template mechanism is fairly flexible. It will interpret
formal types as plain types, const types, pointer types, etc., depending on
the actually provided types. The i(rule of thumb) is that the formal type is
used as a generic mask for the actual type, with the formal type name covering
whatever part of the actual type must be covered. Some examples, assuming the
parameter is defined as tt(Type const &):
center(
table(2)(ll)(
row(
cell(bf(argument type)) cell(bf(Type ==))
)
row(
cell(tt(size_t const)) cell(tt(size_t))
)
row(
cell(tt(size_t)) cell(tt(size_t))
)
row(
cell(tt(size_t *)) cell(tt(size_t *))
)
row(
cell(tt(size_t const *)) cell(tt(size_t const *))
)
)
)
)
As a second example of a function template, consider the following
function template:
verb(
template <typename Type, size_t Size>
Type sum(Type const (&array)[Size])
{
Type tp = Type();
for (size_t idx = 0; idx < Size; idx++)
tp += array[idx];
return tp;
}
)
This template definition introduces the following new concepts and
features:
itemization(
it() The emi(template parameter list). This template parameter list has
two elements. The first element is a well-known template type parameter, but
the second element has a very specific type: a tt(size_t). Template parameters
of specific (i.e., non-formal) types used in template parameter lists are
called
hi(template non-type parameter)em(template non-type parameters).
A template non-type parameter defines the type of a
i(constant expression), which must be known by the time the template is
instantiated and which is specified in terms of existing types, such as a
tt(size_t).
it() Looking at the function's head, we see one parameter:
verb(
Type const (&array)[Size]
)
This parameter defines tt(array) as a reference to an array having
tt(Size) elements of type tt(Type) that may not be modified.
it() In the parameter definition, both tt(Type) and tt(Size) are
used. tt(Type) is of course the template's type parameter tt(Type), but
tt(Size) is also a template parameter. It is a tt(size_t), whose value must be
inferable by the compiler when it compiles an actual call of the tt(sum)
function template. Consequently, tt(Size) must be a tt(const) value. Such a
constant expression is called a em(template non-type parameter), and its type
is named in the template's parameter list.
it() When the function template is called, the compiler must be able to
infer not only tt(Type)'s concrete value, but also tt(Size)'s value. Since
the function tt(sum) only has one parameter, the compiler is only able to
infer tt(Size)'s value from the function's actual argument. It can do so if
the provided argument is an array (of known and fixed size) rather
than a pointer to tt(Type) elements. So, in the following tt(main) function
the first statement will compile correctly but the second statement
will not:
verb(
int main()
{
int values[5];
int *ip = values;
cout << sum(values) << '\n'; // compiles OK
cout << sum(ip) << '\n'; // won't compile
}
)
it() Inside the function's body the statement tt(Type tp = Type()) is used
to initialize tt(tp) to a default value. Note here that no fixed value (like
0) is used. Any type's default value may be obtained using its default
constructor
hi(template type: initialization)
rather than using a fixed numeric value. Of course, not every class
accepts a numeric value as an argument to one of its constructors. But all
types, even the primitive types, support default constructors (actually, some
classes do not implement a default constructor, or make it inaccessible; but
most do). The default constructor hi(constructor: primitive type) of primitive
types will initialize their variables to 0 (or tt(false)). Furthermore, the
statement tt(Type tp = Type()) is a true initialization: tt(tp) is initialized
by tt(Type)'s default constructor, rather than using tt(Type)'s copy
constructor to assign tt(Type)'s copy to tt(tp).
It's interesting to note here (although not directly related to the
current topic) that the syntactic construction tt(Type tp(Type())) em(cannot)
be used, even though it also looks like a proper initialization. Usually an
initializing argument can be provided to an object's definition, like
tt(string s("hello")). Why, then, is tt(Type tp = Type()) accepted, whereas
tt(Type tp(Type())) isn't? When tt(Type tp(Type())) is used it won't result
in an error message. So we don't immediately detect that it's em(not) a
tt(Type) object's default initialization. Instead, the compiler will start
generating error messages once tt(tp) is used. This is caused by the fact that
in bf(C++) (and in bf(C) alike) the compiler will do its best to recognize a
function or
hi(C++: function prevalence rule) function pointer whenever possible: the
emi(function prevalence rule). According to this rule tt(Type()) is (because
of the pair of parentheses) interpreted as a em(pointer to a function)
expecting no arguments; returning a tt(Type). It will do so unless the
compiler clearly is unable to do so. In the initialization tt(Type tp =
Type()) it em(can't) see a pointer to a function as a tt(Type) object cannot
be given the value of a function pointer (remember: tt(Type()) is interpreted
as tt(Type (*)()) whenever possible). But in tt(Type tp(Type())) it em(can)
use the pointer interpretation: tt(tp) is now em(declared) as a em(function)
expecting a pointer to a function returning a tt(Type), with tt(t) itself also
returning a tt(Type). E.g., tt(t) could have been defined as:
verb(
Type t(Type (*tp)())
{
return (*tp)();
}
)
it() Comparable to the first function template, tt(sum) also assumes the
existence of certain public members in tt(Type)'s class. This time
tt(operator+=) and tt(Type)'s copy constructor.
)
Like class definitions, template definitions should not contain tt(using)
hi(templates vs. using)hi(using vs. templates)
directives or declarations: the template might be used in a situation
where such a directive overrides the programmer's intentions: ambiguities or
other conflicts may result from the template's author and the programmer using
different tt(using) directives (E.g, a tt(cout) variable defined in the
tt(std) namespace and in the programmer's own namespace). Instead, within
template definitions only hi(fully qualified name)hi(name: fully qualified)
fully qualified names, including all required namespace specifications should
be used.

View file

@ -0,0 +1,38 @@
With all these overloaded versions in place, we may now start the compiler
to compile the following function:
verb(
using namespace std;
int main()
{
vector<int> v;
add(3, 4); // 1 (see text)
add(v); // 2
add(v, 0); // 3
}
)
itemization(
it() In statement 1 the compiler recognizes two identical types, both
tt(int). It will therefore instantiate tt(add<int>), our very first definition
of the tt(add) template.
it() In statement 2 a single argument is used. Consequently, the compiler
looks for an overloaded version of tt(add) requiring but one argument. It
finds the overloaded function template expecting a tt(std::vector), deducing
that the template's type parameter must be tt(int). It instantiates
verb(
add<int>(std::vector<int> const &)
)
it() In statement 3 the compiler again encounters an argument list having
two arguments. However, this time the types of the arguments aren't equal,
so tt(add) template's first definition can't be used. But it em(can) use the
last definition, expecting entities having different types. As a
tt(std::vector) supports tt(begin) and tt(end), the compiler is now able to
instantiate the function template
verb(
add<std::vector<int>, int>(std::vector<int> const &, int const &)
)
)
Having defined the tt(add) function template for two equal and two
different template type parameters we've exhausted the possibilities for using
an tt(add) function template having two template type parameters.

View file

@ -0,0 +1,11 @@
#include <cmath>
template<typename Type>
void call(Type (*fp)(Type), Type const &value)
{
(*fp)(value);
}
int main()
{
call(sqrt, 2.0);
}

View file

@ -45,51 +45,3 @@ alternatives, all of them possible and available):
But it's good practice to avoid type casts wherever possible. How to do
this is explained in the next section (ref(TEMPFUNEXPLICIT)).
As mentioned in section ref(TEMPFUNDECL), the linker removes
hi(linker: removing identical template instantiations) identical
instantiations of a template from the final program, leaving only one
instantiation for each unique set of actual template type parameters. To
illustrate the linker's behavior we do as follows:
itemization(
it() First we construct several source files:
itemization(
itt(source1.cc) defines a function tt(fun), instantiating
tt(add) for tt(int)-type arguments, including tt(add)'s template
definition. It displays tt(add)'s address:
verbinclude(examples/pointerunion.h)
verbinclude(examples/source1.cc)
itt(source2.cc) defines the same function, but merely declares the
proper tt(add) template using a template declaration (em(not) an instantiation
declaration). Here is tt(source2.cc):
verbinclude(examples/source2.cc)
itt(main.cc) again includes tt(add)'s template definition,
declares the function tt(fun) and defines tt(main), defining tt(add)
for tt(int)-type arguments as well and displaying tt(add)'s function
address. It also calls the function tt(fun). Here is tt(main.cc):
verbinclude(examples/main.cc)
)
it() All sources are compiled to object modules. Note the different sizes
of tt(source1.o) (1912 bytes using tt(g++) version 4.3.4. All sizes reported
here may differ somewhat for different compilers and/or run-time libraries)
and tt(source2.o) (1740 bytes). Since tt(source1.o) contains the instantiation
of tt(add), it is somewhat larger than tt(source2.o), containing only the
template's declaration. Now we're ready to start our little experiment.
it() Linking tt(main.o) and tt(source1.o), we obviously link together two
object modules, each containing its own instantiation of the same template
function. The resulting program produces the following output:
verb(
0x80486d8
0x80486d8
)
Furthermore, the size of the resulting program is 6352 bytes.
it() Linking tt(main.o) and tt(source2.o), we now link together an object
module containing the instantiation of the tt(add) template, and another
object module containing the mere declaration of the same template
function. So, the resulting program cannot but contain a single instantiation
of the required function template. This program has exactly the same size, and
produces exactly the same output as the first program.
)
From our little experiment we conclude that the linker indeed removes
identical template instantiations from a final program. Furthermore we
conclude that using mere template declarations does not result in template
instantiations.

View file

@ -18,7 +18,7 @@ and emi(function template) are introduced and several examples of
templates are provided (both in this chapter and in chapter ref(CONCRETE)).
Template em(classes) are covered in chapter ref(TEMPCLASS).
Templates offered standard by the language include the abstract containers
Templates already offered by the language include the abstract containers
(cf. chapter ref(CONTAINERS)); the tt(string) (cf. chapter ref(String));
streams (cf. chapter ref(IOStreams)); and the generic algorithms (cf. chapter
ref(GENERIC)). So, templates play a central role in present-day bf(C++), and
@ -36,4 +36,4 @@ classes that are not templates), but on constructing templates.
This chapter starts by introducing em(function templates). The emphasis is on
the required syntax. This chapter lays the foundation upon which the other
chapter about templates are built.
chapters about templates are built.

View file

@ -49,32 +49,24 @@ such a function the name of a function may be specified as its argument. The
address of the function is then assigned to the pointer-parameter, deducing
the template type parameter in the process. This is called a
i(function-to-pointer transformation). For example:
verb(
#include <cmath>
template<typename Type>
void call(Type (*fp)(Type), Type const &value)
{
(*fp)(value);
}
int main()
{
call(&sqrt, 2.0);
}
)
verbinclude(examples/lvalue.cc)
In this example, the address of the tt(sqrt) function is passed to
tt(call), expecting a pointer to a function returning a tt(Type) and expecting
a tt(Type) for its argument. Using the function-to-pointer transformation,
tt(sqrt)'s address is assigned to tt(fp), deducing that tt(Type) is tt(double)
in the process. The argument tt(2.0) could not have been specified as tt(2) as
there is no tt(int sqrt(int)) prototype. Furthermore, the function's first
parameter specifies tt(Type (*fp)(Type)), rather than tt(Type (*fp)(Type const
&)) as might have been expected from our previous discussion about how to
specify the types of function template's parameters, preferring references
over values. However, tt(fp)'s argument tt(Type) is not a function template
parameter, but a parameter of the function tt(fp) points to. Since tt(sqrt)
has prototype tt(double sqrt(double)), rather than tt(double sqrt(double const
&)), tt(call)'s parameter tt(fp) em(must) be specified as tt(Type
in the process (note that tt(sqrt) is the em(address) of a function, not a
variable that is a pointer to a function, hence the lvalue
transformation).
The argument tt(2.0) could not have been specified as tt(2) as there is no
tt(int sqrt(int)) prototype. Furthermore, the function's first parameter
specifies tt(Type (*fp)(Type)), rather than tt(Type (*fp)(Type const &)) as
might have been expected from our previous discussion about how to specify the
types of function template's parameters, preferring references over values.
However, tt(fp)'s argument tt(Type) is not a function template parameter, but
a parameter of the function tt(fp) points to. Since tt(sqrt) has prototype
tt(double sqrt(double)), rather than tt(double sqrt(double const &)),
tt(call)'s parameter tt(fp) em(must) be specified as tt(Type
(*fp)(Type)). It's that strict.
)
)

View file

@ -0,0 +1,49 @@
As mentioned in section ref(TEMPFUNDECL), the linker removes
hi(linker: removing identical template instantiations) identical
instantiations of a template from the final program, leaving only one
instantiation for each unique set of actual template type parameters. To
illustrate the linker's behavior we do as follows:
itemization(
it() First we construct several source files:
itemization(
itt(source1.cc) defines a function tt(fun), instantiating
tt(add) for tt(int)-type arguments, including tt(add)'s template
definition. It displays tt(add)'s address:
verbinclude(examples/pointerunion.h)
verbinclude(examples/source1.cc)
itt(source2.cc) defines the same function, but merely declares the
proper tt(add) template using a template declaration (em(not) an instantiation
declaration). Here is tt(source2.cc):
verbinclude(examples/source2.cc)
itt(main.cc) again includes tt(add)'s template definition,
declares the function tt(fun) and defines tt(main), defining tt(add)
for tt(int)-type arguments as well and displaying tt(add)'s function
address. It also calls the function tt(fun). Here is tt(main.cc):
verbinclude(examples/main.cc)
)
it() All sources are compiled to object modules. Note the different sizes
of tt(source1.o) (1912 bytes using tt(g++) version 4.3.4 (sizes of object
modules reported in this section may differ for different compilers and/or
run-time libraries)) and tt(source2.o) (1740 bytes). Since tt(source1.o)
contains the instantiation of tt(add), it is somewhat larger than
tt(source2.o), containing only the template's declaration. Now we're ready to
start our little experiment.
it() Linking tt(main.o) and tt(source1.o), we obviously link together two
object modules, each containing its own instantiation of the same template
function. The resulting program produces the following output:
verb(
0x80486d8
0x80486d8
)
Furthermore, the size of the resulting program is 6352 bytes.
it() Linking tt(main.o) and tt(source2.o), we now link together an object
module containing the instantiation of the tt(add) template, and another
object module containing the mere declaration of the same template
function. So, the resulting program cannot but contain a single instantiation
of the required function template. This program has exactly the same size, and
produces exactly the same output as the first program.
)
From our little experiment we conclude that the linker indeed removes
identical template instantiations from a final program. Furthermore we
conclude that using mere template declarations does not result in template
instantiations.

View file

@ -42,6 +42,9 @@ vector:
return accumulate(vect.begin(), vect.end(), Type());
}
)
COMMENT(OVERLOADING FUNCTION TEMPLATE PARAMETERS)
When overloading function templates we do not have to restrict ourselves
to the function's parameter list. The template's type parameter list itself
may also be
@ -68,9 +71,9 @@ overloaded version of the tt(add) template:
}
)
One may wonder whether the tt(init) parameter could not be left out of the
parameter list as tt(init) will often have a default initialization value. The
answer is a somewhat complex `yes', It em(is) possible to define the tt(add)
function as follows:
parameter list as tt(init) often has a default initialization value. The
answer is `yes', but there are complications. It em(is) possible to define the
tt(add) function as follows:
verb(
template <typename Type, typename Container>
Type add(Container const &cont)
@ -78,9 +81,9 @@ function as follows:
return std::accumulate(cont.begin(), cont.end(), Type());
}
)
But note that the template's type parameters were reordered, which is
necessary because the compiler won't be able to determine tt(Type) in a call
like:
Note, however, that the template's type parameters were reordered, which
is necessary because the compiler won't be able to determine tt(Type) in a
call like:
verb(
int x = add(vectorOfInts);
)
@ -97,145 +100,3 @@ template parameter exists, a em(template template parameter), allowing the
compiler to determine tt(Type) directly from the actual container
argument. Template template parameters are discussed in section
ref(TEMPTEMPPAR).
With all these overloaded versions in place, we may now start the compiler
to compile the following function:
verb(
using namespace std;
int main()
{
vector<int> v;
add(3, 4); // 1 (see text)
add(v); // 2
add(v, 0); // 3
}
)
itemization(
it() In statement 1 the compiler recognizes two identical types, both
tt(int). It will therefore instantiate tt(add<int>), our very first definition
of the tt(add) template.
it() In statement 2 a single argument is used. Consequently, the compiler
looks for an overloaded version of tt(add) requiring but one argument. It
finds the overloaded function template expecting a tt(std::vector), deducing
that the template's type parameter must be tt(int). It instantiates
verb(
add<int>(std::vector<int> const &)
)
it() In statement 3 the compiler again encounters an argument list having
two arguments. However, this time the types of the arguments aren't equal,
so tt(add) template's first definition can't be used. But it em(can) use the
last definition, expecting entities having different types. As a
tt(std::vector) supports tt(begin) and tt(end), the compiler is now able to
instantiate the function template
verb(
add<std::vector<int>, int>(std::vector<int> const &, int const &)
)
)
Having defined the tt(add) function template for two equal and two
different template type parameters we've exhausted the possibilities for using
an tt(add) function template having two template type parameters.
Although it em(is) possible to define another function template tt(add) this
will introduce an ambiguity as the compiler won't be able to choose which of
the two overloaded versions defining two differently typed function parameters
to use. For example when defining:
verb(
#include "add.h"
template <typename T1, typename T2>
T1 add(T1 const &lvalue, T2 const &rvalue)
{
return lvalue + rvalue;
}
int main()
{
add(3, 4.5);
}
)
the compiler will report an ambiguity like the following:
verb(
error: call of overloaded `add(int, double)' is ambiguous
error: candidates are: Type add(const Container&, const Type&)
[with Container = int, Type = double]
error: T1 add(const T1&, const T2&)
[with T1 = int, T2 = double]
)
Now recall the overloaded function template accepting three arguments:
verb(
template <typename Type>
Type add(Type const &lvalue, Type const &mvalue, Type const &rvalue)
{
return lvalue + mvalue + rvalue;
}
)
It may be considered as a disadvantage that only equally typed arguments
are accepted by this function (three tt(int)s, three tt(double)s, etc.). To
remedy this we define yet another overloaded function template, this time
accepting arguments of any type. This function template can only be used if
tt(operator+) is defined between the function's actually used types, but apart
from that there appears to be no problem. Here is the overloaded version
accepting arguments of any type:
verb(
template <typename Type1, typename Type2, typename Type3>
Type1 add(Type1 const &lvalue, Type2 const &mvalue, Type3 const &rvalue)
{
return lvalue + mvalue + rvalue;
}
)
Now that we've defined the above two overloaded function templates
expecting three arguments let's call tt(add) as follows:
verb(
add(1, 2, 3);
)
Should we expect an ambiguity here? After all, the compiler might select
the former function, deducing that tt(Type == int), but it might also select
the latter function, deducing that tt(Type1 == int, Type2 == int) and tt(Type3
== int). Remarkably, the compiler reports no ambiguity.
No ambiguity is reported because of the following. If overloaded template
functions are defined using em(less) and em(more) specialized template type
parameters (e.g., less specialized: all types different vs. more specialized:
hi(function templates: specialized type parameters) all types equal)
then the compiler selects the more specialized function whenever possible.
As a i(rule of thumb): overloaded function templates must allow a unique
combination of template type arguments to be specified to prevent ambiguities
when selecting which overloaded function template to instantiate. The
em(ordering) of template type parameters in the function template's type
parameter list is not important. E.g., trying to instantiate one of the
following function templates results in an ambiguity:
verb(
template <typename T1, typename T2>
void binarg(T1 const &first, T2 const &second)
{}
template <typename T1, typename T2>
void binarg(T2 const &first, T1 const &second)
{}
)
This should not come as a surprise. After all, template type parameters
are just formal names. Their names (tt(T1), tt(T2) or tt(Whatever)) have no
concrete meanings.
Like any function overloaded functions may also be declared, either using
plain declarations or instantiation declarations. Explicit template argument
types may also be used. Example:
itemization(
it() To declare a function template tt(add) accepting certain containers:
verb(
template <typename Container, typename Type>
Type add(Container const &container, Type const &init);
)
it() to use an instantiation declaration (in which case the compiler must
already have seen the template's definition):
verb(
template int add<std::vector<int>, int>
(std::vector<int> const &vect, int const &init);
)
it() to use explicit template type arguments:
verb(
std::vector<int> vi;
int sum = add<std::vector<int>, int>(vi, 0);
)
)