Using unconstrained template template parameters with constrained templates

Published Proposal,

ISO/IEC JTC1/SC22/WG21 14882: Programming Language — C++


Unconstrained template template parameters cannot accept constrained templates. This blocks important use cases and causes legacy templates to behave in unexpected ways as pointed out in Concepts TS issue 14. We propose addressing this.

1. The problem

It is our understanding that any template accepting an unconstrained template template parameter cannot accept a constrained template as pointed out in Concepts TS Issue 14. This creates problems with both legacy and new code.

To illustrate the problem, define the following classes

template<class T, class U> 
struct Unconstrained { Unconstrained(U, V);};

template<class T, class U, class = enable_if_t<is_integral_v<U>>> 
struct Sfinae { Sfinae(U, V); };

template<class T, class U> requires Integral<U> 
struct Conceptized { Conceptized(U, V); };

In Concepts TS issue 14, the following was a blocker to adding concepts to sqlpp11.

// A template that takes a tuple and copies its arguments into another template (sink)
template<typename Tuple, template<typename...> class Sink>
using copy_tuple_args = ...

copy_tuple_args<tuple<char *, int>, Unconstrained> u1; // OK. Unconstrained<char *, int>
copy_tuple_args<tuple<char *, int>, Sfinae> s1;        // OK. Sfinae<char *, int>
copy_tuple_args<tuple<char *, int>, Conceptized> c1;   // Ill-formed instead of desired Conceptized<char *, int> :(

As C++17 template template parameters are incompatible with concept-constrained templates, any attempt to concept constrain templates in existing code bases that make use of template template parameters will be likely to cause difficult or even impossible to fix breakage, discouraging the adoption of Concepts:

// Valid C++17 template
template<template<class, class> class TT, class A>
struct curry {    
    template<class B>
    using type = TT<A, B>; 

curry<Unconstrained, char *>::type<int> u3;  // OK. Unconstrained<char *, int>
curry<Sfinae, char *>::type<int>  s3         // OK. Sfinae<char *, int>
curry<Conceptized, char *>::type<int> c3;    // Ill-formed instead of desired Conceptized<char *, int>
There is not even a way to write new code with template template parameters without introducing restrictions on constraints, hindering use cases like Inferencing heap objects (P1069):
// Notation of P1069
auto u4 = make_unique<Unconstrained>(3, 5); // OK. unique_ptr<Unconstrained<int, int>>
auto s4 = make_unique<Sfinae>(3, 5);        // OK. unique_ptr<Unconstrained<int, int>>
auto c4 = make_unique<Conceptized>(3, 5);   // Ill-formed instead of desired Conceptized<int, int>

Likewise, ranges::to (P1206) awkwardly relies on standard library containers being unconstrained:

// copy a list to a vector of the same type, deducing value_type
Same<std::vector<int>> auto c = ranges::to<std::vector>(l); // OK, vector is not concept-constrained

2. The solution

We propose simply that an unconstrained template template parameter match concept-constrained classes. E.g.,
copy_tuple_args<tuple<char *, int>, Conceptized> c1;   // Proposed: Conceptized<char *, int>
curry<Conceptized, char *>::type<int> c3;              // Proposed: Conceptized<char *, int>
auto c4 = make_unique<Conceptized>(3, 5);              // Proposed: Conceptized<int, int>
Note that violations of the constraints will still produce concept-aware error messages
// Proposed: Ill-formed with concept violation as desired
copy_tuple_args<tuple<int, char *>, Conceptized> c5; 

3. Without a language change, can we accept a general template as a template template parameter?

As far as we can tell, you can’t. So use cases like the above are simply impossible. (Did we miss something? We’d love to be proven wrong).

4. Why not create a new notation that matches any template?

While one could imagine some new notation for template template parameters that works with and without constraints, we believe that would still be an unsatisfactory solution for the following reasons

5. What if we want to only match unconstrained templates?

We are unaware of use cases for the current behavior of only matching unconstrained concepts (especially compared to the significant use cases for this proposal). However, note that natural notations could easily be developed and added, e.g., by making the lack of constraints explicit:
template<template<class, class> requires {} class X> // Not proposed at this time
class OnlyMatchesUnconstrained {};

OnlyMatchesUnconstrained<Unconstrained> u5; // OK
OnlyMatchesUnconstrained<Sfinae> s5;        // OK
OnlyMatchesUnconstrained<Conceptized> s5;   // Ill-formed as desired