Questions about templates and different behavior of 4 different front-ends

Hi,

While experimenting with some heavily templated code I have faced with some problems. First of all, I do not understand whether my code should be compiled at all (because of processing of trailing parameters packs) and I have found nothing about it in C++17 standard draft. I tried 4 different compilers (with different front-ends) on godbolt: gcc, clang, icc, msvc.

  1. First question is about variadic templates and how they are expanded. Consider the following code:

template struct S {};

template<typename T, typename…Rest>
using U = S<T, Rest…>; // is it valid?

using SI = U; // seems, that there should be S and there is nothing ill-formed

The problem here is that only gcc (starting from version 7.1) is able to compile this code. Other compilers report that using directive is ill-formed because it provides too many template parameters for S. Who is right in this case?

  1. Second example is quite more complicated. The code is:

template<typename A, typename B> struct S {};

template<template<typename…> typename F>
struct Flip {
template<typename A, typename B, typename…Rest>
using Type = F<B, A, Rest…>;
};

template<template<typename…> typename F, typename…Args>
struct PartialApply {
template<typename…Rest>
using Type = F<Args…, Rest…>;
};

using X = typename PartialApply<Flip::Type, int>::template Type; // there should be S<bool, int>

Now this can be compiled only by icc. Other compilers become mad and start to report very strange error. For example, gcc says that there is “pack expansion of ‘B’” in Flip. clang and msvc produce similar error. Is this code valid C++ after all?

Also if write similar code but in context of lambdas all compilers will work as expected. Does processing of variadic lambdas differ from processing of variadic templates?

I assume that some compilers are wrong and probably bugs should be filed but I’m not definitely sure. It will be very good if someone can explain whether these examples are valid or not and if not then why.

Hi,

While experimenting with some heavily templated code I have faced with some problems. First of all, I do not understand whether my code should be compiled at all (because of processing of trailing parameters packs) and I have found nothing about it in C++17 standard draft. I tried 4 different compilers (with different front-ends) on godbolt: gcc, clang, icc, msvc.

This seems like a question that would be more suited to posting on the cpplang Slack or StackOverflow, rather than cfe-dev specifically (since it has nothing directly to do with the Clang front-end but is a question about C++ in general).
However…

  1. First question is about variadic templates and how they are expanded. Consider the following code:

template struct S {};

template<typename T, typename…Rest>
using U = S<T, Rest…>; // is it valid?

using SI = U; // seems, that there should be S and there is nothing ill-formed

The problem here is that only gcc (starting from version 7.1) is able to compile this code. Other compilers report that using directive is ill-formed because it provides too many template parameters for S. Who is right in this case?

My understanding is that all four compilers’ behavior is correct. Template U is well-formed only when Rest… is an empty pack, and thus the program is ill-formed (no diagnostic required) via http://eel.is/c++draft/temp.res#8.3. So technically any behavior at all is conforming to the standard. (This doesn’t mean the compiler’s behavior couldn’t be made more user-friendly! An enhancement request might still be warranted.)

  1. Second example is quite more complicated. The code is:

template<typename A, typename B> struct S {};

template<template<typename…> typename F>
struct Flip {
template<typename A, typename B, typename…Rest>
using Type = F<B, A, Rest…>;
};

template<template<typename…> typename F, typename…Args>
struct PartialApply {
template<typename…Rest>
using Type = F<Args…, Rest…>;
};

using X = typename PartialApply<Flip::Type, int>::template Type; // there should be S<bool, int>

Now this can be compiled only by icc. Other compilers become mad and start to report very strange error. For example, gcc says that there is “pack expansion of ‘B’” in Flip. clang and msvc produce similar error. Is this code valid C++ after all?

Well, same as above: the template Flip<S>::Type is invalid and thus causes the entire program to be ill-formed, no diagnostic required.

Also if write similar code but in context of lambdas all compilers will work as expected. Does processing of variadic lambdas differ from processing of variadic templates?

You’d have to show an example.

For more on the wacky Wild West situation around interchangeability of pack and non-pack template parameters, see the later sections of
https://quuxplusone.github.io/blog/2019/01/20/covariance-and-contravariance/

–Arthur

Hi,

While experimenting with some heavily templated code I have faced with some problems. First of all, I do not understand whether my code should be compiled at all (because of processing of trailing parameters packs) and I have found nothing about it in C++17 standard draft. I tried 4 different compilers (with different front-ends) on godbolt: gcc, clang, icc, msvc.

This seems like a question that would be more suited to posting on the cpplang Slack or StackOverflow, rather than cfe-dev specifically (since it has nothing directly to do with the Clang front-end but is a question about C++ in general).
However…

  1. First question is about variadic templates and how they are expanded. Consider the following code:

template struct S {};

template<typename T, typename…Rest>
using U = S<T, Rest…>; // is it valid?

using SI = U; // seems, that there should be S and there is nothing ill-formed

The problem here is that only gcc (starting from version 7.1) is able to compile this code. Other compilers report that using directive is ill-formed because it provides too many template parameters for S. Who is right in this case?

My understanding is that all four compilers’ behavior is correct. Template U is well-formed only when Rest… is an empty pack, and thus the program is ill-formed (no diagnostic required) via http://eel.is/c++draft/temp.res#8.3. So technically any behavior at all is conforming to the standard. (This doesn’t mean the compiler’s behavior couldn’t be made more user-friendly! An enhancement request might still be warranted.)

  1. Second example is quite more complicated. The code is:

template<typename A, typename B> struct S {};

template<template<typename…> typename F>
struct Flip {
template<typename A, typename B, typename…Rest>
using Type = F<B, A, Rest…>;
};

