C++ Logo

std-proposals

Advanced search

Proposal of adding 'explicit' in template declarations

From: Desmond Gold <desmondbongcawel922_at_[hidden]>
Date: Fri, 25 Jun 2021 13:13:50 +0800
*Introduction *

Usually, the template arguments are implicitly deduced by the compiler
when not specifying the template arguments in such cases.

There are times that you must specify the type template argument in
order to resolve the ambiguity if and only if

the return type is the template parameter (T).


Let us consider this little example (maybe ugly but only for the sake
of example):

 template <typename T>
 T initial_value() {
   return T(0); }

What you usually do is to specify the template argument otherwise you would
end up getting compilation error.

 initial_value<double>(); // ok
 initial_value(); // error

Another one:

 template <typename T> void force_print(T x) {
   std::cout << x; }

The template argument under the force_print function will be provided by
the compiler once the function argument is specified:

 force_print("hello"); // hello
 force_print<float>(6.28); // 6.28 of type `double will be implicitly
converted into `float`.

There is no need to explicitly specify it when you don't need to
because the compiler will do it for you.


In order to force the user to explicitly specify the template
argument, we shall use std::type_identity_t:

 template <typename T> void force_print(std::type_identity_t<T> x) {
   std::cout << x; }

Or using explicit:

 template explicit <typename T> void force_print(T x) {
   std::cout << x; }

And then:

 force_print("hello"); // error
 force_print<const char[]>("hello"); // ok

*Sample Implementation on std::forward **(Taken from libstdc++)*

 template <typename T>
 [[nodiscard]] constexpr T&& forward(std::remove_reference_t<T>& t) noexcept {
   return static_cast<T&&>(t);
 }

 template <typename T>
 [[nodiscard]] constexpr T&& forward(std::remove_reference_t<T>&& t) noexcept {
   static_assert(!std::is_lvalue_reference_v,
  "template argument substituting T must not be an lvalue reference type");

   return static_cast<T&&>(t);
 }

Application:

 template <typename T>
 constexpr T add_10(T&& x) {
   return std::forward<T>(x) + 10;
 }

It seems normal, but when you didn't provide template argument like
std::forward(x) only, you will be given a compilation error.

Error messages:

 error: no matching function for call to 'forward(int&)' note:
candidate: 'template<class T> constexpr T&&
std::forward(std::remove_reference_t<T>:&)' note: couldn't deduce
template parameter 'T'


In my proposal, I shall use explicit for implementation:

 template explicit <typename T>
 [[nodiscard]] constexpr T&& forward(std::remove_reference_t<T>& t) noexcept {
   return static_cast<T&&>(t);
 }

 template explicit <typename T>
 [[nodiscard]] constexpr T&& forward(std::remove_reference_t<T>&& t) noexcept {
   static_assert(!std::is_lvalue_reference_v,
   "template argument substituting T must not be an lvalue reference type");

   return static_cast<T&&>(t);
 }

In this current implementation, the function forward would force the user
to specify the template argument.

 std::forward<T>(x); // ok
 std::forward(x); // error

Sample error message:

 error: in function 'std::forward', the template parameter T shall be
explicitly specified. note: required from here: 'template <explicit
class T> constexpr T&& std::forward(std::remove_reference_t<T>:&)'


*Motivation*

There are some cases that we should specify the template arguments in
order to resolve some ambiguity or the user's intent.

We can use explicit so that we know what we are doing and expresses
understandably that we should explicitly specify.


Common templates that would abide the rules of using explicit template
argument are:


   - *std::forward*
   - *std::declval*
   - *std::make_unique*
   - *std::bitset*
   - *std::bit_cast*
   - *std::ranges::to (when shipped)*
   - *... (etc)*

For type aliases, concepts, and variable templates, the explicit
keyword is optional since they are already needed to be specified
explicitly.

For function templates, and class templates, the explicit keyword is
targeted when intended to use.

When using explicit with class templates, it should disable the
addition of CTAD and the deduction guides, otherwise the compiler will
throw an error.


std::type_identity_t has already this kind of usage, but with
explicit, it should really express our intent.


There are some limitations when using explicit with the template:


   - In class template, it disables CTAD and the addition of a
user-defined deduction guide.
   - It prohibits the usage of default template arguments.
   - The explicit can be used selectively inside template parameter
list, but only in left-most orientation (somewhat similar to
non-default parameter).

*Specification **(Adapted from cppreference)*

Every template <https://en.cppreference.com/w/cpp/language/templates>
is parameterized by one or more template parameters, indicated in the
parameter-list of the template declaration syntax:

*template explicit*(optional) *<* parameter-list *>* declaration


Each parameter in parameter-list may be:

   - a non-type template parameter;
   - a type template parameter;
   - a template template parameter.

A template declaration that is declared explicit shall apply to every
template parameter that should be explicitly specified.

For instance:

*template explicit* *<typename T, typename U**>* *struct* A;
>
is equivalent to:

