Modern c ++ template meta-programming

In the recent review "c ++ programming new thinking" this classic, a lot of emotion. Since the book earlier, many examples in the book Programming Element using c ++ 98 realized. Today c ++ 20 new features coming with the concept, Ranges, etc. come together, have to say that time flies. After c ++ 11, thanks to a lot of complex skills new standard of meta-programming can be simplified, STL also provides as <type_traits>such infrastructure, c ++ 14 is greatly extends the range of applications compile computing, these are meta-programming had no small impact. Today, I will use the book most simple is the most basic element of the container TypeListto the initial introduction of modern glamor in c ++ meta-programming field.

Citation

What is TypeList

TypeList the name suggests, is a type of storing and manipulating list, you read right, it is stored type (type of information) rather than data.

These stored type are also referred to as metadata, storage thereof is also referred TypeList container element.

So we store these metadata what use is it? The answer is a lot of useful, such tuple, the factory mode, which later will illustrate two; techniques may also be used to implement CRTP (a meta-programming techniques), the linear structure of inheritance, the original book which are detailed presentation.

But just look at me in the above explanation is probably not understand what is TypeList and what does it do with, but that's okay, meta-programming itself is highly abstract mental activity, only to read the code hard thinking to be harvested. Here I will show you how to use modern c ++ to achieve a TypeList, as well as c ++ 11 previous classical versions do a simple comparison.

The definition of TypeList

The initial question is how we want to store type it? Data can be saved variables, alone and something different type of data, how do?

Smart and you might think, we can make the template parameter becomes container type information.

But then the second question, it's the number of elements in the so-called list is fixed, but until 11 before the number of template parameters are fixed, then how do c ++?

In fact, very simple, common reference list of the linked list implementation method, we can also use the same idea to construct a "heterogeneous list":

template <typename T, typename U>
struct TypeList {
    typedef T Head;
    typedef U Tail;
};

This is the simplest definition, which, T is a common type, and U is a common type or TypeList. Creating TypeList is this:

// 创建unsigned char和signed char的list
typedef TypeList<unsigned char, signed char> TypedChars;
// 现在我们把char也添加进去
typedef TypeList<char, TypeList<unsigned char, signed char> > Chars;
// 创建int,short,long,long long的list
typedef TypeList<int, TypeList<short, TypeList<long, long long> > > Ints;

It can be seen through a chain TypeList, we can put all types are stored in a linked list of classes in the template. But this realization has many drawbacks:

  1. First, define the type is not convenient, the third example above, just to list 4 elements nested we will write a lot of code readability greatly reduced;
  2. The original book that, in order to simplify the definition, Loki library provides TYPELIST_Nthis macro, but it is hard-coded, but only supports a maximum 50 elements, hard-coded in the programmer's world is always ugly, not to mention there the maximum number of hard-coded, but doing so also violated "never repeat yourself" principle, but for c ++ 98, it can only be so;
  3. We can not clearly represent only one element or have no list of elements, so we can only introduce an empty class NullTypeto represent no data exists on the list a bit, such as: TypeList<char, NullType>or TypeList<NullType, NullType>, of course, you specialize in a single parameter TypeList just a new name.
  4. It can not be effectively obtained indicating the end of the list, except as above using NullType most end mark.

Fortunately, modern c ++ templates have a longer, mostly above restrictions do not exist:

template <typename...> struct TypeList;

template <typename Head, typename... Tails>
struct TypeList<Head, Tails...> {
    using head = Head;
    using tails = TypeList<Tails...>;
};

// 针对空list的特化
template <>
struct TypeList<> {};

By variable-length templates, we can define any length easily list:

using NumericList = TypeList<short, unsigned short, int, unsigned int, long, unsigned long>;

At the same time, we are a specialized empty TypeList, now we can use it as a marker to terminate, without the introduction of a new type. If you are unfamiliar with longer templates, you can search for relevant information on cnblogs there are many excellent tutorials to introduce this syntax characteristic has exceeded the scope of this article.

Of course, variable length template is not do any harm, the first variable length packet template parameter always unpacked an empty packet, which can lead to partial specialization master template and template discrepancy, so some of the membership function in the process time (calculated compile a template class some metadata function is called the yuan, from concept to boost.mpl) is necessary to be careful; Secondly, although we facilitate the type definitions and processing section, but add to the list head data is very difficult, with reference to the following examples:

