It is possible to implement a type trait, sort
, which, provided a certain type T
, defines a tuple containing all the types of the original tuple sorted according to a strict-weak ordering criterion. If the type T
is not a specialization of the std::tuple
class, the program is ill-formed.
Implementation
Insertion sort has been chosen as sorting algorithm. The reasons can be summarized as the best trade-off between simplicity and performance.
It works by repeatedly extracting an element from the input list and inserting it into the correct position within the sorted list. In particular, the algorithm can be divided into two components: the main loop, which takes an element from the unsorted range and passes it to the insert operation; the insert function, which performs a linear probing on the sorted range until the correct position, where the new element is placed.
The average cost has a quadratic computational cost (O(N²)). This makes it not a good generic sorting algorithm for large lists. However, it performs efficiently on small or partly sorted lists, even better than quicksort.
Example:
namespace detail {
template <typename, typename, typename, template <typename...> typename>
struct insert
{};
template <typename T, typename ...Ts, template <typename...> typename Comp>
struct insert<T, std::tuple<Ts...>, std::tuple<>, Comp>
{ using type = std::tuple<Ts..., T>; };
template <typename T, typename ...Ts, typename U, typename ...Us, template <typename...> typename Comp>
struct insert<T, std::tuple<Ts...>, std::tuple<U, Us...>, Comp>
{ using type = std::conditional_t<!Comp<T, U>::value, typename insert<T, std::tuple<Ts..., U>, std::tuple<Us...>, Comp>::type, std::tuple<Ts..., T, U, Us...>>; };
template <typename, typename, template <typename...> typename>
struct sort
{};
template <typename ...Ts, typename U, typename ...Us, template <typename...> typename Comp>
struct sort<std::tuple<Ts...>, std::tuple<U, Us...>, Comp>
: sort<typename insert<U, std::tuple<>, std::tuple<Ts...>, Comp>::type, std::tuple<Us...>, Comp> {};
template <typename ...Ts, template <typename...> typename Comp>
struct sort<std::tuple<Ts...>, std::tuple<>, Comp>
{ using type = std::tuple<Ts...>; };
}
template <typename T, template <typename...> typename Comp = less>
struct sort
: detail::sort<std::tuple<>, T, Comp> {};
template <typename T, template <typename...> typename Comp = less>
using sort_t = typename sort<T, Comp>::type;
Design choices
1. Insert operation
In a generic insertion sort, the insert operation is performed by probing the sorted list in reverse order, while, for each element, it is compared to the new one. If the new element is greater than or equal to the current one, it is inserted; otherwise, the operation is repeated. It is important to note that, if the sorted list is empty or the entire list has already been probed without finding a proper position, the new element is inserted at the beginning of the sequence.
The implementation is similar to the description, but has an important difference: the linear probing is performed forwards. It derives from the fact that a parameter pack can not be expanded backwards.
The sort
class extracts the first type of the input list and passes it to the insert
class. This maintains the type T
to be inserted and two tuples, the left list, which contains all types less than or equal to T
, and the right list, which contains the remaining types. If the type T
is greater than the first type U
of the right list, it is inserted in the current position; otherwise, the type U
is moved to the left list and the operation is repeated. It is important to note that, if the right is empty, the type T
is directly inserted.
2. Best / Worst cases
If the list is already sorted, it is in the best case, which reduces the computational cost to linear (O(N)). This happens because, at each iteration, as it is verified that the new element is greater than or equal to the rightmost element in the sorted list, the insert operation is not even performed. The worst case, which has also a quadratic computational cost, may happen if the input list is sorted in the reverse order. Specifically, the sorting algorithm performs a linear probing on i elements, in the insert operation, for n times. Provided that i represents the length of the sorted list, which increases by one at each insert operation, the total number of passes is n(n + 1)/2.
They would be the best and worst cases if the linear probing was performed backwards. However, the metaprogramming algorithm, due to the limitation of the parameter pack expansion, can perform the scanning only forwards. This means that the common conditions that lead to the best case and worst case are swapped: if the input list is sorted in reverse order, the computational cost is linear; otherwise, the computational cost remains quadratic. When the input list is already sorted, the maximum number of passes is executed.
3. Ordering criterion
The greatest limitation with types is that they can not be compared. Thus, it is necessary to identify a different way to define the strict-weak ordering criterion, which can be potentially performed on any couple of types. The simplest solution concerns with the comparison between the sizes of types, which can be computed at compile-time through the sizeof operator and, since the returning type is always an unsigned integer, allows to have a trivial less-than comparison.
The solution determines also a performance improvement: by ordering types in an increasing or decreasing order, the structure can be packed in such a way that the padding be minimized.
It is important to note that the order is actually considered either ascending or descending, since there is no guarantee that the implementation of the std::tuple
class store types in the same order as template arguments are defined. Indeed, the C++ Standard does not mandate any particular order for storing types, but just requires that they can be accessed with the same index as that of the corresponding template argument. Provided that implementations that may store types in a completely random order are not considered in this analysis, it is possible to conclude that the requirements allow libraries to pack types in two opposite orders. This divides the three greatest libraries into two groups: libc++, which store types in the same order as template arguments are declared; libstdc++, MSVC, which store types in the reversed order.
4. Comparison object
A more generic approach would be the use of a custom compare object. In this way, it would be possible to specify the type as a template argument, which may or may not be defaulted, with the possibility of defining a more appropriated strict-weak ordering criterion.
To achieve this goal, it is possible to exploit the existing practice of standard type traits in order that the metaprogramming version of the compare object can be defined in a well-known way. However, in absence of a standard design, an alternative one can be used, maybe inspired by robust and popular libraries, such as Boost.
In this case, the tmpbook's design is adopted. With a little adjustment, the generic comparison object can be defined in the following way.
A template class Comp
, which can be specialized for two types T
, U
, is a metaprogrammaing comparison object if its static constant attribute value
is contextually convertible to bool
, and yields true
if the first argument appears before the second argument in the strict-weak ordering criterion and false
otherwise.
For simplicity, the default comparison object less
, which performs a less-than comparison between the size of the first type and that of the second type, is provided.
Example:
template <typename T, typename U>
struct less
: std::bool_constant<sizeof(T) < sizeof(U)> {};
Unique sorting is a sorting that, for every permutation of a certain list of elements, rearrange it in an unique manner. This means that, given an ordering criterion Comp, for every couple of elements x, y from a set S, with x ≠ y, either the Comp(x, y) or Comp(y, x) expression is always true.
In your case, solution would be to obtain a distinct value for every type. For example, it would be useful to get the original name of the type as a string and then performs a lexicographical comparison.
The problem is that C++ does not offer a generic type reflection. As a result, many pathetic attempts have been made to emulate the same effect. The best one is the use of a GCC identifier, PRETTY_FUNCTION, which holds the name of the specified function printed in a language specific fashion.
T != T2
butsizeof(T1) == sizeof(T2)
? How would you uniquely sort them so thatstd::is_same
works as expected? – Heinousstd::less<>
for types, or such. – HeinousT1
andT2
are equal size, as any ordering (for such pair) would work then. – Heinousstd::is_same
. So not sure about OP's eventual goal. – Heinousconstexpr
all the things" proposal. Maybe... – Ovule