typename and template are redundant for constrained types

Document #: xxx
Date: 2022-12-01
Project: Programming Language C++
SG17
Reply-to: Mihail Naydenov
<>

1 Abstract

This paper argues, a constrained type has all the necessary information to not require either typename or template disambiguation when using its members.

2 Motivation

“Generic Programming is just Programming”
B. Stroustrup

Good code is reusable code and reusable code is often parametrized. Currently however, if the programmer developes a piece of code that he/she wants to make more reusable by abstracting away the types, the code itself will probably fail to compile:

Original version of some-great-code

void someGreatCode(SomeClass c)
{
   SomeClass::type t;
   // use t
   c.doGreatWork<std::string>();
   ...
}

Generic version of some-great-code (fails to compile)

template<class AnyClass>
void someGreatCode(AnyClass c)
{
   AnyClass::type t;
   // use t
   c.doGreatWork<std::string>();
   ...
}

Why this happens is well known - the compiler does not know if a member is a value, a type or a template. Where before the concrete class definition can be looked up, on a generic type this is not possible. The compiler instead have to guess based on usage and in case of ambiguity, the member must be disambiguated explicitly. This is unfortunate, but somewhat understandable, considering, some information is removed by making the type non-concrete.

However, what if the programmer has supplied this information by other means? That’s exactly the case with constrained types - what members are is part of the concept, by definition.

template<class T>
concept SomeClasses = requires(T t){
  typename T::type;                      //< `type` MUST BE a type
  t.template doGreatWork<std::string>(); //< `doGreatWork` MUST BE a function template
}

template<SomeClasses AnyClass>
void someGreatCode(AnyClass c)
{
   AnyClass::type t;             //< can `type` NOT BE a type?!?
   // use t
   c.doGreatWork<std::string>(); //< can `doGreatWork` NOT BE a function template?!?
   ...
}

In this modern scenario having to provide disambiguation starts to feel excessive. Now it looks like, the compiler is “dumb”, asking for the same thing twice, even when the second time it is effectively “forbidden” to get a different result!

3 Proposal

This paper suggest to use the concept, constraining a type to query what its members are.

template<class T>
concept SomeClasses = requires(T t){
  typename T::type;                      //< `type` MUST BE a type
  t.template doGreatWork<std::string>(); //< `doGreatWork` MUST BE a function template
}

template<SomeClasses AnyClass>
void someGreatCode(AnyClass c)
{
   AnyClass::type t;             //< proposed `type` IS A type, no disambiguation needed
   // use t
   c.doGreatWork<std::string>(); //< proposed `doGreatWork` IS A function template, no disambiguation needed
   ...
}

Using the concept this way will allow for natural code migration from concrete to generic forms. It will also serve as an additional incentive for introducing concepts in user code, even in cases, one might find them unnecessary.

Open question
Should only explicit constrains contribute to the disambiguation:

template<class T>
concept SomeClasses = requires(T::type t){  //< "implicit" type requirement, should this disambiguate?
  ...
}

This proposal does not hold a strong opinion, but leans towards only considering explicit constrains to ease the implementation.

Pathological case

For better or worse, a class can have members with the same names, but be of different kinds:

struct Class {
  struct member{ };
  static const int member = 1;
};

It is also legal to create concepts that express such requirements:

template<class T>
concept interesting = requires {
  typename T::member;
  { T::member } -> std::convertible_to<int>;
};

Similar cases can arise in a way of inheritance - a subclass can introduce the same name with a different type, much to the same effect.
This paper considers this a pathological, corner case and suggest to simply abort lookup for the name if it is found behind more then one entity. This is, the name must be unique in order for the automatic disambiguation to work. If it is not unique, current behavior is used as a fallback, possibly accompanied by a warning message. Requiring manual disambiguation in such cases is understandable as the ambiguity is obvious.

Should be noted, currently only GCC allows using manual disambiguation, enabling code like this to compile:

template<interesting T>
int func()
{
  typename T::member m;
  return T::member;
}

4 Appendix

A more niche example:

template<class T>
concept C = requires {
  typename T::X;
};
template<C T> 
void f() {
  void (*pf)(T::X); //< proposed pointer to function (currently an error)
  void g(T::X);     //< proposed function declaration (currently an error)
};