10

I'm trying to do some template metaprogramming and I'm finding the need to "extract" the highest index of a specialization of some structure in some type.

For example, if I have some types:

struct A
{
    template<unsigned int> struct D;
    template<> struct D<0> { };
};

struct B
{
    template<unsigned int> struct D;
    template<> struct D<0> { };
    template<> struct D<1> { };
};

struct C
{
    template<unsigned int> struct D;
    template<> struct D<0> { };
    template<> struct D<1> { };
    template<> struct D<2> { };
};

How can I then write a metafunction like this:

template<class T>
struct highest_index
{
    typedef ??? type;
    // could also be:   static size_t const index = ???;
};

to give me the highest-indexed D that has been specialized inside an arbitrary struct like the ones above, without requiring the struct to have declared the count explicitly?

TemplateRex
  • 69,038
  • 19
  • 164
  • 304
user541686
  • 205,094
  • 128
  • 528
  • 886
  • Let's try a simpler problem first: given a general class template `D` and a specialization `D<0>`, how are you going to detect at compile time whether `D` is a specialization? – TemplateRex Jan 08 '13 at 08:20
  • In other words, how would a type trait `is_specialization_of` look like, for general type `T` and specialization `S`? – TemplateRex Jan 08 '13 at 08:26
  • I think it is impossible to say that. Because for ANY index `i`, we will always have a valid type `T::D`. Now it is impossible to say `D` is a explicit specialization, or not. Since the type `D` is always valid, we cannot use SFINAE. – Nawaz Jan 08 '13 at 08:26
  • I also don't think this is possible without requiring a specific typedef / index member in the `D` struct, since the idea is that you can't differentiate between a base template and its explicit specializations just like that. – Xeo Jan 08 '13 at 08:35
  • @Xeo, Nawaz: I can rely on the base template being undefined like in the examples -- would that help, or would it still be impossible? – user541686 Jan 08 '13 at 08:43
  • @rhalbersma: I don't know that either, I'll try to see if I can figure out a way for that. – user541686 Jan 08 '13 at 08:44
  • @Mehrdad: That might actually work - you'd just need to recursively SFINAE-check if `D` is defined. – Xeo Jan 08 '13 at 08:46
  • @Mehrdad: I don't think so, as undefined-ness of struct is caught at the linker time, not at the compile-time. which means that doesn't make any difference, IMO. – Nawaz Jan 08 '13 at 08:48
  • @Xeo: Yup, but *how* do I check if it's defined? (That's what rhalbersma mentioned as well I think.) I'm trying approaches with overloading right now (e.g. `test(...)` and `test(T const &, blah = T::something)`) but I haven't figured out a way yet. – user541686 Jan 08 '13 at 08:50
  • Do you have a guarantee for *anything* being defined in all specializations? Like, default-ctor or some member function? – Xeo Jan 08 '13 at 08:51
  • @Xeo: Yeah I can make them all inherit from a class, if that helps. A constructor should also be possible... although there is a default constructor anyway right? – user541686 Jan 08 '13 at 08:51
  • @Nawaz: Isn't it a compile time issue? Detecting whether a type is defined should be a compile issue... – user541686 Jan 08 '13 at 08:52
  • @Xeo: On another note, I just came across an interesting bug in VC++... http://stackoverflow.com/questions/3052579/explicit-specialization-in-non-namespace-scope – user541686 Jan 08 '13 at 08:55
  • @Mehrdad: You're right. I think `sizeof` can help here too. Let me try! – Nawaz Jan 08 '13 at 08:58
  • @Nawaz: Haha okay thanks. :) I'm trying to fix the namespace-level declaration bug now lol... 'cause apparently this example isn't even standard C++. – user541686 Jan 08 '13 at 09:00
  • [Here](http://stacked-crooked.com/view?id=6bf65f18c218cc1e9bd404687a2b2612)'s a basic mock-up for the SFINAE part. The recursive testing is left as an excercise to the reader. ;) (Note: Divide-and-conquer may help if you know the max index and if it is particularly big.) – Xeo Jan 08 '13 at 09:04
  • @Xeo: Ooh huh, seems like `enable_if` is a better idea than overloading... cool, lemme try. :) – user541686 Jan 08 '13 at 09:10

3 Answers3

4

This is the first version which gets you the maximum index for which specialization is defined. From this, you will get the corresponding type!

Implementation:

template<class T>
struct highest_index
{
  private:
     template<int i>
     struct is_defined {};

     template<int i>
     static char f(is_defined<sizeof(typename T::template D<i>)> *);

     template<int i>
     static int f(...);

     template<int i>
     struct get_index;

