cppannotations/annotations/yo/functiontemplates/considerations.yo
2015-03-07 21:43:32 +01:00

193 lines
8.9 KiB
Text

We've managed to design our first function template:
verbinclude(-a 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}) is 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) defined 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. Formal types are
interpreted 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{}; // note: the default constructor must exist.
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 initializes 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 starts
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 does 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). The compiler will do so unless
it clearly isn't possible 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.