4

So, my motivation here is to determine whether the same named type declaration within several classes are the same type. In this example, I'm looking to see that all of Foo, Bar, and Baz have an internal type Q.

#include <type_traits>

template <typename N,typename ...Ns>
using equal_type_t = typename std::enable_if_t<(std::is_same_v<N, Ns> && ...), N>;

template <typename N>
using ExtractQ_t = typename N::Q;

template <typename ...Ns>
using EqualQ_t = equal_type_t<ExtractQ_t<Ns>...>;


int main()
{
    struct Qness{};
    struct Foo{using Q = Qness;};
    struct Bar{using Q = Qness;};
    struct Baz{using Q = Qness;};
    using F = EqualQ_t<Foo,Bar,Baz>;
    static_assert(std::is_same_v<F,Qness>);

    return 0;
}

Tested in clang9 (praise be to godbolt).

The error reported is:

#1 with x86-64 clang 9.0.0
<source>:10:31: error: pack expansion used as argument for non-pack parameter of alias template

using EqualQ_t = equal_type_t<ExtractQ_t<Ns>...>;

I could probably solve this by way of doing some template recursion, but I'm trying to learn to use parameter pack expansion wherever possible.

Is this possible? Is this not an allowed context? If I separate out a few individual N types, it works fine:

template <typename N1,typename N2, typename N3, typename ...Ns>
using EqualQ_t = equal_type_t<ExtractQ_t<N1>,ExtractQ_t<N2>,ExtractQ_t<N3>>;

I have to be having a pre-coffee brain-fog and can't see where I might be hosing the syntax.

Is there an expansion variant of this that will work?

CodeWeaver
  • 41
  • 3

2 Answers2

1

The error diagnostic tries to say that the first parameter of equal_type_t cannot be a pack, yet you are expanding a pack into it. Thus, the simple fix is to do the same thing you did earlier:

template <typename N, typename ...Ns>
using EqualQ_t = equal_type_t<ExtractQ_t<N>, ExtractQ_t<Ns>...>;

https://godbolt.org/z/j6_HGU

The unpacking into a non-pack + pack would require template argument deduction, but that doesn't happen for alias templates, see cppreference. You would need a struct template specialization (or template function call) to get deduction.


Using SFINAE seems a little weird in this case though. If the condition is not fulfilled, you get some compiler gibberish about SFINAE thrown in your face. There are other ways to cause a hard error during compilation.

I would say the following is the idiomatic way to write the same code, which gives you a good error when there is a problem and would (not exactly coincidentally) avoid your original problem:

template <typename ...Ns>
struct equal_type;

template <typename N,typename ...Ns>
struct equal_type<N, Ns...>
{
    static_assert((std::is_same_v<N, Ns> && ...), "These types must be the same!");
    using type = N;
};

template <typename ...Ns>
using equal_type_t = typename equal_type<Ns...>::type;

template <typename N>
using ExtractQ_t = typename N::Q;

template <typename ...Ns>
using EqualQ_t = equal_type_t<ExtractQ_t<Ns>...>;

https://godbolt.org/z/u52mUE


For completeness, the pre-C++17 way (before fold expressions existed) does indeed use recursion:

template <typename N1, typename N2, typename ...Ns>
struct equal_type
{
    static_assert(std::is_same_v<N1, N2>, "These types must be the same!");
    using type = typename equal_type<N1, Ns...>::type;
};

template <typename N1, typename N2>
struct equal_type<N1, N2>
{
    static_assert(std::is_same_v<N1, N2>, "These types must be the same!");
    using type = N1;
};

https://godbolt.org/z/NKmMZD

Max Langhof
  • 23,383
  • 5
  • 39
  • 72
  • 1
    TIL. What a weird corner case. – Quentin Oct 09 '19 at 16:18
  • @Quentin Yeah, I didn't know this either (although it makes sense). Thankfully the compiler provided the right hint! – Max Langhof Oct 09 '19 at 16:19
  • Okay, that was some impressively fast analysis. You have my thanks. Studying this now. – CodeWeaver Oct 09 '19 at 16:37
  • Okay, definitely learning loads from this one question. I didn't realize you could specialize a template (equal_type) based solely on the 'peeling off' of a parameter like that. – CodeWeaver Oct 09 '19 at 16:44
  • @CodeWeaver Now you have me curious how you would do the template recursion you suggested without that... I suspect you've actually done this specialization before without realizing. ;) – Max Langhof Oct 09 '19 at 16:51
  • I think I figured out where my disconnect was, though I doubt I would have ended up with as elegant a solution as Max's. I think I got it into my head that it was the ExtractQ_t alias that wasn't properly having substituted each of the types in Ns (in EqualQ_t). So I focussed on a failure there instead of one layer up, where my processing of a parameter pack into another parameter pack wasn't being handled by equal_type_t. I also needed the hint about aliases not participating in template argument deduction, and that the kind of matching I was doing needed it. Once again, thanks. – CodeWeaver Oct 09 '19 at 16:59
  • As to template recursion... actually I was merely assuming I could. I have no idea if I can, or what it would look like, but now you've got me curious too. :) – CodeWeaver Oct 09 '19 at 17:00
  • @CodeWeaver I was stuck there at first too, but then I noticed that the compiler error specifically complains about the parameters given to `equal_type_t`, which brought me on the right track. Regarding recursion, this would be the pre-C++17 (i.e. pre fold expression) way to do it: https://godbolt.org/z/NKmMZD (added it to the answer since it's already here). – Max Langhof Oct 09 '19 at 17:04
  • Illustrating at this point fairly firmly, the TIMTOWTDI principle. :) In my case it wasn't the fold that got me. For anyone else following this thread, I posted an attempt at what I would have resorted to in a separate 'answer' (probably below). – CodeWeaver Oct 10 '19 at 04:47
0

Figured this was best suited to a self-answer, but this is mostly directed at Max, who asked a return question, and this response 'is too long to fit in the margins'.

If I'd never tried parameter packs, I probably wouldn't have gotten to this variation on template recursion to solve my problem, but it's probably where I might have gone to if I hadn't been educated on the real issue.

#include <type_traits>

template <typename N,typename ...Ns>
using equal_type_t = typename std::enable_if_t<(std::is_same_v<N, Ns> && ...), N>;

template <typename N>
using ExtractQ_t = typename N::Q;

template <typename N,typename ...Ns>
class EqualQ
{
public:
    using type = equal_type_t<ExtractQ_t<N>,typename EqualQ<Ns...>::type>;
};

template <typename N>
class EqualQ<N>
{
public:
    using type = ExtractQ_t<N>;
};

template <typename ...Ns>
using EqualQ_t = typename EqualQ<Ns...>::type;

int main()
{
    struct Qness{};
    struct Foo{using Q = Qness;};
    struct Bar{using Q = Qness;};
    struct Baz{using Q = Qness;};
    using F = EqualQ_t<Foo,Bar,Baz>;
    static_assert(std::is_same_v<F,Qness>);

    return 0;
}

Yes, I realize it's not idiomatic, definitely less clean than either of Max's solutions, and doesn't answer my original question, but explores what I was trying to avoid.

One of the things I did discover doing it in this manner though was that aliases can't be specialized like class templates can. So point of education there too. I had to turn EqualQ into a templated struct.

The thing is, doing it this way wouldn't have educated me on why I couldn't unpack my parameter packs the way I originally wanted to, and certainly not to Max's idiomatic pattern, which I shall now be adopting. :)

CodeWeaver
  • 41
  • 3