A learning note from reference [1].
Author: Simon Lee
[TOC]
We can treat template as a function, who takes as input some values and returns a new type:
(T1, T2, T3, ...) => NT
Observe,
template <typename T>
class ContainerWrapper {
public:
T first() { return container.front(); }
private:
std::vector<T> container;
};
We can treat the template type FunctionName<T>
as a function, namely FunctionName, taking as input the type T
and returning FunctionName<T>
.
We can now expand the template above, and makes the member container
to be an parameter,
template <typename T, typename Container = std::vector<T>>
class ContainerWrapper {
public:
T first() { return container.front(); }
private:
Container container;
};
We can see from above, that the template the now similar to the generic programming in Java. The difference is that, in template meta programming (C++), we treat Container
as a duck type (by now in C++ 11), the only two (implied) requirements of which is that (1) Container
must have a default constructor, and (2) it must have a function of name front and of type () => T
. Recall the same semantics in Java, things get quite different. In Java, we have to appoint that the generic type Container<T>
implements an interface containing a function front
, which obeys the thought of polymorphism.
Or, even conciser:
template <typename T,
template <typename E, typename A = std::allocator<E>> class Container = std::vector>
class ContainerWrapper {
public:
T first() { return container.front(); }
private:
Container<T> container;
};
Therefore, what is a template ?
Template is a function, who takes as input a type, a non-type1, even a template, and returns a new type.
When we've already defined a template, say, SomeTemplate<A, B, C>
, where A
/B
/C
can be a type, a non-type, and a template, we can do some operations on them to make them behave differently, similar to function overload. Specialization is only dependent on the concrete type, not the template parameters.
-
We can partial specialize
SomeTemplate<A, B, C>
toPartialSomeTemplate<A, B>
by designate a concrete valueCC
toC
(a type/non-type/template) to it. And then redefined its behavior (the specialized template). The only requirements here is that, we must guarantee that, the designated valueCC
fits the definition ofC
.- if
C
is a non-type, say a int, thenCC
must be a number which can be calculated while compiling. - if
C
is a type, thenCC
must be a specific type. - if
C
is a template, saytemplate <typename E, typename A = std::allocator<E>>
, thenCC
must be an instantiation of it, such asstd::vector
e.g. we can specialize
ContainerWrapper
defined above:template <typename T> class ContainerWrapper<T, std::array> { // here, we can stay unchanged, or change it to adapt std::array, e.g. delete front, modify free, and other functions, or anything you can do. }
- if
-
Different from above, a full template specialization is a specialization of a template with all parameters designated legally.
template <> // this must not be abondaned class ContainerWrapper<int, std::vector> { // here, we can stay unchanged, or change it to adapt std::vector, e.g. delete front, modify free, and other functions, or anything you can do. }
Once a specialization is done, the compiler will generate the corresponding codes. And when a template is instantiated, the compiler will look for the correct template (like a pattern matching). For instance, the compiler will use ContainerWrapper<int, std::vector>
to instantiate ContainerWrapper<int, std::vector>
, use ContainerWrapper<T, std::array>
to instantiate ContainerWrapper<double, std::array>
, use ContainerWrapper<T, template <typename E, typename A = std::allocator<E>> class Container = std::vector>
to instantiate ContainerWrapper<double, std::deque>
.
Using this, we can do some interesting things, such as implement an operator to check the equivalence of two type,
template <typename T1, typename T2>
struct is_type_equal {
enum { ret = false };
};
template <typename T>
struct is_type_equal<T, T> {
enum { ret = true };
};
std::cout << is_type_equal<int, float>::ret << std::endl; // prints flase
-
For non-type parameters, the compiler can do numeric computation while compiling. Observe,
template <int x, int y> struct calculate { enum { sum = x + y, sub = x - y, mul = x * y, div = x / y }; }; int main() { std::cout << calculate<1,2>::sum << std::endl; // prints 3 std::cout << calculate<1,2>::sub << std::endl; // prints -1 std::cout << calculate<1,2>::mul << std::endl; // prints 2 std::cout << calculate<1,2>::div << std::endl; // prints 0 }
The compiler will compute all the four
enum
s in compile time. And then you can just usecalculate
as an operator likecalculate<1, 2>::sum
. -
For type parameters, the compiler can do type computation while compiling. Observe,
template <typename T> struct extract_type { using lref_t = T &; using rref_t = T &&; using const_lref_t = const T &; using const_rref_t = const T &&; using pointer_t = T *; using const_pointer_t = const T *; }; int x = 9; typename extract_type<int>::lref_t lrefx = x;
When compiling, the compiler can compute all the type in
extract_type<T>
. -
The compiler can do recursive computation, too. Observe,
// numeric computation #define __f(...) fib<__VA_ARGS__>::ret template <int N> struct fib { enum { ret = __f(N-1) * __f(N-2) }; }; template <> struct fib<0> { enum { ret = 1 }; }; template <> struct fib<1> { enum { ret = 1 }; }; std::cout << fib<5>::ret << std::endl; // prints 5 // type computation #define __p(...) typename pointer_of<__VA_ARGS__>::type template <typename T, int N> struct pointer_of { using type = __p(__p(T, N-1), 1); }; template <typename T> struct pointer_of<T, 1> { using type = T *; }; int *px = nullptr; typename pointer_of<int, 2>::type ppx = &px;
Code shown above gives an operator
fib<N>
which can compute the fibonacci number in compilation, and an operatorpointer_of<T, N>
to compute the pointer^N^ of type T. And we use template specialization to define their stop condition (at N = 0 or/and 1). This is a powerful capability !
As shown above, any computation results are stored inside the template (either fib<N>::ret
or extract_type<T>::lref_t
), and we can use them whenever needed.
We've seen above that, a template is just a function, of whom the arguments are passed in the angle bracket <>
,and the return value is stored inside the template. And we name these functions (calculate
/extract_type
/fib
/pointer_of
/…) meta functions, and it is the foundation of C++ meta programming.
In functional programming, the function itself is a first class member, an ordinary data type like string, int, etc… And a function is a high order function if, it takes as input a function, or returns a function. Similarly, in C++ meta programming, we have high order functions. Observe,
template <int X, int Y, template <int> class F>
struct max_if_f {
enum { ret = F<X>::ret < F<Y>::ret ? Y : X };
};
template <int X>
struct square {
enum { ret = X * X };
};
template <int X, int Y>
using max_if_square = max_if_f<X, Y, square>;
std::cout << max_if_square<-5, 2>::ret << std::endl; // prints -5
As shown, we create a high order meta function max_if_t
, who takes as input two ints, and a meta function F
, then returns the max value between F
of them. And we pass square
to it then alias it as max_if_square
, and this is called Meta Function Forwarding.
Attention here: C++ (11) does not allow the template specialization inside a template or class, and if needed, define them outside, and use
using
to refer to them.
As codes above shown, we use enum { ret = … }
to represent the return value, and using type = …
to represent a return type. This is not that acceptable in our subsequent processing. For unification, we make every thing a type. Observe,
template <int X>
struct aint {
enum { value = X };
using type = aint<X>;
};
Then we can use aint<4>::value
to represent the value, and aint<4>::type
to represent its type. Similarly,
struct anull {}; // anull indicates the end
template <bool> struct abool;
template <>
struct abool<true> {
enum { value = true };
using type = abool<true>;
};
template <>
struct abool<false> {
enum { value = false };
using type = abool<false>;
};
using atrue = abool<true>;
using afalse = abool<false>;
Then whenever we want to pass a non-type, say 4
, we can use aint<4>
and inside use aint<4>::value
. As an example, we modify max_if_f<X, Y, F>
,
template <typename X, typename Y, template <typename> class F>
struct max_if_f {
enum { value = F<X>::value < F<Y>::value ? Y::value : X::value };
};
template <typename X>
struct square {
enum { value = X::value * X::value };
};
template <typename X, typename Y>
using max_if_square = max_if_f<X, Y, square>;
std::cout << max_if_square<aint<-5>, aint<1>>::value << std::endl; // prints -5
Well, beautiful !
In functional programming, the usage of aint<5>::type
is not acceptable. Therefore we encapsulate some functions for them,
template <typename T1, typename T2>
struct is_eq {
enum { value = false };
using type = afalse;
};
template <typename T>
struct is_eq<T, T> {
enum { value = true };
using type = atrue;
};
// __value(T)
template <typename T>
struct the_value {
enum { value = 0 };
};
template <int X>
struct the_value<aint<X>> {
enum { value = X };
};
template <bool B>
struct the_value<abool<B>> {
enum { value = B };
};
template <>
struct the_value<anull> {
enum { value = -1 };
};
// do not even try `t::value` and maybe, `((t)::value)`! you will regret it, trust me! :-). want to know the reason? use them and preprocess the file to see the preprocessed result using opetion -E in g++/clang. :-)
#define __value(t) the_value<t>::value
#define __int(x) typename aint<(x)>::type
#define __bool(v) typename abool<(v)>::type
#define __true() typename atrue::type
#define __false() typename afalse::type
#define __is_eq(x, y) typename is_eq<x, y>::type
Then, we can use everything in a function,
std::cout << __value(__is_eq(__int(4), __int(4))) << std::endl; // 1
std::cout << __value(__is_eq(__int(4), __bool(true))) << std::endl; // 0
std::cout << __value(__is_eq(__true(), __true())) << std::endl; // 1
std::cout << __value(__is_eq(__true(), __false())) << std::endl; // 0
std::cout << __value(__is_eq(__false(), __false())) << std::endl; // 1
They are all computed in compile time. However, we could do more. Let's define some operations on aint
and abool
.
// __add(x, y)
template <typename X, typename Y> struct f_add;
template <int X, int Y>
struct f_add<aint<X>, aint<Y>> {
using type = aint<X+Y>;
};
#define __add(x, y) typename f_add<x, y>::type
// __and(x, y)
template <typename X, typename Y> struct f_and;
template <bool X, bool Y>
struct f_and<abool<X>, abool<Y>> {
using type = abool<X && Y>;
};
#define __and(x, y) typename f_and<x, y>::type
std::cout << __value(__add(__int(1), __int(2))) << std::endl; // prints 3
std::cout << __value(__and(__true(), __false())) << std::endl; // prints false
Moreover, __sub(x, y)
/__mul(x, y)
/__div(x, y)
/__mod(x, y)
/__or(x, y)
/__not(x, y)
/… can be defined using the similar way.
There are two ways to do pattern matching: (1) template specialization (we've introduced above), and (2) function overloading.
-
Using knowledge we've known above, we can define a
__if(c, t, f)
statement that receives a condition typec
and then determine tot
when__true()
, andf
when__false()
.template <typename C, typename T, typename F> struct s_if; template <typename T, typename F> struct s_if<atrue, T, F> { using type = T; }; template <typename T, typename F> struct s_if<afalse, T, F> { using type = F; }; #define __if(c, t, f) typename s_if<c, t, f>::type
Then, have a try! We define a operator
larger_type<T1, T2>
to choose a larger type,template <typename T1, typename T2> struct larger_type { using type = __if(__bool(sizeof(T1) > sizeof(T2)), T1, T2); }; struct LargerOne { static const char *s; char paddig[2]; }; struct SmallerOne { static const char *s; char paddig; }; const char *LargerOne::s = "larger_one"; const char *SmallerOne::s = "smaller_one"; // have a try std::cout << larger_type<LargerOne, SmallerOne>::type::s << std::endl; // wow! prints "larger_one" :-)
-
One of the powerful function in programming language is the function overloading, via which, the compiler will help us to choose the suitable function according to our data type. Hence, we do not have to differentiate them by their names. Therefore, we can use it to do something.
Let's implement an operator
__is_convertible(T, U)
checking whether a typeT
can be converted to typeU
by compiler.template <typename D, typename B> struct is_convertible { private: using yes = char; using no = int; static yes test(B); // if D is a B, then this will be invoked static no test(...); // we don't care about the parameters static D a_D(); public: using type = abool<sizeof(test(a_D())) == sizeof(yes)>; // we use a_D() instead of D() to avoid the overhead of constructing a new object }; #define __is_convertible(D, B) typename is_convertible<D, B>::type
Code above uses the overloaded function
test
to check the convertibility. One more step, we can implements__is_a(D, B)
to check ifD
is a subtype ofB
.#define __is_a(D, B) __and(__is_convertible(const D *, const B *), \ __and(__not(__is_eq(const B*, const void*)), \ __not(__is_eq(const D, const B))))
Check it!
std::cout << __value(__is_a(int, char)) << std::endl; // false std::cout << __value(__is_a(char, int)) << std::endl; // false std::cout << __value(__is_a(Derived, Base)) << std::endl; // true std::cout << __value(__is_a(Base, Derived)) << std::endl; // false
As the condition statement relies on pattern matching, the loop statement relies on recursion. C++ provides variadic templates, which makes the loop available.
Let's firstly define an operator reduce
which is of much power in functional programming.
// __reduce(f, x, ...), x is the initial value
template <template <typename, typename> class F, typename X, typename ...N> struct f_reduce;
// assume N is the first, <4, 3, 2, 1, ...>, then N is 4, the last to be dealed with
template <template <typename, typename> class F, typename X, typename N, typename ...R>
struct f_reduce<F, X, N, R...> {
using type = typename f_reduce<F, typename f_reduce<F, X, R...>::type, N>::type;
};
template <template <typename, typename> class F, typename X, typename N>
struct f_reduce<F, X, N> {
using type = typename F<X, N>::type;
};
#define __reduce(f, x, ...) typename f_reduce<f, x, __VA_ARGS__>::type
Fine now, how to implement a sum
so that sum
could receive variadic ?
#define __sum(...) __reduce(f_add, __VA_ARGS__)
Wow, so easy! Let's test it!
std::cout << __value(__sum(__int(1), __int(2))) << std::endl; // 3
std::cout << __value(__sum(__int(1), __int(2), __int(3))) << std::endl; // 6
std::cout << __value(__sum(__int(1), __int(2), __int(-4))) << std::endl; // -1
std::cout << __value(__sum(__int(1))) << std::endl; // compile error
Our __sum()
works fine for parameters >= 2, but when it comes to 1 parameter, a compile error happened! Why? Recall that reduce works rely on a function F
who takes as input 2 very parameters! Oops! The requirement of __reduce
is that the number of parameters (except the function F
) is 2 (with one initial value and an array with least length of 1). Let's modify the reduce to make the reduce operation more flexible2! Add a specialization, and modify the macro,
template <template <typename, typename> class F, typename X>
struct f_reduce<F, X> {
using type = X;
};
#define __sum(...) __reduce(f_add, __VA_ARGS__)
Okay, we get it using template partial specialization! The __sum(...)
works! Similarly, we can define __max(…)
/__min(…)/…
, a lot of them.
Once you want to make reduce match the original semantics, i.e. takes at least 2 input values, you can define
__sum(…)
like,// __sum(...) template <typename ...N> struct f_sum { using type = typename f_reduce<f_add, N...>::type; }; template <typename N> struct f_sum<N> { using type = N; }; #define __sum(...) typename f_sum<__VA_ARGS__>::type
Variables we use during compilation are all immutable. Can we write such code ?
using A = int;
A = char; // illegal
Absolutely no! All variables are bounded to their initial value, and can never be modified. In other words, they are all const.
The immutability in programming provides many advantages.
The compiler is lazy. Observe the following codes,
using Zero = __int(0);
If we bound __int(0)
to Zero, the value Zero::value
won't be calculated, even be generated until we explicitly use it. In other words, if we does not use Zero::value
in our code, the compiler won't calculate it. The laziness of the compiler improves the performance of the compiler, and also reduce the space needed.
Moreover, if we define a function for a class(or struct, or union), but never invoke it, the compiler won't generate it!
We mentioned previously that, C++ template meta programming leverages duck type. We say that because we only need to provide concrete types that have some functions.
And, when you treat C++ template meta programming as a separate programming language, it is more like a interpretive language with strong type checking, within (1) type, (2) non-type(int, char, bool, pointers, etc..) , and (3) template. When you pass a non-type (e.g. 1
) to a parameter who needs a type (e.g. typename T
), the compiler complains errors.
In programming, list is a fundamental element to most data types. And here we can implement a __typelist(…)
that can contain a list of types in compile time.
Let's first consider the question: what is a list?
In function programming, a list is a recursive data type,
[1, [2, [3, [4, []]]]]
Ok, have this knowledge is enough for us. Because we have introduced the recursion
previously.
Firstly, let's define a easy data type called apair<F, S>
of whom the first element is F
, and the second S
.
// __pair(F, S)
template <typename F, typename S>
struct apair {
using first = F;
using second = S;
};
#define __pair(F, S) apair<F, S>
Using apair<F, S>
, we can define a recursive operator typelist<…>
to create a typelist. Watch out here, we will clearly claim that, the __typelist(…)
we'll define is merely a meta function, not a type, with no differences with other meta functions like __value()
, but with difference with aint
. __typelist
will accept a bunch of types and returns a apair<F, S>
.
// __empty()
struct aempty {};
#define __empty() aempty
// __typelist(...)
template <typename... T>
struct typelist {
using type = aempty;
};
template <typename H, typename... T>
struct typelist<H, T...> {
using type = apair<H, typename typelist<T...>::type>;
};
template <typename H>
struct typelist<H> {
using type = apair<H, aempty>;
};
#define __typelist(...) typename typelist<__VA_ARGS__>::type
The aempty
above is a empty type means containing nothing. It is of great importance.
With no operations on the list, we can do nothing. Therefore, let's define some operations on it! The common ones are length, get, set, insert and remove.
We've claimed that, recursion is of great importance. Algorithms following are all in recursion manner apparently from the definition of typelist. Let's show a relatively complex one, and the rest ones you can define them by yourself!
// __tl_insert
template <typename TL, int N, typename X>
struct tl_insert {
using type = TL;
};
template <typename F, typename S, int N, typename X>
struct tl_insert<apair<F, S>, N, X> {
using type = apair<F, typename tl_insert<S, N-1, X>::type>;
};
template <typename F, typename S, typename X>
struct tl_insert<apair<F, S>, 0, X> {
using type = apair<X, apair<F, S>>;
};
template <int N, typename X>
struct tl_insert<aempty, N, X> {
using type = apair<X, aempty>;
};
#define __tl_insert(TL, n, S) typename tl_insert<TL, (n), S>::type
#define __tl_append(TL, S) __tl_insert(TL, __value(__tl_length(TL)), S)
#define __tl_prepend(TL, S) __tl_insert(TL, 0, S)
Now have a try!
using L = __typelist(int, float, void *, Base, Derived, const LargerOne &);
ASSERT_EQ(1, __value(__is_eq(aint<6>, __tl_length(L))));
ASSERT_EQ(1, __value(__is_eq(int, __tl_get(L, 0))));
ASSERT_EQ(1, __value(__is_eq(float, __tl_get(L, 1))));
ASSERT_EQ(1, __value(__is_eq(void *, __tl_get(L, 2))));
ASSERT_EQ(1, __value(__is_eq(Base, __tl_get(L, 3))));
ASSERT_EQ(1, __value(__is_eq(Derived, __tl_get(L, 4))));
ASSERT_EQ(1, __value(__is_eq(const LargerOne &, __tl_get(L, 5))));
ASSERT_EQ(1, __value(__is_eq(anull, __tl_get(L, 6))));
ASSERT_EQ(1, __value(__is_eq(anull, __tl_get(L, 7))));
ASSERT_EQ(1, __value(__is_eq(anull, __tl_get(int, 7))));
using LI1 = __tl_insert(L, 3, double);
ASSERT_EQ(1, __value(__is_eq(aint<7>, __tl_length(LI1))));
ASSERT_EQ(1, __value(__is_eq(int, __tl_get(LI1, 0))));
ASSERT_EQ(1, __value(__is_eq(float, __tl_get(LI1, 1))));
ASSERT_EQ(1, __value(__is_eq(void *, __tl_get(LI1, 2))));
ASSERT_EQ(1, __value(__is_eq(double, __tl_get(LI1, 3))));
ASSERT_EQ(1, __value(__is_eq(Base, __tl_get(LI1, 4))));
ASSERT_EQ(1, __value(__is_eq(Derived, __tl_get(LI1, 5))));
ASSERT_EQ(1, __value(__is_eq(const LargerOne &, __tl_get(LI1,6))));
ASSERT_EQ(1, __value(__is_eq(anull, __tl_get(LI1, 7))));
ASSERT_EQ(1, __value(__is_eq(anull, __tl_get(LI1, 8))));
ASSERT_EQ(1, __value(__is_eq(anull, __tl_get(int, 7))));
using LI2 = __tl_append(L, double);
ASSERT_EQ(1, __value(__is_eq(aint<7>, __tl_length(LI2))));
ASSERT_EQ(1, __value(__is_eq(int, __tl_get(LI2, 0))));
ASSERT_EQ(1, __value(__is_eq(float, __tl_get(LI2, 1))));
ASSERT_EQ(1, __value(__is_eq(void *, __tl_get(LI2, 2))));
ASSERT_EQ(1, __value(__is_eq(Base, __tl_get(LI2, 3))));
ASSERT_EQ(1, __value(__is_eq(Derived, __tl_get(LI2, 4))));
ASSERT_EQ(1, __value(__is_eq(const LargerOne &, __tl_get(LI2,5))));
ASSERT_EQ(1, __value(__is_eq(double, __tl_get(LI2,6))));
ASSERT_EQ(1, __value(__is_eq(anull, __tl_get(LI2, 7))));
ASSERT_EQ(1, __value(__is_eq(anull, __tl_get(LI2, 8))));
ASSERT_EQ(1, __value(__is_eq(anull, __tl_get(int, 7))));
using LI3 = __tl_prepend(L, double);
ASSERT_EQ(1, __value(__is_eq(aint<7>, __tl_length(LI3))));
ASSERT_EQ(1, __value(__is_eq(double, __tl_get(LI3, 0))));
ASSERT_EQ(1, __value(__is_eq(int, __tl_get(LI3, 1))));
ASSERT_EQ(1, __value(__is_eq(float, __tl_get(LI3, 2))));
ASSERT_EQ(1, __value(__is_eq(void *, __tl_get(LI3, 3))));
ASSERT_EQ(1, __value(__is_eq(Base, __tl_get(LI3, 4))));
ASSERT_EQ(1, __value(__is_eq(Derived, __tl_get(LI3, 5))));
ASSERT_EQ(1, __value(__is_eq(const LargerOne &, __tl_get(LI3,6))));
ASSERT_EQ(1, __value(__is_eq(anull, __tl_get(LI3, 7))));
ASSERT_EQ(1, __value(__is_eq(anull, __tl_get(LI3, 8))));
ASSERT_EQ(1, __value(__is_eq(anull, __tl_get(int, 7))));
Wow! Worked!
As we mentioned previously, we can treat C++ as a 2.5-phase programming languages after template is introduced.
- 0.5 the first half phase is the macro. Macro is a powerful tools (but not turing-complete because it just replaces text) with the help of the preprocessor.
- 1 the next full phase is the template meta programming. In this phase, the compiler acts as an interpreter. It will lazily generates the result of our meta functions for us.
- 1 the last full phase is the run-time C++ as we already familiar with.
Now let's open this. Right here, you will see a great number of compile-time functions, such as std::enable_if
. And open one of them, you will see the Possible implementation, and now, as a programmer of template meta programming, how easy they are! And you have the ability to implement them. Such as,
template <typename B, typename T = void>
struct enable_if;
template <typename T>
struct enable_if<atrue, T> {
using type = T;
};
template <typename T>
struct enable_if<afalse, T> {};
Observe the following code,
template <typename Iter>
?? val(Iter it) {
return *it;
}
We put ??
where the return type should be placed. The function val
we defined would like to get all the inner value of an iterator, then how to handle it? Recall the ::type
? Yes, we can use it!
template <typename Iter>
typename Iter::type val(Iter it) {
return *it;
}
Okay, as long as the type Iter
has a field type then we can get it. Therefore, we need to write an interface for all iterators who really want to use the function above.
template <typename T>
struct IteratorInterface {
using type = T;
};
Fine by now, all iterators can use it,
class MyIntIterator : public IteratorInterface<int> {
// blabla
};
// use it
val<MyIntIterator>(my_int_iterator); // or just val(my_iterator)
But you may notice, the raw pointer, e.g. char*
, is also a type of iterator, and we also want to use the val
function. Question here is that, a raw pointer e.g. char*
does not have an inner type called type
. How to handle it?
Okay, you may find the way to it. We can write a meta function who returns (or extracts, or traits) all the inner type defined, and for the raw type, we specialize it!
template <typename Iter>
struct iterator_traits {
using type = Iter::type;
};
template <typename T>
struct iterator_traits<T *> {
using type = T;
};
template <typename T>
struct iterator_traits<const T *> {
using type = T;
};
Then the val
get turned to,
template <typename Iter>
typename iterator_traits<Iter>::type val(Iter it) {
return *it;
}
And when comes across a raw type, e.g. char *x = &p; char v = val(x);
the compiler will find the specialized iterator_traits<T *>
or iterator_traits<const T *>
and returns T
. Perfect :-) !
Yes, as you see, this is called iterator traits ~
In STL, there are more inner types to be designated, such as value_type
(tppe
in our case), difference_type
, pointer
, reference
and iterator_category
. See here, they are easy to you now! :-)
Thanks for [1] greatly. It helps organize my knowledge on template of C++, so that I can treat it from a higher lever. And this, helps me greatly to learn more on the design details of the traits skills I've already known but not that know before.
Footnotes
-
A constant expression that designates the address of a complete object with static storage duration and external or internal linkage or a function with external or internal linkage, including function templates and function template-ids but excluding non-static class members, expressed (ignoring parentheses) as
&
id-expression, where the id-expression is the name of an object or function, except that the&
may be omitted if the name refers to a function or array shall be omitted if the corresponding template-parameter is a reference. ↩ -
In some library and language (Javascript for example) implementations, their reduce will return the initial value when they comes across 1 parameters. So do we. ↩