// TL1是一个包含int和long的list,现在我们在头部添加一个short
// 古典实现很简单
using New = TypeList<short, TL1>;

// 而现代的实现就没那么轻松了
// using New = TypeList<short, TL1>; 这么做是错的

What's the problem? ...Operators can only parameter pack unpack extension, and TL1 is a type, not a parameter package, but we need the parameters included TL1 out, so the question arises.

For this demand we can only use one yuan functions to solve this shortcoming is one of the few modern methods.

Achieve membership function

It defines TypeList, followed by the definition of the various functions of the yuan.

You might wonder why not put yuan function is defined as a function of internal static template class constexpr it? Modern c ++ not already have a strong compile-time computing power yet?

The answer is no, compile-time constants function can only calculate values, and our metadata also includes a type, then the function can not be processed.

But words can not say dead, because the role of dealing with numerical constants in place constexpr is still very large, I will back the membership functions with constexpr auxiliary functions.

Length seeking membership function list length

The most common demand is calculated TypeList stored a number of elements, of course, this is also the easiest to implement demand.

Let's look at classical techniques, the so-called classical technique is to allow recursive template specialization, relying on specialization and partial specialization is determined to achieve the purpose of exit criteria evaluation.

Difficult to compile because the storage needs of the next iteration of the intermediate state, so we have to rely on this as a recursive function like handling skills:

template <typename TList> struct Length; // 主模板,为下面的偏特化服务

template <>
struct Length<TypeList<>> {
    static constexpr int value = 0;
}

template <typename Head, typename... Types>
struct Length<TypeList<Head, Types...>> {
    static constexpr int value = Length<Types...>::value + 1;
};

Explain, static constexpr int valueis c ++ 17 new features, which will be treated as a static variable inline variable in the class, you can place initialize (c ++ 11). Otherwise, you may need to value is defined as anonymous enum, one of which is a common meta-programming techniques.

We started one by one from the first parameter processing parameter package, the null packet return 0 end recursion, and then gradually returns from the bottom, so that each layer +1 result, because each layer represents a type.

In fact, we can use c ++ new features 11 --sizeof ... operator, which can be returned directly to the number of parameters package parameters:

template <typename... Types>
struct Length<TypeList<Types...>> {
    static constexpr int value = sizeof...(Types);
};

The use of modern c ++ code simple, because the parameters can always be expanded into an empty bag package, this time value is 0, you can also write a little specialization.

TypeAt get type on the index positions

The second operation on the common list is acquired by the position data corresponding index. To c ++ habits and the same, we require TypeList index also start from zero.

In Python you can reference this list of data list_1[3], but we do not give Yuan container creates an entity, containers and Yuan Yuan functions are compiled with typedef or other means of achieving compile calculations, only need to use its own type and type aliases. So we can only operate yuan containers: using res = typename TypeAt<TList, 3>::type.

With the function call in the form of dollars, we can begin to achieve a:

template <typename TList, unsigned int index> struct TypeAt;
template <typename Head, typename... Args>
struct TypeAt<TypeList<Head, Args...>, 0> {
    using type = Head;
};

template <typename Head, typename... Args, unsigned int i>
struct TypeAt<TypeList<Head, Args...>, i> {
    static_assert(i < sizeof...(Args) + 1, "i out of range");
    using type = typename TypeAt<TypeList<Args...>, i - 1>::type;
};

First is the statement of the main template, the specific implementation to the partial specialization.

While c ++ compiler already supports operation in constexpr of iterative function, but for now we can not be the template parameter pack direct iteration, even folding the expression c ++ 17 provided only to achieve a parameter pack in an expression expand place, far less than required iterations. So we had to use the old way, starting with the first parameter, and gradually reduce the number of parameters in the parameters of the package, a reduction in the index after the partial specialization of templates, index must be 0, and it must be that we need Head type, it can be set to the type, and the upper element function only shrinking the value of index, and the package Head removed from the parameters, and the remaining parameters passed to the next layer index membership function TypeAt i.e. can.

Incidentally, static_assert not necessary, because you pass illegal index, the compiler will detect directly, but in my (g ++ 8.3, clang ++ 8.0.1, vs2017) compiler complained that it issued to such issues is difficult for humans to read, so we use static_assert to clear the error message, and the rest of the information, such as how many illegal index, the compiler will give you tips.

