C++ Logo

std-proposals

Advanced search

Re: Proposal of adding 'explicit' in template declarations

From: Михаил Найденов <mihailnajdenov_at_[hidden]>
Date: Fri, 25 Jun 2021 19:51:05 +0300
I must admit, not knowing which template argument to specify (and why!) can
be annoying and frustrating.

But there is another issue - specifying an argument that is not supposed to
be specified!
This is a much bigger problem, because it can change the behaviour of the
program. It is essentially a cast.

I was thinking, if we can get some magic trait/intrinsic/reflection which
tells the function, if it is used with explicit arguments or not.

template<class T>
const auto& annoying_max(const T& a, const T& b)
{
  static_assert(__is_deduced<T>, "You should not explicitly set the
template param. Use cast on the arguments instead.")
  return a > b;
}

or as a concept

template<class T>
concept explict_targ = ! __is_deduced<T>;

template<explict_targ T>
T&& forward(T&& t)


On Fri, Jun 25, 2021 at 8:16 AM Desmond Gold via Std-Proposals <
std-proposals_at_[hidden]> wrote:

> *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?
>
>
>
>
>
>
>
> --
> Std-Proposals mailing list
> Std-Proposals_at_[hidden]
> https://lists.isocpp.org/mailman/listinfo.cgi/std-proposals
>

Received on 2021-06-25 11:51:24