template<template<typename…> typename F, typename…Args>
struct PartialApply {
template<typename…Rest>
using Type = F<Args…, Rest…>;
};

using X = typename PartialApply<Flip::Type, int>::template Type; // there should be S<bool, int>

Now this can be compiled only by icc. Other compilers become mad and start to report very strange error. For example, gcc says that there is “pack expansion of ‘B’” in Flip. clang and msvc produce similar error. Is this code valid C++ after all?

Well, same as above: the template Flip<S>::Type is invalid and thus causes the entire program to be ill-formed, no diagnostic required.

Okay, it really seems that Flip is ill-formed. But changing Flip in the second example to:

template<template<typename, typename> typename F>
struct Flip {
template<typename A, typename B>
using Type = F<B, A>;
};

actually doesn’t fix compilers. They’re still saying that the program is incorrect because of this mystical pack expansion of ‘B’. I assume that now it is a bug.

Also if write similar code but in context of lambdas all compilers will work as expected. Does processing of variadic lambdas differ from processing of variadic templates?

You’d have to show an example.

Code with lambda functions:

auto fvar = [](auto f) {
return [f](auto a, auto…rest) { return f(a, rest…); };
};

auto sqr = [](auto a) { return a * a; };

auto foo() {
return fvar(sqr)(2); // works fine
}

This case looks similar to templates and now it seems that it is ill-formed too. Translating second case to lambdas and adding flip2 to avoid probable ill-formedness also make compilers work correct:

auto shift = [](auto a, auto b) { return a << b; };

auto flip2 = [](auto f) {
return [f](auto a, auto b) { return f(b, a); };
};

auto partialApply = [](auto f, auto…args) {
return f, args… { return f(args…, rest…); };
};

auto bar() {
return partialApply(flip2(shift), 3)(1); // works fine
}

That is this difference in processing of code I asked about.

06.04.2019, 19:36, “Arthur O’Dwyer” <arthur.j.odwyer@gmail.com>:

  1. First question is about variadic templates and how they are expanded. Consider the following code:

template struct S {};

template<typename T, typename…Rest>
using U = S<T, Rest…>; // is it valid?

using SI = U; // seems, that there should be S and there is nothing ill-formed

The problem here is that only gcc (starting from version 7.1) is able to compile this code. Other compilers report that using directive is ill-formed because it provides too many template parameters for S. Who is right in this case?

My understanding is that all four compilers’ behavior is correct. Template U is well-formed only when Rest… is an empty pack, and thus the program is ill-formed (no diagnostic required) via http://eel.is/c++draft/temp.res#8.3. So technically any behavior at all is conforming to the standard. (This doesn’t mean the compiler’s behavior couldn’t be made more user-friendly! An enhancement request might still be warranted.)

  1. Second example is quite more complicated. The code is:

template<typename A, typename B> struct S {};

template<template<typename…> typename F>
struct Flip {
template<typename A, typename B, typename…Rest>
using Type = F<B, A, Rest…>;
};

template<template<typename…> typename F, typename…Args>
struct PartialApply {
template<typename…Rest>
using Type = F<Args…, Rest…>;
};

using X = typename PartialApply<Flip::Type, int>::template Type; // there should be S<bool, int>

Now this can be compiled only by icc. Other compilers become mad and start to report very strange error. For example, gcc says that there is “pack expansion of ‘B’” in Flip. clang and msvc produce similar error. Is this code valid C++ after all?

Well, same as above: the template Flip<S>::Type is invalid and thus causes the entire program to be ill-formed, no diagnostic required.

Okay, it really seems that Flip is ill-formed. But changing Flip in the second example to:

template<template<typename, typename> typename F>
struct Flip {
template<typename A, typename B>
using Type = F<B, A>;
};

actually doesn’t fix compilers. They’re still saying that the program is incorrect because of this mystical pack expansion of ‘B’. I assume that now it is a bug.

Sadly, no. The culprit this time is that Flip<S>::Type is an alias template, and the status quo as of C++17 is that it is specifically forbidden to expand a parameter pack into the template argument list of an alias template.

Here is a Godbolt illustrating the difference in what’s supported for alias templates versus class templates: https://godbolt.org/z/YAzHMc

This is CWG open issue 1430. Implementors seem to agree that mixing parameter packs and aliases is troublesome for them. Clang’s code-comments on CWG1430 talk about “canonicalization,” which I think means “asking whether two template declarations declare the same template or different templates.”
Example: https://godbolt.org/z/C9Qiwd

Also if write similar code but in context of lambdas all compilers will work as expected. Does processing of variadic lambdas differ from processing of variadic templates?

You’d have to show an example.

auto fvar = [](auto f) {
return [f](auto a, auto…rest) { return f(a, rest…); };
};

auto sqr = [](auto a) { return a * a; };

auto foo() {
return fvar(sqr)(2); // works fine
}

Written out in C++03, we have
https://godbolt.org/z/eJ97M2
The problematic template is FRet::call. However, since that template is itself generated from a template, the compiler would have to do extra work to check it “prior to any instantiation” under http://eel.is/c++draft/temp.res#8 — and so the compiler just doesn’t do that (optional) check, and so the code appears to work.

My understanding is that the template FRet::call (i.e. decltype(fvar(sqr))::operator()) is just as ill-formed as your original template U from example number one, and so this code does have undefined behavior. However, I believe it’s perfectly safe to use this code in practice.
The major difference between this code and your original example number one is that in this one the ill-formed template is created by template argument substitution — it’s not “visible” to the compiler prior to substituting things into dependent expressions. Example: https://godbolt.org/z/LZvWhA And the major difference between this code and your example number two is that this one doesn’t involve alias templates.

–Arthur