If you do not want out of range error but returns NullType, you can write:

template <typename Head, typename... Args>
struct TypeAt<TypeList<Head, Args...>, 0> {
    using type = Head;
};

template <typename Head, typename... Args, unsigned int i>
struct TypeAt<TypeList<Head, Args...>, i> {
    // 如果i越界就返回NullType
    using type = typename TypeAt<TypeList<Args...>, i - 1>::type;
};

// 越界后的退出条件
template <unsigned int i>
struct TypeAt<TypeList<>, i> {
    using type = NullType;
};

Not wanting out of bounds after an error, so we have to provide cross-border parameter after package is empty exit conditions, it will immediately use the new specialization in the parameter packet processing finished, return NullType.

The astute reader may ask why not SFINAE, yes, and it's in the class template partial specialization, we can also list the template parameters or parameter list after the class name used enable_if achieve SFINAE, but there are two problems here , after the parameter list First class name must be able to push all performances template parameter list, the second parameter is the class name and other columns can not be the same partial specialization, but also meet the invocation master template. With the above restrictions, the use of SFINAE becomes extremely difficult. (Of course, if you can find a use to achieve SFINAE, you can also reply by telling me that we can learn from each other; SFINAE is not clear what the reader can refer to the Introduction on cppreference, very easy to understand)

Of course, to do so would be static assertion reluctantly part, to the richness of the interface performance, Loki authors alone will not realize the error of TypeAt yuan for different functions:

template <typename TList, unsigned int Index> struct TypeAtNonStrict;
template <typename Head, typename... Args>
struct TypeAtNonStrict<TypeList<Head, Args...>, 0> {
    using type = Head;
};

template <typename Head, typename... Args, unsigned int i>
struct TypeAtNonStrict<TypeList<Head, Args...>, i> {
    using type = typename TypeAtNonStrict<TypeList<Args...>, i - 1>::type;
};

template <unsigned int i>
struct TypeAtNonStrict<TypeList<>, i> {
    using type = Null;
};

IndexOf to get the index in the list to specify the type of

IndexOf TypeAt routines and almost, but not where the recursive parameter to scan the entire packet (packet-by-order processing parameter, and the scan is not the same as it), only needs to match the same type to be matched and Head, return 0; if not like TypeAt in matching dollars as a recursive call function, it returns the result +1, because the result after this layer, so it is necessary to add into the index in this layer, gradually adding the recursive call forward after the final result is the return type is located the index (starting from 0).

IndexOf an important function is to determine whether a certain type in TypeList in.

If done with parameters corresponding to the type of package still can not find it? At this time the empty TypeList be specialized return -1 on the line, of course, a function of the front element specializations also needs to be done to deal with this situation.

Now calling IndexOf we look at the form: "IndexOf <TList, int> :: value"

Now we realize it did as this form:

template <typename TList, typename T> struct IndexOf;
template <typename Head, typename... Tails, typename T>
struct IndexOf<TypeList<Head, Tails...>, T> {
private:
    // 为了避免表达式过长,先将递归的结果起了别名
    using Result = IndexOf<TypeList<Tails...>, T>;
public:
    // 如果类型相同就返回,否则检查递归结果,-1说明查找失败,否则返回递归结果+1
    static constexpr int value =
            std::is_same_v<Head, T> ? 0 :
            (Result::value == -1 ? -1 : Result::value + 1);
};

// 终止条件,没找到对应类型
template <typename T>
struct IndexOf<TypeList<>, T> {
    static constexpr int value = -1;
};

Thanks to the help of c ++ 11 type_traits, we can write a little lazy like this partial specialization:

template <typename... Tails, typename T>
struct IndexOf<TypeList<T, Tails...>, T> {
    static constexpr int value = 0;
};

However, the power of modern c ++ is much more than that, we said earlier iterations of the parameter packet can not be achieved, but we can make use of fold expression, constexpr function, the compiler of the three vessels, the mapping parameters of each packet parameters to compile container it can be iterated operations on the vessel after the compile, to avoid the recursive partial specialization.

Of course, this program only proves the possibility of c ++, and truly implement than the recursive way to more trouble, and performance may not be much better than the recursive (of course, are calculated compile will not pay the running costs ), but also to fully support c ++ 14, to support at least compile c ++ 17 fold expression (vs2019 can be set using Clang, native compiler support for a bit miserable c ++ 17).

