Skip to content

[temp.func.order] Making the intent of transformations clearer #7855

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
seha-bot opened this issue Apr 24, 2025 · 5 comments
Open

[temp.func.order] Making the intent of transformations clearer #7855

seha-bot opened this issue Apr 24, 2025 · 5 comments

Comments

@seha-bot
Copy link

seha-bot commented Apr 24, 2025

The wording of [temp.func.order]/3

To produce the transformed template, for each type, constant, type template, variable template, or concept template parameter (including template parameter packs ([temp.variadic]) thereof) synthesize a unique type, value, class template, variable template, or concept, respectively, and substitute it for each occurrence of that parameter in the function type of the template.

is vague and leaves the precise nature of the transformed function type to be easily misunderstood. Consider, for example:

template <typename T>
void f(T, typename std::type_identity_t<T>);

Intuitively, one might expect the transformed function type to be

void (A, A)

where A is some unique invented type. However, the intention seems to be that the transformed function type in this case becomes

void (A, B)

where A and B are distinct invented types. i.e., the transformation performs substitution only, type-specifiers that would require template instantiation to resolve are considered to give rise to distinct types of their own.

We would suggest to add a note of this fact and, ideally, some examples to illustrate this key issue. e.g.:

#include <type_traits>

template <typename T> void f(T, T);                        // #1
template <typename T> void f(T, std::type_identity_t<T>);  // #2, less specialized than #1

template <typename T> using identity_t = T;
template <typename T> void f(T, identity_t<T>); // redeclaration of #1

Big thanks to @michael-kenzel for the explanation and the example.

@Eisenwave
Copy link
Contributor

I don't see why #2 would be less specialized than #1. If anything, wouldn't it be more specialized? Also, why would either be more or less specialized?

Intuitively, if we synthesizes some types X and Y for their Ts and have

(X, X)
(Y, std::type_identity_t<Y>)

... then it seems like with either set of parameter types, we could call the other function, and deduction would succeed in the same cases, so neither is more specialized.

Perhaps it would work in the following example:

template <typename T> void f(T);
template <typename T> void f(std::type_identity_t<T>); // more specialized

In any case, it's not obvious to me that the wording is unclear, or that the example would clarify anything.

@seha-bot
Copy link
Author

Here's why we think that #2 is less specialized than #1 in the example:

There are two pairs of original and transformed [temp.func.order]/3 types P and A here:

  1. f1: (T, T) and f2: (Y, std::type_identity_t<Y>)
  2. f2: (T, std::type_identity_t<T>) and f1: (X, X)

X and Y are some unique invented types.

In the first case, deduction fails since it would deduce conflicting arguments for T [temp.deduct.type]/2, so f2 is not at least as specialized as f1 [temp.deduct.partial]/8.
In the second case, deduction succeeds since T can be deduced from the first argument type. The second parameter type is in a non-deduced context, so it will not affect deduction in this case. Because of that, f1 is at least as specialized as f2.

Therefore, the first overload is more specialized than the second [temp.deduct.partial]/10.

In your example it is correct that the second overload is more specialized than the first, but for a different reason. If we were to do the same analysis, there would be no conflicting arguments in the first case and deduction would succeed, and in the second case deduction would fail because the template parameter is only used in a non-deduced context.

@jensmaurer
Copy link
Member

In the first case, deduction fails since it would deduce conflicting arguments for T

Why? The arguments are (Y, Y), since that's what std::type_identity_t<Y> actually is. Note that Y is (in the model) a unique invented type, and it's not dependent, so we can determine the actual (resulting) type of std::type_identity_t<Y>.

I've heard some implementations might keep those types dependent, which would explain the difference.

@michael-kenzel
Copy link

michael-kenzel commented Apr 25, 2025

Why? The arguments are (Y, Y), since that's what std::type_identity_t<Y> actually is. Note that Y is (in the model) a unique invented type, and it's not dependent, so we can determine the actual (resulting) type of std::type_identity_t<Y>.

As far as my understanding goes, the key here is [temp.func.order]/3 (emphasis mine):

To produce the transformed template, for each type, constant, type template, variable template, or concept template parameter (including template parameter packs ([temp.variadic]) thereof) synthesize a unique type, value, class template, variable template, or concept, respectively, and substitute it for each occurrence of that parameter in the function type of the template.

The intention (and what all implementations seem to be doing) appears to be that only substitution into the function type is performed during production of the transformed template, no actual type is produced. In particular, no instantiation of any constituent templates takes place. Thus, whether two "types" are "the same" in this context comes down to purely the structure of the type specifiers rather than what types they actually would resolve to. The fact that the wording talks about a "transformed template" rather than a "transformed type" may also be a hint in this direction, though [temp.deduct.partial] then refers to both as types rather than templates, except where it doesn't. I believe that "type" in this context is taken to mean the type expressed in terms of the template parameters / substituted invented types; at least that's the only way I can make some sense of the wording. From what I could gather, the reasoning behind this seems to be that partial ordering should be decidable solely based on the declarations of the templates themselves. I can only guess that this is to avoid issues to do with what it would actually mean to instantiate random templates with these invented types: What is the nature of these invented types? What are valid uses of them? What happens when an instantiation with such an invented type results in an invalid construct? How would all of this interact with explicit or partial specializations?

As evidenced by the reactions in this thread, this behavior is highly counter-intuitive and non-obvious from the wording, which is why we think there should at the very least be a note and example to point this out. Ideally, the wording would of course be changed to actually specify this behavior in clear terms.

This issue seems to also have been brought up before in #523 and CWG 2160.

@Eisenwave
Copy link
Contributor

As evidenced by the reactions in this thread, this behavior is highly counter-intuitive and non-obvious from the wording, which is why we think there should at the very least be a note and example to point this out. Ideally, the wording would of course be changed to actually specify this behavior in clear terms.

Yeah, I've checked on CE and all big three compilers implement it so that (T, std::type_identity_t<T>) is less specialized, which is surprising.

This is worth an example.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants