C++ template parameter pack expansion of alias

Viewed 397

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?

2 Answers

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

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. :)

Related