The key technology is the c ++ 14's std::arrayand std::index_sequence.

The former is a compilation of containers we need to use (vector and perhaps the future will be the compilation of a container, dynamic memory allocation has entered compile c ++ 20), which is responsible for a string of digital maps as a template parameter pack to fold expression dEPLOYED. (Fold expression may refer to the explanation on a still cppreference)

std::index_sequenceAn example:

using Ints = std::make_index_sequence<5>; // 产生std::index_sequence<0, 1, 2, 3, 4>

// 将一串数字传递给模板,重新映射为变长模板参数
template <typename T, std::size_t... Nums>
void some_func(T, std::index_sequence<Nums...>) {/**/}

some_func("test", Ints{}); // 这时Nums包含<0, 1, 2, 3, 4>

One usage of this usage looked like meta-programming label distribution, but if you look carefully the two are not the same kind of skills do not find the specific name of this technique, so we temporarily called "Mapping the sequence of integers" .

With these pre-knowledge, we can now see realized:

template <typename TList, typename T> struct IndexOf2;
template <typename T, typename... Types>
struct IndexOf2<TypeList<Types...>, T> {
    using Seq = std::make_index_sequence<sizeof...(Types)>;
    static constexpr int index()
    {
        std::array<bool, sizeof...(Types)> buf = {false};
        set_array(buf, Seq{});
        for (int i = 0; i < sizeof...(Types); ++i) {
            if (buf[i] == true) {
                return i;
            }
        }
        return -1;
    }

    template <typename U, std::size_t... Index>
    static constexpr void set_array(U& arr, std::index_sequence<Index...>)
    {
        ((std::get<Index>(arr) = std::is_same_v<T, Types>), ...);
    }
};

// 空TypeList单独处理,简单返回-1即可,因为list里没有任何东西自然只能返回-1
template <typename T>
struct IndexOf2<TypeList<>, T> {
    static constexpr int index()
    {
        return -1;
    }
};

Where the index is well understood, first initialize a array, then map the status of each parameter of the parameter packet to the array, the index after the first cycle to find a true, the whole process is carried out at compile time.

The question is set_arraywhere, what really happened inside of it?

First, the sequence of integers mapped we mentioned earlier, Index after mapping is {0, 1, 2, ..., len_of(Array) - 1}then folded to expand the expression:

(
    (std::get<0>(arr) = std::is_same_v<T, Types_0>),
    (std::get<1>(arr) = std::is_same_v<T, Types_1>),
    (std::get<2>(arr) = std::is_same_v<T, Types_2>),
    ...,
    (std::get<len_of(Array) - 1>(arr) = std::is_same_v<T, Types_(len_of(Array) - 1>)),
)

Real expansion is similar to Arg1, (Arg2, (Arg3, Arg4))this, for readability I put brackets omitted here anyway execution order does not affect the results.

get will return the contents of the array index specified references, so we can assign to it, Types_Nis from left to right is expand the parameters so that the recursive parameter will package all finished processing parameters without the aid.

But the essence is still far away Scheme B-style juggling, practicality is not high, but it fully demonstrate the possibilities of modern c ++ template meta-programming to bring.

Append additional element is TypeList

After reading the first few yuan function you may already feel a little tired, we see nothing simple relax.

Append can be added before TypeList elements (although strictly speaking this is not called Append operation, but later often use and achieve similar, so please allow me to treat it as a special Append), or other elements added in a later TypeList TypeList all the elements.

Call the following form:

Append<int, TList>::result_type;
Append<TList, long>::result_type;
Append<TList1, TList2>::result_type;

With variable length template is quite simple to implement:

template <typename, typename> struct Append;
template <typename... TList, typename T>
struct Append<TypeList<TList...>, T> {
    using result_type = TypeList<TList..., T>;
};

template <typename T, typename... TList>
struct Append<T, TypeList<TList...>> {
    using result_type = TypeList<T, TList...>;
};

template <typename... TListLeft, typename... TListRight>
struct Append<TypeList<TListLeft...>, TypeList<TListRight...>> {
    using result_type = TypeList<TListLeft..., TListRight...>;
};

Erase and remove elements EraseAll

As the name suggests, Erase responsible for deleting the first match type, EraseAll delete all match type, which has the same call in the form of:

Erase<TList, int>::result_type;
EraseAll<TList, long>::result_type;

