atomgalaxy / isocpp-universal-template-param

We propose a way to spell a universal template parameter kind. This would allow for a generic apply and other higher-order template metafunctions, and certain typetraits.
https://atomgalaxy.github.io/isocpp-universal-template-param/d1985r0.pdf
2 stars 2 forks source link

Add an example of a concept templated on a UTP #10

Open BengtGustafsson opened 1 year ago

BengtGustafsson commented 1 year ago

How do we spell a constrained UTP? If we have a concept templated on an UTP, how do we use it in a template declaration?

template<template auto X> concept TypeOrValue = is_type_v<X> || is_value_v<X>;

template<TypeOrValue TV> size_t mySizeof();

or maybe:

template<TypeOrValue template auto TV> size_t mySizeof();

NO: Actually, we need both, otherwise the compiler can't know if the argument or its type is to be subjected to the concept, in case it should happen to be a value. This gets messy, I guess, but is a consequence of the concept being usable both for the value itself and for its type.

The only reasonable solution to this I can think of is:

template<TypeOrValue TV>     // Check if the argument's type models the concept, in case the argument is a value.
template<TypeOrValue auto TV>  // Check if the value itself models the concept, in case the argument is a value.

Another more unorthodox approach would be:

template<TypeOrValue TV>                 // Check the value if the argument is a value
template<TypeOrValue decltype TV>  // Check the type of the argument if it is a value

I actually think that the latter form is easier to understand.

An alternative would be to define the concept using a concept, I don't know if this is allowed:

template<TypeOrTemplate TT> concept MyConcept ...; Now this would have to come with the rule that if the concept itself does not accept values it is the type of the value that is checked if the argument should happen to be a value. The opposite trick would also work if the concept doesn't take types.

In this scenario a concept which takes both types and values would check if the value rather than its type models the concept.

BengtGustafsson commented 1 year ago

On telecon #2 the idea to have a concept that only allows UTPs (or maybe dependent names) as the argument:

template<anykind U> concept must_be_utp = ?

template<must_be_utp U> struct X;

My take on this was that this is not possible as concepts are checked during instantiation and then the argument we try to match to U is already of a known kind. I still can't see how this could be made to work as we wanted.

camaclean commented 1 year ago

I think it might need compiler magic

template<anykind U> concept  must_be_utp = __is_utp<anykind U /*preserve UTP*/>;
camaclean commented 1 year ago

I've also been mulling things over and think we could propose that all dependent names passed to templates are UTPs. This slightly extends "down with typename!"

template<typename auto T>
struct myclass {};

myclass<myclass2::name> a; // Passing dependent name, treat as UTP
myclass<myclass2::name*2> b; // Contains a dependent name but the result isn't itself a dependent name, myclass2::name parsed as value
myclass<myclass2::name*> c; // Syntax error. Expression itself isn't a dependent name, so myclass2::name is parsed as a value and this doesn't make sense
myclass<typename myclass2::name*> d; // A type is passed
myclass<static_cast<decltype(auto)>(myclass2::name)> e; // Force parsing as value

template<typename T>
struct myclass3 {};

myclass3<myclass2::name> f; // UTP narrows to type when applied to this template.

This effectively means that dependent names are preserved until used in an expression or narrowed template.

This would reduce the need to check if something is a UTP due to removing the need to ambiguate to UTP.

BengtGustafsson commented 1 year ago

This is close to what I wrote in the follow up mail pdf Thursday, but I think you mean that declaration a is only correct as long as the template parameter of myclassis an UTP, while in my examples it doesn't matter what it is. To implement any of these ideas means that the compiler must be able to "break open" the constant-expression created at parse time to check if it consists of a dependent name or UTP. For the restrictive semantics here the compiler uses this ability during substitution of UTPs and if a dependent name is found it is used without being auto-ambiguated to value. In the more relaxed interpretation of my pdf the compiler uses this ability directly at parse time and if the template argument was a dependent name or UTP it refrains from checking its kind towards the kind of the template parameter. (This check is always done by the three major compilers but I find it unlikely to be mandated by the standard, it is just a convenience to find templates that can never be successfully instantiated).

With the more restricted system I see nasty edge cases. One is if the template parameter is an UTP restricted by a concept. It would be easy to say "this still applies" but the concept could actually restrict the kind to be typename only which would open up a world where people declare their template parameters template instead of template just to allow dependent names in template arguments without having to disambiguate them. You could also say that the rule only applies when there is no concept restrictions but this would require a ambiguation syntax to be applied every time an UTP is forwarded to a constrained UTP template parameter, which would be really scary if the constraint is added after the use was written.

There is also the case of overloaded function templates allowing two of three kinds. What would happen if a dependent name is used as the argument to such a pair:

template<typename X> void f();
template<auto X> void f();

template<anykind T> void g()
{
    return f<T>();   // Must defer checking or first function can never be called!
}

Here we must defer checking but f does not have a UTP template parameter. Some more advanced compiler logic is needed to figure out that two different kinds for f's template parameter are allowed. If compilers can't be trusted to do this we must introduce a ambiguation syntax again or live with the fact that the top f can never be called from g and if T is a type instantiation fails even though it looks as it should succeed.

So I think the only reasonable rule is that if the template argument is a dependent name no checking takes place, as I wrote in the pdf.

This can be viewed as another type of co-/contravariance issue. Example:

template<anykind T> struct S {
    std::vector<T> aa;
};

With covariant behavior this isn't an error as T could be a type, while it is an error for contravariant behavior as not everything that T can resolve to later is usable as a template argument to vector.

Note that we currently have asymmetric covariant behavior for dependent names: If a dependent name is not disambiguated it is treated as a value but at instantiation it could turn out to not be, breaking the contravariant promise. That you have to disambiguate a dependent name to treat it as a type or template is an issue of parsing problems, not a desire for contravariancy.

I don't think we should be afraid of introducing another covariant behavior if it makes source code easier to write. We already pretty much decided we want covariant behavior of template template parameters. Gasper clams that auto template parameters behave contravariantly, could you provide an example, I can't get any compiler to complain when I break what I would call contravariance in this context.

BengtGustafsson commented 1 year ago

As we only have constraints on types in template parameter lists all my previous comments here are invalid. I thought that we also had constraints on NTTP values, but maybe that was in the TS and subsequently removed. Fortunately this did away with any ambiguity that I saw (or thought I saw). On the other hand, doesn't it do away with the need for concepts templated on UTPs all together?

According to cppreference you can still create concepts contraining values but I don't know what you can do with them that you can't do with a bool variable template. Could not find any writing in this direction on cppreference, did not check the standard itself.