*template* *<**explicit* *typename T,*
> *explicit* *typename U**>* *struct* A;
>
*Non-type template parameter*


   1. *explicit*(optional) type name (optional)
   2. *explicit*(optional) type *...* name(optional)
   3. *explicit*(optional) placeholder name

Type template parameter

   1. *explicit*(optional) type-parameter-key name(optional)
   2. *explicit*(optional) type-parameter-key *...* name(optional)
   3. *explicit*(optional) type-constraint name(optional)
   4. *explicit*(optional) type-constraint *...* name(optional)

Template template parameter

   1. *template* *explicit*(optional) *<* parameter-list *>*
*explicit*(optional)* typename|class* name(optional)
   2. *template* *explicit*(optional) *<* parameter-list *>*
*explicit*(optional)* typename|class...* name(optional)

Lambda expressions
*[* captures *]*
*explicit*(optional) <tparams>(optional) *(* params *)*
lambda-specifiers *{* body *}*
Explicit template parameters

   - For every template parameter that is declared as explicit, they
shall be provided by the user during template invocation.

 template <typename T, typename U> struct A {
     T data_1;
     U data_2; };
 template explicit <typename T, typename U> // same as template
<explicit typename T, explicit typename U> struct B {
     T data_1;
     U data_2; };
 template <explicit typename T, typename U = T> struct C {
     T data_1;
     U data_2; };

 A a1{15, 'A'}; // ok
 A<double, bool> a2{3.14, true>; // ok
 A<int, const std::ostream&> a3{4, std::cout}; // ok

 B b1{"hello", 6.0}; // error
 B<int, std::string_view> b2{15, "earth"}; // ok
 B<float> b3{13.0, 34.0}; // error

 C c1{nullptr, false}; // error
 C<std::nullptr_t, bool> c2{nullptr, false}; // ok
 C<int> c3{4, 5}; // ok

template <typename... Ts>auto to_tuple(Ts&&... args) {
    return std::forward_as_tuple(args...);}
auto tup1 = to_tuple(1, 'A', "hello"); // okauto tup2 =
to_tuple<double, size_t, char>(1.429, 4, 'T'); // ok
template <explicit typename... Ts> // same as template explicit
<typename... Ts>auto expl_to_tuple(Ts&&... args) {
    return std::forward_as_tuple(args...);}
auto tup3 = expl_to_tuple(1, 'A', "hello"); // errorauto tup4 =
expl_to_tuple<double, size_t, char>(1.429, 4, 'T'); // ok

For lambda expressions, the accepted call style with template is ugly:

 auto compare = [] explicit <typename T>(T a, T b) {
          return a < b; };

 compare(2, 50); // error
 compare<int>(2, 50); // error
 compare.template operator()<int>(2, 50); // ok


   - Templates declared with explicit prohibits the usage of default
   template arguments unless not every template parameter are declared as
   explicit.

 template explicit <typename T, typename U> struct A0; // ok template
explicit <typename T, typename U = int> struct A1; // error template
<explicit typename T = int> struct A2; // error template <explicit
typename T, typename U = T> struct A3; // ok template explicit
<std::integral T> struct A4; // ok template <explicit typename T,
typename... Ts> struct A5; // error

 template explicit <auto X> struct A6; // ok template <explicit
std::floating_point auto F = 1.0> struct A7; // error template
<explicit std::floating_point auto F> struct A8; // ok template
<template <typename...> explicit typename> struct A9; // ok


   - Doubling the explicit within the template declaration is not allowed.

 template explicit <explicit T> struct A0; // error template explicit
<template explicit <typename> typename T> struct A1; // ok template
explicit <template explicit <typename> explicit typename T> struct A2;
// error


   - The order of the explicit template parameters must be on the left-most
   side and skipping one is not allowed.

 template <explicit typename T, typename U> struct A0; // ok template
<typename T, explicit typename U> struct A1; // error template
<explicit typename T1, auto N2, explicit char N3> struct A2; // error


   - Class template declaration with at least 1 explicit-declared template
   parameter may not be allowed to add a deduction guides.
   - Template Specialization - TBA...
   - Variable templates, templated alias, and concepts declared with
   explicit within template declaration may be ill-formed, no diagnostic
   required.

 template explicit <typename T>
 constexpr auto var1 = T(5); // ok
 template explicit <int A, int B>
 constexpr auto add = A + B; // ok
 template <explicit int A, int B>
 constexpr auto subtract = A - B; // ill-formed, no diagnostic required
 template <explicit int A, explicit int B>
 constexpr auto multiply = A * B; // ok
 template <int A, explicit int B>
 constexpr auto divide = A / B; // error
 template explicit <size_t N> using int_array = std::array<int, N>; // ok
 template <explicit typename R, typename... Args> using func =
R(*)(Args...); // ill-formed, no diagnostic required
 template explicit <typename T>
 concept arithmetic = std::integral<T> || std::floating_point<T>; // ok
 template <explicit typename T, size_t N>
 concept less_N_bytes = sizeof(T) < N; // ill-formed, no diagnostic required

Any suggestions? or opinions?

Received on 2021-06-25 00:14:05