variadic
patterns, type_traits library, tuples and constexpr
's, metaprogramming has become more convenient and visual, opening the way to the realization of many ideas that previously could be implemented only with the help of extensions of a specific compiler or complex multi-storeyed macros.if X is a node and Y is in the left subtree of X , then Y.key ≤ X.key . If the node Y is in the right subtree of X , then X.key ≤ Y.key .It is understood that we are able to compare keys (a transitive order relation is set on a set of key values, that is, simply speaking, the operation is “less”) and to talk about equality of keys. In the implementation without loss of generality, we will operate with strict order inequalities, using only the operation "<" and "==", built on its basis from the relation
x = y \; \ Leftrightarrow \; \ ;! (x <y) \: \ & \ :! (y <x)
template <class U, class V> struct tuple_concat; template <class Tuple, class T> struct tuple_push;
type_traits
header file (as part of a type support library) containing a collection of useful meta-functions. All of them are patterns of structures, and after instantiation, they locally define some type
or a numeric value
constant (or nothing, as in the case of disconnecting the overload using std::enable_if
). We will adhere to the same design principles of meta-functions.int
and float
) is a meaningless operation, so the basic pattern of these structures is not defined (this will prevent it from being instantiated for arbitrary types), and all the useful work is done in partial specializations : template <template <class...> class T, class... Alist, class... Blist> struct tuple_concat<T<Alist...>, T<Blist...>> { using type = T<Alist..., Blist...>; }; template <template <class...> class Tuple, class... Args, class T> struct tuple_push<Tuple<Args...>,T> { using type = Tuple<Args..., T>; };
tuple_concat
:T
) with a variable number of arguments, and they were instantiated with the Alist
and Blist
parameter Blist
, then locally define the pseudonym type
as instantiated the version of the same template of class T
with a concatenated list of arguments, i.e. T<Alist..., Blist...>
.tuple_concat
with two tuples of the same type (for example, with two std::tuple
), the type of the same tuple with the “stitched” list of arguments of the input tuples is determined inside the structure. Other instantiations of instantiation are simply not compiled (the compiler will not be able to infer types for the partial specialization defined above, and instantiating a common pattern will be impossible due to the absence of its body). Example: using t1 = std::tuple<char,int>; using t2 = std::tuple<float,double>; using t3 = std::tuple<char,int,float,double>; using conc = typename tuple_concat<t1,t2>::type; // using err = typename tuple_concat<int,bool>::type; // static_assert(std::is_same<t3,conc>::value, ""); // ,
tuple_push
specialization tuple_push
not be a big deal. Additionally, for convenience, we define the corresponding template aliases : template <typename U, typename V> using tuple_concat_t = typename tuple_concat<U,V>::type; template <typename Tuple, typename T> using tuple_push_t = typename tuple_push<Tuple,T>::type;
typename tuple_concat<t1,t2>::type
tuple_concat_t<t1,t2>
to access type
instead of the typename tuple_concat<t1,t2>::type
.tuple
header contains a definition (not a meta-) of the tuple_cat()
) function, which constructs a tuple by concatenating an unspecified number of std::tuple
'passed to it as arguments. An attentive reader may notice that tuple_concat
can be more easily implemented by outputting the result type decltype(tuple_cat(...))
, however, firstly, the implementation obtained above is not limited to the type of the std::tuple
, and secondly, it was warm-up exercise for the gradual immersion in more complex type arithmetic. /// - STL-like template<size_t number> struct num_t : std::integral_constant<size_t, number> {}; // template<size_t number> struct num_t { enum : size_t { value = number } };
num_t<0>
, num_t<13>
, num_t<42>
, etc. No more than for convenience, we endow this structure with a static numeric value
, which will allow us to explicitly get back a number from the template argument (by accessing some_num_type::value
) without resorting to type inference.WOOD: NIL | [TREE, DATA, TREE]
Comp
( comparator , let 's talk about it further). /// struct NIL {}; /// template<typename T, typename Left, typename Right, typename Comp> struct node { using type = T; // node value using LT = Left; // left subtree using RT = Right; // right subtree using comp = Comp; // types comparator }; /// : ( ) template <typename T, typename Comp> using leaf = node<T,NIL,NIL,Comp>;
template<...> struct one { struct two; using three = one<two, ...>; struct two : one<three, ...> {}; };
Note: it was experimentally found that such structures are compiled and instantiated by modern gcc and clang, however, I have not yet verified the strict adherence to the standard for declarations of such unusual patterns.However, in practice it is very, VERY difficult to work with such entities and create them. The reciprocal reference to the parent element produces an interesting effect: in fact, our "simply connected tree" turns into a real graph (tasty!), Which, with any modification, must instantiate itself "at a time", again and completely (sadly). A deeper analysis of this situation is beyond the scope of this article and is among my future plans for exploring the possibilities of metaprogramming in C ++.
Comp
structure must contain type comparison metafunctions (i.e., less than and equal to operation patterns). Let us write an example of such a comparator based on the sizeof
'ah types (perhaps, the only numerical characteristic defined for all full types): struct sizeof_comp { template <typename U, typename V> struct lt : std::integral_constant<bool, (sizeof(U) < sizeof(V))> {}; // template <typename U, typename V> struct eq : std::integral_constant<bool, (sizeof(U) == sizeof(V))> {}; // };
lt
is a “less” metafunction for types, eq
is an “equal” metafunction. The approach shown earlier for determining the types of numbers was used: inheritance from std::integral_constant
will endow the instantiated lt
and eq
static Boolean values. struct num_comp { template <typename U, typename V> struct lt : std::integral_constant<bool, (U::value < V::value)> {}; template <typename U, typename V> struct eq : std::integral_constant<bool, (U::value == V::value)> {}; };
value
. /// template <typename lt_traits> struct eq_comp { template <typename U, typename V> struct lt : std::integral_constant<bool, lt_traits::template lt<U,V>::value> {}; template <typename U, typename V> struct eq : std::integral_constant<bool, (!lt<U,V>::value && !lt<V,U>::value)> {}; }; /// sizeof, eq_comp::eq struct sizeof_comp : public eq_comp<sizeof_comp> { template <typename U, typename V> struct lt : std::integral_constant<bool, (sizeof(U) < sizeof(V))> {}; }; /// num_t struct num_comp : public eq_comp<num_comp> { template <typename U, typename V> struct lt : std::integral_constant<bool, (U::value < V::value)> {}; };
The size of the definition has decreased and a mathematical background has appeared - isn't it beautiful? using t1 = node< num_t<5>, node< num_t<3>, leaf<num_t<2>>, leaf<num_t<4>> >, node< num_t<7>, NIL, leaf<num_t<8>> > >;
Note: Here and hereinafter in the examples, the default is to comparenum_comp
, explicitly indicating it in the list of template arguments is omitted. In general, after the development of theinsert
operation, we will not have to build trees in this way (by definition).
printf/std::cout
. There are techniques that allow typing readable inferred types inside the compiler error messages, and, in principle, this is quite a useful feature for checking generated structures (for example, after modifying a tree).type_traits
can be simply printed as typeid(t).name()
(starting from C ++ 11, you can legally look into RTTI). Simple data structures can be displayed on the screen with special metafunctions with tail recursion; for complex ones, printers will have to be built, comparable in complexity to operations on the structure itself./ - {2} / - {3} - < \--{four} - {5} --- < \ - {7} - \ \--{eight}
/// Input: T - tree, exit: h - height
HEIGHT (T): IF T == NIL OUTPUT 0 ANYWAY OUTPUT 1 + MAX (HEIGHT (T.LEFT), HEIGHT (T.RIGHT))
/// template <typename Tree> struct height; /// : " T == NIL" template <> struct height<NIL> : std::integral_constant<size_t, 0> {}; /// () template <typename Tree> struct height { static constexpr size_t value = 1 + max( height<typename Tree::LT>::value, height<typename Tree::RT>::value ); };
Note: we deliberately went for a small simplification, because of which the calculated tree height will be 1 more than its mathematical definition (the height of the empty set is not defined). , 1 , height
.
: height
, value = 0
, , ( NIL
), . C++: , ( ), ( Tree::LT
, Tree
NIL
).max
. constexpr
( , ), : template <typename T> constexpr T max(T a, T b) { return a < b ? b : a; }
height
: static_assert(height<t1>::value == 3, "");
height
: n , n — ; h — .. .constexpr
- variadic- .height
gcc, > 900. O - ( ), .std::integer_sequence
): N 0..N-1, , . , , ( , 30- 9000- )./// Input: T - tree, output: w - list of data from in-order bypass nodes
INORDER (T): IF T == NIL CONCLUSION {} ANYWAY INORDER(T.LEFT) + {T.KEY} + INORDER(T.RIGHT)
/// template <typename Tree> struct walk; /// : " T == NIL" template <> struct walk<NIL> { using type = std::tuple<>; }; /// () template <typename Tree> struct walk { private: // using accL = typename walk<typename Tree::LT>::type; // using accX = tuple_push_t<accL, typename Tree::type>; // using accR = tuple_concat_t<accX, typename walk<typename Tree::RT>::type>; public: // using type = accR; };
private
: , , . , ?walk
: O(n) , n — ( O - : 3n ). , tuple_concat
tuple_push
1 , ( parameter pack
'). , height
, h — .Comp
. :/// : T - , k - -, /// : N - , k` == k
SEARCH(T,k): T == NIL k == T.KEY T k < T.KEY SEARCH(T.LEFT, k) SEARCH(T.RIGHT, k)
/// template <typename Tree, typename T> struct search; /// template <typename T> struct search<NIL,T> { using type = NIL; }; /// template <typename Tree, typename T> struct search { using Comp = typename Tree::comp; using type = typename std::conditional< Comp::template eq<T, typename Tree::type>::value, // k == T.KEY ? Tree, // , : typename std::conditional< Comp::template lt<T, typename Tree::type>::value, // k < T.KEY ? typename search<typename Tree::LT, T>::type, // typename search<typename Tree::RT, T>::type // -- >::type >::type; };
Tree::comp
, : , ( , ), ( node<>
).Tree::comp::eq<...>
Tree::comp::lt<...>
), template
.std::conditional
— , ( ?: ). — . , — .search
— - O(n) , — h ( ). «!» — , — « ?»std::conditional
?: : , (, , ). std::conditional
( ), std::conditional
, . using found3 = search_t<NIL, num_t<0>, num_comp>; // using found4 = search_t<t1, num_t<5>, num_comp>; // using found5 = search_t<t1, num_t<8>, num_comp>; // static_assert(std::is_same<found3, NIL>::value, ""); // static_assert(std::is_same<found4, t1>::value, ""); // static_assert(std::is_same<found5, leaf<num_t<8>>>::value, ""); //
std::map
) trees are also stored in the nodes of the pair ( std::pair
), and the first element of the pair is considered the key , which, in fact, participates in comparisons. It is enough to keep the same in our tree std::pair
and make the comparator Comp
compare pairs of the first type in a pair - and get a classic associative (meta) container! We will return to this idea at the end of the article./// Input: T is a tree, k is a key type for insertion, /// exit: T 'is a new tree with an inserted element
INSERT (T, k): IF T == NIL CONCLUSION {NIL, k, NIL} ANYWAY IF k <T.KEY OUTPUT {INSERT (T.LEFT, k), T.KEY, T.RIGHT} ANYWAY CONCLUSION {T.LEFT, T.KEY, INSERT (T.RIGHT, k)}
template <typename Tree, typename T, typename Comp = typename Tree::comp> struct insert; /// template <typename T, typename Comp> struct insert<NIL,T,Comp> { using type = leaf<T,Comp>; }; /// template <typename Tree, typename T, typename Comp> struct insert { using type = typename std::conditional< Comp::template lt<T, typename Tree::type>::value, // k < T.KEY? node<typename Tree::type, // {INSERT(T.LEFT,k), T.KEY, T.RIGHT} typename insert<typename Tree::LT, T, Comp>::type, typename Tree::RT, Comp>, node<typename Tree::type, // {T.LEFT, T.KEY, INSERT(T.RIGHT,k)} typename Tree::LT, typename insert<typename Tree::RT, T, Comp>::type, Comp> >::type; };
Comp
; , - * . using t2 = leaf<num_t<5>, num_comp>; using t3 = insert_t<t2, num_t<3>>; using t4 = insert_t<t3, num_t<7>>; static_assert(height<t4>::value == 2, ""); // 2 using t5 = insert_t<insert_t<insert_t<t4, num_t<2>>, num_t<4>>, num_t<8>>; static_assert(std::is_same<t5, t1>::value, ""); //
insert_tuple
, ( insert
), : using t6 = insert_tuple_t<NIL, std::tuple< num_t<5>, num_t<7>, num_t<3>, num_t<4>, num_t<2>, num_t<8> >, num_comp>; static_assert(std::is_same<t1, t6>::value, "");
/// : T - , l - , /// : t -
COLLECT_LEVEL(T,l): T == NIL {} l == 0 {T.KEY} COLLECT_LEVEL(T.LEFT,l-1) + COLLECT_LEVEL(T.RIGHT,l-1)
collect_level
).std::tuple
— . , runtime , , ( std::get
std::tuple
).std::map
( std::set
) — , (, ) ( — , ..), compile-time, runtime . : , , . : ?static_assert
, std
- ( — tutorial ). , .std::conditional
. search
O(h) : template <typename Node, typename T, typename Comp> struct search { using type = typename std::conditional< Comp::template eq<T, typename Node::type>::value, Node, typename search<typename std::conditional< Comp::template lt<T, typename Node::type>::value, typename Node::LT, typename Node::RT >::type, T, Comp>::type >::type; };
(+ insert
) .insert
search
remove
O(h) ( insert
). SFINAE + decltype
( , . ). remove
: template <typename Tree, typename T> struct remove { private: enum : bool { is_less = Tree::comp::template lt<T, typename Tree::type>::value }; enum : bool { is_equal = Tree::comp::template eq<T, typename Tree::type>::value }; enum : size_t { children = children_amount<Tree>::value }; using key = typename min_node_t< typename std::conditional< is_equal && children == 2, typename Tree::RT, leaf<typename Tree::type, typename Tree::comp> >::type >::type; using recursive_call = typename remove< typename std::conditional< is_less, typename Tree::LT, typename std::conditional< !is_equal || children == 2, typename Tree::RT, NIL >::type >::type, typename std::conditional< is_equal, key, T >::type >::type; static typename Tree::RT root_dispatcher(...); template <typename Bush> static typename std::enable_if< sizeof(typename Bush::LT::type), typename Bush::LT >::type root_dispatcher(Bush&&); public: using type = typename std::conditional< is_equal && (children < 2), decltype(root_dispatcher(std::declval<Tree>())), node< key, typename std::conditional< is_less, recursive_call, typename Tree::LT >::type, typename std::conditional< !is_less, recursive_call, typename Tree::RT >::type, typename Tree::comp > >::type; };
, , . , , , , .Source: https://habr.com/ru/post/320686/
All Articles