Erase algorithm is relatively simple, the use of recursion, first look at this layer, if the return match rid Head of TypeList, or continue to call Erase the remaining parts:

template <typename TList, typename T> struct Erase;
template <typename Head, typename... Tails, typename T>
struct Erase<TypeList<Head, Tails...>, T> {
    using result_type = typename Append<Head, typename Erase<TypeList<Tails...>, T>::result_type >::result_type;
};

// 终止条件1,删除匹配的元素
template <typename... Tails, typename T>
struct Erase<TypeList<T, Tails...>, T> {
    using result_type = TypeList<Tails...>;
};

// 终止条件2,未发现要删除的元素
template <typename T>
struct Erase<TypeList<>, T> {
    using result_type = TypeList<>;
};

Note template first argument must be a TypeList.

If Head and T does not, we need the help Append the Head stick back TypeList, this is the definition of one of the drawbacks mentioned in that section, because we can not directly expand TypeList type, it is not a variable packet size parameter template. Several yuan function later in the need to use Append to accomplish the same work, not enough to achieve this is indeed elegant compared with the traditional chain.

With Erase, achieve EraseAll on many simple, we only need 1 where the termination condition is not terminated, but for the rest of the list can be continued EraseAll:

template <typename TList, typename T> struct EraseAll;
template <typename Head, typename... Tails, typename T>
struct EraseAll<TypeList<Head, Tails...>, T> {
    using result_type = typename Append<Head, typename EraseAll<TypeList<Tails...>, T>::result_type >::result_type;
};

// 这里不会停止,而是继续把所有匹配的元素删除
template <typename... Tails, typename T>
struct EraseAll<TypeList<T, Tails...>, T> {
    using result_type = typename EraseAll<TypeList<Tails...>, T>::result_type;
};

template <typename T>
struct EraseAll<TypeList<>, T> {
    using result_type = TypeList<>;
};

With Erase and EraseAll, following removal of repeating elements of the membership function can be achieved also.

NoDuplicates remove all duplicate type

NoDuplicates may look very complicated, it is not.

NoDuplicates algorithm requires only three steps:

  1. After removal of the first TypeList Head for NoDuplicates operation, a L1; L1 now ensure that there are no duplicate elements
  2. Head to delete all of the L1 operation, a L2, since there may have L1 and Head like elements;
  3. Finally, add the Head back TypeList

Step 1 recursive calls will repeat the same steps, so that the last TypeList ensures that there will not be repeated elements appear. The yuan is also more commonly used functions, such as you would not want two of the same type appear in the abstract factory class template, which is not correct is not necessary.

Call in the form of:

NoDuplicates<TList>::result_type;

Follow the steps algorithm is not difficult:

template <typename TList> struct NoDuplicates;
template <>
struct NoDuplicates<TypeList<>> {
    using result_type = TypeList<>;
};

template <typename Head, typename... Tails>
struct NoDuplicates<TypeList<Head, Tails...>> {
private:
    // 保证L1中没有重复的项目
    using L1 = typename NoDuplicates<TypeList<Tails...>>::result_type;
    // 删除L1中所有和Head相同的项目,L1中已经没有重复,所以最多只会有一项和Head相同,Erase就够了
    using L2 = typename Erase<L1, Head>::result_type;
public:
    // 把Head添加回去
    using result_type = typename Append<Head, L2>::result_type;
};

In dealing with L1 We only used the Erase, comments have been given reason.

Replace和ReplaceAll

In addition to deleting, occasionally we also want to replace some type to the new type.

Here I will explain the realization Replace, Replace and ReplaceAll like the difference between Erase and EraseAll, is omitted.

Replace is actually a replica of Erase, but it does not delete the matching Head, but it will be replaced with a new type.

template <typename TList, typename Old, typename New> struct Replace;
template <typename T, typename U>
struct Replace<TypeList<>, T, U> {
    using result_type = TypeList<>;
};

template <typename... Tails, typename T, typename U>
struct Replace<TypeList<T, Tails...>, T, U> {
    using result_type = typename Append<U, TypeList<Tails...>>::result_type;
};

template <typename Head, typename... Tails, typename T, typename U>
struct Replace<TypeList<Head, Tails...>, T, U> {
    using result_type = typename Append<Head, typename Replace<TypeList<Tails...>, T, U>::result_type>::result_type;
};