     template<bool b, int j>
     struct next
     {
        static const int value = get_index<j>::value;
     };
     template<int j>
     struct next<false, j>
     {
        static const int value = j-2;
     };
     template<int i>
     struct get_index
     {
        static const bool exists = sizeof(f<i>(0)) == sizeof(char);
        static const int value = next<exists, i+1>::value;
     };

    public:
     static const int index = get_index<0>::value; 
};

Test code:

#include <iostream>

struct A
{
    template<unsigned int> struct D;
};
template<> struct A::D<0> { };
template<> struct A::D<1> { };

struct B
{
    template<unsigned int> struct D;
};
template<> struct B::D<0> { };
template<> struct B::D<1> { };
template<> struct B::D<2> { };


int main()
{
    std::cout << highest_index<A>::index << std::endl;
    std::cout << highest_index<B>::index << std::endl;
}

Output:

1
2

Live demo. :-)

Nawaz
  • 353,942
  • 115
  • 666
  • 851
  • @Mehrdad: I'm thinking of reducing the size of the implementation. It looks too long! But then I suppose it is easy to read. – Nawaz Jan 08 '13 at 09:40
  • 1
    Yours looks a heck of a lot better than mine. :) What's funny is that Visual Studio can't compile either of ours! Apparently it chokes on `sizeof()`. – user541686 Jan 08 '13 at 09:43
  • @Mehrdad: Realizing that `is_defined` class template, though, adds readability to the code, is not really needed. One could use `char (*) [sizeof(typename T::template D)] ` as parameter type of the function template `f`, like [this](http://stacked-crooked.com/view?id=29c512ef94f491b1e8eb406bb0b6b870) :-) – Nawaz Jan 08 '13 at 10:14
  • @Nawaz: While it works, I would point out that the complexity is O(N); it will therefore (in all likelihood) fail utterly in the case where `D` is unconstrained or specialized for a wide range of inputs. I think it could be reworked into a binary search to limit the recursion depth. – Matthieu M. Jan 08 '13 at 10:14
  • @Nawaz: I tried it (in `constexpr` methods), however it's extremely slow; I am afraid that `?:` patterns are not lazily evaluated (ie, both branches are systematically evaluated). – Matthieu M. Jan 08 '13 at 11:08
  • @MatthieuM.: Yes, And short-circuiting doesn't work with template instantiation also, something like this : [Short-circuiting while instantiating template?](http://stackoverflow.com/questions/4600565/short-circuiting-while-instantiating-template) – Nawaz Jan 08 '13 at 11:11
  • @Nawaz: the short-circuiting you refer to speaks of classes, while I have a function here. I expect the compiler to check that both alternatives are correctly typed, but whether it computes the values is different. – Matthieu M. Jan 08 '13 at 11:18
  • @MatthieuM.: I saw your code, and I think *the same* reasoning which Johannes gave in his comment, applies to your case also. He said : *"The Standard doesn't allow them to do "short circuit" here. That's why you need to implement it yourself, as done here. The reason is simple: You need to know the type of `other::value` and whether it is convertible to `bool` at all and makes sense. For doing so `other` needs to be instantiated. What if its type would yield to an overloaded `op||` being called? The initializer would be invalid then."* – Nawaz Jan 08 '13 at 11:38
  • @Nawaz: Thanks to Luc Touraille I finally got a version that binary searches for the value (yeeha), I find it quite unfortunate that I am required to abandon the functional way though... – Matthieu M. Jan 08 '13 at 14:42
2

Figured it out with the help from the comments under the question!

struct A { template<unsigned int> struct D; };
template<> struct A::D<0> { };

struct B { template<unsigned int> struct D; };
template<> struct B::D<0> { };
template<> struct B::D<1> { };

struct C { template<unsigned int> struct D; };
template<> struct C::D<0> { };
template<> struct C::D<1> { };
template<> struct C::D<2> { };
template<> struct C::D<3> { };

template<unsigned int>
static unsigned char test(...);

template<unsigned int N, class T>
static typename enable_if<
    sizeof(typename T::template D<N>),
    unsigned char (&)[1 + sizeof(test<N + 1>(T()))]
>::type test(T, typename T::template D<N> = typename T::template D<N>());

int main()
{
    return sizeof(test<0>(C())) - 1;  // Evaluates to number of specializations
}
user541686
  • 205,094
  • 128
  • 528
  • 886
1

Here is my little contribution.

We start off with the existence methods:

template <unsigned>
static unsigned char exists_impl(...);

template <unsigned N, typename T>
static auto exists_impl(T const&&) ->
    typename std::enable_if<sizeof(typename T::template D<N>),
                            unsigned char (&)[2]>::type;

template <typename T, unsigned N>
static constexpr bool exists() {
    return sizeof(exists_impl<N>(std::declval<T>())) != 1;
}

I believe here that constexpr and function usage do bring a lot to the table in terms of readability, so I don't use the typical types.

Then, we use a typical binary search (2nd attempt, see first attempt at bottom), at a loss of readability, but to benefit from lazy instantiation, we use partial template specialization and std::conditional:

template <typename T, unsigned low, unsigned high, typename = void>
struct highest_index_in;

template <typename T, unsigned low>
struct highest_index_in<T, low, low>: std::integral_constant<unsigned, low> {};

template <typename T, unsigned low, unsigned high>
struct highest_index_in<T, low, high, typename std::enable_if<(high == low + 1)>::type>:
  std::integral_constant<unsigned, low + exists<T, low+1>()> {};

template <typename T, unsigned low, unsigned high>
struct highest_index_in<T, low, high, typename std::enable_if<(high > low + 1)>::type>:
  std::conditional< exists<T, (low+high)/2>(),
                    highest_index_in<T, (low+high)/2, high>,
                    highest_index_in<T, low, (low+high)/2> >::type
{};

template <typename T>
static constexpr unsigned highest_index() {
   return highest_index_in<T, 0, ~(0u)>::value;
} // highest_index

Demo at liveworkspace, computing highest_index<C>() is near instantaneous.


First attempt at binary search, unfortunately the compiler need instantiate the function bodies recursively (to prove they can be instantiated) and thus the work it has to do is tremendous:

template <typename T, unsigned low, unsigned high>
static constexpr auto highest_index_in() ->
   typename std::enable_if<high >= low, unsigned>::type
{
   return low == high                 ? low :
          high == low + 1             ? (exists<T, high>() ? high : low) :
          exists<T, (high + low)/2>() ? highest_index_in<T, (high+low)/2, high>() :
                                        highest_index_in<T, low, (high+low)/2>();
} // highest_index_in

template <typename T>
static constexpr unsigned highest_index() {
   return highest_index_in<T, 0, ~(0u)>();
} // highest_index

So, unfortunately, highest_index is not usable and the clang is dang slow (not that gcc appears to be doing better).

Matthieu M.
  • 287,565
  • 48
  • 449
  • 722
  • I wonder why SFINAE programmers use `char[1]` and `char[2]` or `unsigned char (&)[2]` (as you've used in this case). Wouldn't `char` and `int` work just fine as their sizes are guaranteed to be different? – Nawaz Jan 08 '13 at 11:15
  • Or... your could just make it a class template, which ensures lazy-evaluation (like [here](http://stackoverflow.com/a/8763548/500104)). @Nawaz: `char` and `int` could be the same size. :) – Xeo Jan 08 '13 at 11:15
  • @Nawaz: I am not sure, I don't think there is a guarantee that `char` and `int` don't have the same size. – Matthieu M. Jan 08 '13 at 11:16
  • @Xeo: Means `int` could be just `1` byte? How exactly? – Nawaz Jan 08 '13 at 11:17
  • @Nawaz: normally `int` is specified in terms of a minimum range of possible values, while a byte is defined as `sizeof(char)`, thus in theory there is nothing preventing a platform to define 32 bits `char` types. – Matthieu M. Jan 08 '13 at 11:20
  • @Xeo: I must be missing something, I don't see where the lazy initialization kicks in here. I've asked the question of lazy evaluation of the ternary operator [here](http://stackoverflow.com/questions/14213754/may-a-compiler-elide-the-evaluation-of-the-not-taken-branch-in-a-constexpr-fun). – Matthieu M. Jan 08 '13 at 11:22
  • 1
    I think I picked the wrong link, but the point still stands - make it a class template and partially-specialize it on the conditions. – Xeo Jan 08 '13 at 11:28
  • @LucTouraille: There's `std::conditional`, which is exactly your `if_`. Just as an FYI. :) – Xeo Jan 08 '13 at 13:52
  • I took the liberty of editing your answer with a solution using `enable_if`; I don't know if this kind of "heavy" editing is accepted on SO, feel free to revert if you disagree with my initiative. – Luc Touraille Jan 08 '13 at 13:52
  • @Xeo: Thanks for the reminder, I'm so often disappointed by the lack of metafunctions in the standard library that I keep using Boost.MPL, so I often forget those that were actually included in C++11 :). – Luc Touraille Jan 08 '13 at 13:58
  • @LucTouraille: Ah thanks, I was actually on my way to that (but stumbled on the `enable_if` trick); I reformatted your code with `std::conditional` and `std::integral_constant` and provided a live-demo on liveworkspace. It works really great now! *Regarding the edit: I don't know either, but I am sure glad you did ;)* – Matthieu M. Jan 08 '13 at 14:36