Derived2Front the derived type list to move the front portion

The foregoing are basically the membership function for the parameters of Head and Tails packet decomposition, followed by a recursive process, but the algorithm is now described a bit more complicated.

By giving a Base, we hope TypeList Base in all derived classes can be found in the front of the list, the position before the Base, which can be helpful when you're dealing with inheritance hierarchy, of course, we have not followed the example With this feature, but as an important interface, we still need a certain understanding of.

First, I want to move to the front of the derived class will need to first identify the derived types on the end of the list, we use metafunctions MostDerived a class to help achieve:

template <typename TList, typename Base> struct MostDerived;
// 终止条件,找不到任何派生类就返回Base自己
template <typename T>
struct MostDerived<TypeList<>, T> {
    using result_type = T;
};

template <typename Head, typename... Tails, typename T>
struct MostDerived<TypeList<Head, Tails...>, T> {
private:
    using candidate = typename MostDerived<TypeList<Tails...>, T>::result_type;
public:
    using result_type = std::conditional_t<std::is_base_of_v<candidate, Head>, Head, candidate>;
};

First, we recursive call MostDerived, save the results as candidate, which is the Base list after removal of the Head of the deepest in the derived class or Base ourselves, then we judge whether a candidate Head of the derived class, and if returned Head, otherwise candidate, so that you get a derived class type of the extreme end.

std::conditional_tIs one of the infrastructure c ++ type_traits 11 is provided, the value returned by type bool, and with it we will save the effort of their own to achieve Select.

Help yuan after the completion of the function can begin to realize the Derived2Front:

template <typename TList> struct Derived2Front;
template <>
struct Derived2Front<TypeList<>> {
    using result_type = TypeList<>;
};

template <typename Head, typename... Tails>
struct Derived2Front<TypeList<Head, Tails...>> {
private:
    using theMostDerived = typename MostDerived<TypeList<Tails...>, Head>::result_type;
    using List = typename Replace<TypeList<Tails...>, theMostDerived, Head>::result_type;
public:
    using result_type = typename Append<theMostDerived, List>::result_type;
};

Algorithm steps are not complicated, first find the end of the derived class, then remove TypeList head with the end of the most-derived class the same element with the Head, and finally we have the very end of the derived class is added in front of the treated TypeList to complete the derived class moves from the tip to the front end.

Metafunction realize summary

By way of example these elements function, we can see the modern c ++ programming for the yuan have more built-in support, with the new standard library and language features we can write a lot less code, you can also see achieved before c ++ 11 it seems impossible task.

Of course, modern c ++ also brings its own unique problems, such as the side length of the template parameter packet can not be directly iteration, which leads to most of the time we still need to rely on recursive and partial specialization such classical techniques.

However, it is undeniable that the difficulty with the evolution of meta-programming language, c ++ declining, meta-programming capabilities and expressive code also getting stronger.

Examples

I would like to better show TypeList and the power of modern c ++ by two examples.

The first example is a simple tuple type, mimicking the standard library.

The second example is a factory class, the traditional factory model either can not avoid complex inheritance structures, or can not avoid a large number of hard-coded lead to expansion difficulties, we use TypeList to solve these problems.

Homemade tuple

The first is our toy tuple, simple reason that it is only because we choose to implement the interface to get this one, and the standard library tuple is not ours to achieve, so here's just a toy tuple with demo Bale.

The first is the node we are used to store data:

template <typename T>
struct Data {
    explicit Data(T&& v): value_(std::move(v))
    {}
    T value_;
};

Then we realized Tuple:

template <typename... Args>
class Tuple: private Data<Args>... {
    using TList = TypeList<Args...>;
public:
    explicit Tuple(Args&&... args)
    : Data<Args>(std::forward<Args>(args))... {}

    template <typename Target>
    Target& get()
    {
        static_assert(IndexOf<TList, Target>::value != -1, "invalid type name");
        return Data<Target>::value_;
    }

    template <std::size_t Index>
    auto& get()
    {
        static_assert(Index < Length<TList>::value, "index out of range");
        return get<typename TypeAt<TList, Index>::type>();
    }

    // const的重载
    template <typename Target>
    const Target& get() const
    {
        static_assert(IndexOf<TList, Target>::value != -1, "invalid type name");
        return Data<Target>::value_;
    }

    template <std::size_t Index>
    const auto& get() const
    {
        static_assert(Index < Length<TList>::value, "index out of range");
        return get<typename TypeAt<TList, Index>::type>();
    }
};

// 空Tuple的特化
template <>
class Tuple<> {};

Tuple achieve our simple violence, inheritance through private, we can store a variety of different data at the same time, only when referring to the need Data<type>.value_, so we first get it is easy to achieve, and only need to check whether there is a corresponding TypeList type can be.

But the standard library get there is a second form: get<1>(). For the first get, in fact without the aid of TypeList we can achieve, but for the second we have to use the power of the TypeList, because apart from the order of appearance record than the type of no other way to use meta container (which is also Why such a standard library does not realize one of the reasons the tuple). So we use TypeAtmetadata to find the corresponding function type and then get its value.

Also standard library is not the most important reason is that this form if you are storing the same type of data in the tuple more than two years, will complain, it is easy to imagine why.

Therefore, similar technology is more suitable for variantsuch an object, but here is just an example so we ignore these problems.

Here are some simple tests:

Tuple<int, double, std::string> t{1, 1.2, "hello"};
std::cout << t.get<std::string>() << std::endl;
t.get<std::string>() = "Hello, c++!";
std::cout << t.get<2>() << std::endl;
std::cout << t.get<1>() << std::endl;
std::cout << t.get<0>() << std::endl;

// Output:
// hello
// Hello, c++!
// 1.2
// 1

Simplify factory pattern

Suppose we have a WidgetFactory, kind of Widgets, Widgets are used to create different styles there are many, such as Button, Label and so on:

class WidgetFactory {
public:
    virtual CreateButton() = 0;
    virtual CreateLabel() = 0;
    virtual CreateToolBar() = 0;
};

// 风格1
class KDEFactory: public WidgetFactory {
public:
    CreateButton() override;
    CreateLabel() override;
    CreateToolBar() override;
};

// 风格2
class GnomeFactory: public WidgetFactory {
public:
    CreateButton() override;
    CreateLabel() override;
    CreateToolBar() override;
};

// 使用
WidgetFactory* factory = new KDEFactory;
factory->CreateButton(); // KDE button
delete factory;
factory = new GnomeFactory;
factory->CreateButton(); // Gnome button

This implementation has two problems, one is if the increase / change / reduction product, you need to change a lot of code, error-prone; the second is to create a different kind of widget code is often more similar, so we need to constantly repeat here themselves, which is often one of the root causes of the bug.

What is more ideal form is it? If the widget configuration procedure is the same, but there are differences in the parameters, you might have thought, we have longer templates and perfect forwarding:

class WidgetFactory {
public:
    template <typename T, typename... Args>
    auto Create(Args&&... args)
    {
        return new T(std::forward<Args>(args)...);
    }
};

In this way we can Create<KDEButton>(...)create different objects, however this is not a factory, we create one of the purposes of the plant is to limit the types of products, but now we put restrictions lifted!

So then solve it? The answer is TypeList, TypeList by limiting the types of products:

template <typename... Widgets>
class WidgetFactory {
    // 我们不需要重复的类型
    using WidgetList = NoDuplicates<TypeList<Widgets...>>::result_type;
public:
    template <typename T, typename... Args>
    auto Create(Args&&... args)
    {
        static_assert(IndexOf<WidgetList, T>::value != -1, "unknow type");
        return new T(std::forward<Args>(args)...);
    }
};

using KDEFactory = WidgetFactory<KDEButton, KDEWindow, KDELabel, KDEToolBar>;
using GnomeFactory = WidgetFactory<GnomeLabel, GnomeButton>;

Now if we want to add or change a product of a factory, only need to modify the code to a limited number, but we have limited the product categories while repeating code abstract set. At the same time, compile-type checking is handled without any run-time costs!

Of course, the downside is that this simplified flexibility decreases because of the different factories now essence is not related to different types, it is impossible by Base*or Base&associated with them, but for the same interfaces, but the same type of object, we can still achieve a static dispatch template-dependent, this is just the design trade-offs only.

to sum up

This article is to discuss the entry-level template meta-programming, designed to introduce modern c ++ if you use a simplified yuan and generic programming tasks.

While this article can not take you on meta-programming, but allows you to have an overall overview of the concept of meta-programming, in-depth study is helpful.

Guess you like

Origin www.cnblogs.com/apocelipes/p/11289840.html