public inbox for libstdc++@gcc.gnu.org
 help / color / mirror / Atom feed
From: Jason Merrill <jason@redhat.com>
To: Marek Polacek <polacek@redhat.com>,
	GCC Patches <gcc-patches@gcc.gnu.org>,
	libstdc++ <libstdc++@gcc.gnu.org>,
	Jonathan Wakely <jwakely@redhat.com>
Subject: Re: [PATCH] c++: Implement __is_{nothrow_,}convertible [PR106784]
Date: Thu, 22 Sep 2022 18:14:44 -0400	[thread overview]
Message-ID: <b8712969-ec22-31e1-769a-6f7d900b742d@redhat.com> (raw)
In-Reply-To: <20220922133900.142238-1-polacek@redhat.com>

On 9/22/22 09:39, Marek Polacek wrote:
> To improve compile times, the C++ library could use compiler built-ins
> rather than implementing std::is_convertible (and _nothrow) as class
> templates.  This patch adds the built-ins.  We already have
> __is_constructible and __is_assignable, and the nothrow forms of those.
> 
> Microsoft (and clang, for compatibility) also provide an alias called
> __is_convertible_to.  I did not add it, but it would be trivial to do
> so.
> 
> I noticed that our __is_assignable doesn't implement the "Access checks
> are performed as if from a context unrelated to either type" requirement,
> therefore std::is_assignable / __is_assignable give two different results
> here:
> 
>    class S {
>      operator int();
>      friend void g(); // #1
>    };
> 
>    void
>    g ()
>    {
>      // #1 doesn't matter
>      static_assert(std::is_assignable<int&, S>::value, "");
>      static_assert(__is_assignable(int&, S), "");
>    }
> 
> This is not a problem if __is_assignable is not meant to be used by
> the users.

That's fine, it's not.

> This patch doesn't make libstdc++ use the new built-ins, but I had to
> rename a class otherwise its name would clash with the new built-in.

Sigh, that's going to be a hassle when comparing compiler versions on 
preprocessed code.

> Bootstrapped/regtested on x86_64-pc-linux-gnu, ok for trunk?
> 
> 	PR c++/106784
> 
> gcc/c-family/ChangeLog:
> 
> 	* c-common.cc (c_common_reswords): Add __is_convertible and
> 	__is_nothrow_convertible.
> 	* c-common.h (enum rid): Add RID_IS_CONVERTIBLE and
> 	RID_IS_NOTHROW_CONVERTIBLE.
> 
> gcc/cp/ChangeLog:
> 
> 	* constraint.cc (diagnose_trait_expr): Handle CPTK_IS_CONVERTIBLE
> 	and CPTK_IS_NOTHROW_CONVERTIBLE.
> 	* cp-objcp-common.cc (names_builtin_p): Handle RID_IS_CONVERTIBLE
> 	RID_IS_NOTHROW_CONVERTIBLE.
> 	* cp-tree.h (enum cp_trait_kind): Add CPTK_IS_CONVERTIBLE and
> 	CPTK_IS_NOTHROW_CONVERTIBLE.
> 	(is_convertible): Declare.
> 	(is_nothrow_convertible): Likewise.
> 	* cxx-pretty-print.cc (pp_cxx_trait_expression): Handle
> 	CPTK_IS_CONVERTIBLE and CPTK_IS_NOTHROW_CONVERTIBLE.
> 	* method.cc (is_convertible): New.
> 	(is_nothrow_convertible): Likewise.
> 	* parser.cc (cp_parser_primary_expression): Handle RID_IS_CONVERTIBLE
> 	and RID_IS_NOTHROW_CONVERTIBLE.
> 	(cp_parser_trait_expr): Likewise.
> 	* semantics.cc (trait_expr_value): Handle CPTK_IS_CONVERTIBLE and
> 	CPTK_IS_NOTHROW_CONVERTIBLE.
> 	(finish_trait_expr): Likewise.
> 
> libstdc++-v3/ChangeLog:
> 
> 	* include/std/type_traits: Rename __is_nothrow_convertible to
> 	__is_nothrow_convertible_lib.
> 	* testsuite/20_util/is_nothrow_convertible/value_ext.cc: Likewise.
> 
> gcc/testsuite/ChangeLog:
> 
> 	* g++.dg/ext/has-builtin-1.C: Enhance to test __is_convertible and
> 	__is_nothrow_convertible.
> 	* g++.dg/ext/is_convertible1.C: New test.
> 	* g++.dg/ext/is_convertible2.C: New test.
> 	* g++.dg/ext/is_nothrow_convertible1.C: New test.
> 	* g++.dg/ext/is_nothrow_convertible2.C: New test.
> ---
>   gcc/c-family/c-common.cc                      |   2 +
>   gcc/c-family/c-common.h                       |   1 +
>   gcc/cp/constraint.cc                          |   6 +
>   gcc/cp/cp-objcp-common.cc                     |   2 +
>   gcc/cp/cp-tree.h                              |   4 +
>   gcc/cp/cxx-pretty-print.cc                    |   6 +
>   gcc/cp/method.cc                              |  31 ++
>   gcc/cp/parser.cc                              |  10 +
>   gcc/cp/semantics.cc                           |   8 +
>   gcc/testsuite/g++.dg/ext/has-builtin-1.C      |   6 +
>   gcc/testsuite/g++.dg/ext/is_convertible1.C    | 269 +++++++++++++++++
>   gcc/testsuite/g++.dg/ext/is_convertible2.C    |  46 +++
>   .../g++.dg/ext/is_nothrow_convertible1.C      | 270 ++++++++++++++++++
>   .../g++.dg/ext/is_nothrow_convertible2.C      |  19 ++
>   libstdc++-v3/include/std/type_traits          |   4 +-
>   .../is_nothrow_convertible/value_ext.cc       |   4 +-
>   16 files changed, 684 insertions(+), 4 deletions(-)
>   create mode 100644 gcc/testsuite/g++.dg/ext/is_convertible1.C
>   create mode 100644 gcc/testsuite/g++.dg/ext/is_convertible2.C
>   create mode 100644 gcc/testsuite/g++.dg/ext/is_nothrow_convertible1.C
>   create mode 100644 gcc/testsuite/g++.dg/ext/is_nothrow_convertible2.C
> 
> diff --git a/gcc/c-family/c-common.cc b/gcc/c-family/c-common.cc
> index c0f15f4cab1..dce3045c9f2 100644
> --- a/gcc/c-family/c-common.cc
> +++ b/gcc/c-family/c-common.cc
> @@ -541,6 +541,8 @@ const struct c_common_resword c_common_reswords[] =
>     { "__is_constructible", RID_IS_CONSTRUCTIBLE, D_CXXONLY },
>     { "__is_nothrow_assignable", RID_IS_NOTHROW_ASSIGNABLE, D_CXXONLY },
>     { "__is_nothrow_constructible", RID_IS_NOTHROW_CONSTRUCTIBLE, D_CXXONLY },
> +  { "__is_convertible", RID_IS_CONVERTIBLE, D_CXXONLY },
> +  { "__is_nothrow_convertible", RID_IS_NOTHROW_CONVERTIBLE, D_CXXONLY },
>     { "__reference_constructs_from_temporary", RID_REF_CONSTRUCTS_FROM_TEMPORARY,
>   					D_CXXONLY },
>     { "__reference_converts_from_temporary", RID_REF_CONVERTS_FROM_TEMPORARY,
> diff --git a/gcc/c-family/c-common.h b/gcc/c-family/c-common.h
> index 2f592f5cd58..31397d80029 100644
> --- a/gcc/c-family/c-common.h
> +++ b/gcc/c-family/c-common.h
> @@ -184,6 +184,7 @@ enum rid
>     RID_IS_UNION,                RID_UNDERLYING_TYPE,
>     RID_IS_ASSIGNABLE,           RID_IS_CONSTRUCTIBLE,
>     RID_IS_NOTHROW_ASSIGNABLE,   RID_IS_NOTHROW_CONSTRUCTIBLE,
> +  RID_IS_CONVERTIBLE,		RID_IS_NOTHROW_CONVERTIBLE,
>     RID_REF_CONSTRUCTS_FROM_TEMPORARY,
>     RID_REF_CONVERTS_FROM_TEMPORARY,
>   
> diff --git a/gcc/cp/constraint.cc b/gcc/cp/constraint.cc
> index 568318f0ba1..5839bfb4b52 100644
> --- a/gcc/cp/constraint.cc
> +++ b/gcc/cp/constraint.cc
> @@ -3697,6 +3697,12 @@ diagnose_trait_expr (tree expr, tree args)
>       case CPTK_HAS_UNIQUE_OBJ_REPRESENTATIONS:
>         inform (loc, "  %qT does not have unique object representations", t1);
>         break;
> +    case CPTK_IS_CONVERTIBLE:
> +      inform (loc, "  %qT is not convertible from %qE", t2, t1);
> +      break;
> +    case CPTK_IS_NOTHROW_CONVERTIBLE:
> +	inform (loc, "  %qT is not %<nothrow%> convertible from %qE", t2, t1);

It's odd that the existing diagnostics quote "nothrow", which is not a 
keyword.  I wonder why these library traits didn't use "noexcept"?

> +      break;
>       case CPTK_REF_CONSTRUCTS_FROM_TEMPORARY:
>         inform (loc, "  %qT is not a reference that binds to a temporary "
>   	      "object of type %qT (direct-initialization)", t1, t2);
> diff --git a/gcc/cp/cp-objcp-common.cc b/gcc/cp/cp-objcp-common.cc
> index 1ffac08c32f..64975699351 100644
> --- a/gcc/cp/cp-objcp-common.cc
> +++ b/gcc/cp/cp-objcp-common.cc
> @@ -463,6 +463,8 @@ names_builtin_p (const char *name)
>       case RID_IS_NOTHROW_ASSIGNABLE:
>       case RID_IS_NOTHROW_CONSTRUCTIBLE:
>       case RID_UNDERLYING_TYPE:
> +    case RID_IS_CONVERTIBLE:
> +    case RID_IS_NOTHROW_CONVERTIBLE:
>       case RID_REF_CONSTRUCTS_FROM_TEMPORARY:
>       case RID_REF_CONVERTS_FROM_TEMPORARY:
>         return true;
> diff --git a/gcc/cp/cp-tree.h b/gcc/cp/cp-tree.h
> index f19ecafc266..e4d89207e2a 100644
> --- a/gcc/cp/cp-tree.h
> +++ b/gcc/cp/cp-tree.h
> @@ -1407,6 +1407,8 @@ enum cp_trait_kind
>     CPTK_IS_CONSTRUCTIBLE,
>     CPTK_IS_NOTHROW_ASSIGNABLE,
>     CPTK_IS_NOTHROW_CONSTRUCTIBLE,
> +  CPTK_IS_CONVERTIBLE,
> +  CPTK_IS_NOTHROW_CONVERTIBLE,
>     CPTK_REF_CONSTRUCTS_FROM_TEMPORARY,
>     CPTK_REF_CONVERTS_FROM_TEMPORARY
>   };
> @@ -7116,6 +7118,8 @@ extern tree forward_parm			(tree);
>   extern bool is_trivially_xible			(enum tree_code, tree, tree);
>   extern bool is_nothrow_xible			(enum tree_code, tree, tree);
>   extern bool is_xible				(enum tree_code, tree, tree);
> +extern bool is_convertible			(tree, tree);
> +extern bool is_nothrow_convertible		(tree, tree);
>   extern bool ref_xes_from_temporary		(tree, tree, bool);
>   extern tree get_defaulted_eh_spec		(tree, tsubst_flags_t = tf_warning_or_error);
>   extern bool maybe_explain_implicit_delete	(tree);
> diff --git a/gcc/cp/cxx-pretty-print.cc b/gcc/cp/cxx-pretty-print.cc
> index 44590830a61..e18143e39a9 100644
> --- a/gcc/cp/cxx-pretty-print.cc
> +++ b/gcc/cp/cxx-pretty-print.cc
> @@ -2696,6 +2696,12 @@ pp_cxx_trait_expression (cxx_pretty_printer *pp, tree t)
>       case CPTK_IS_NOTHROW_CONSTRUCTIBLE:
>         pp_cxx_ws_string (pp, "__is_nothrow_constructible");
>         break;
> +    case CPTK_IS_CONVERTIBLE:
> +      pp_cxx_ws_string (pp, "__is_convertible");
> +      break;
> +    case CPTK_IS_NOTHROW_CONVERTIBLE:
> +      pp_cxx_ws_string (pp, "__is_nothrow_convertible");
> +      break;
>       case CPTK_REF_CONSTRUCTS_FROM_TEMPORARY:
>         pp_cxx_ws_string (pp, "__reference_constructs_from_temporary");
>         break;
> diff --git a/gcc/cp/method.cc b/gcc/cp/method.cc
> index 573ef016f82..c35a59fe56c 100644
> --- a/gcc/cp/method.cc
> +++ b/gcc/cp/method.cc
> @@ -2236,6 +2236,37 @@ ref_xes_from_temporary (tree to, tree from, bool direct_init_p)
>     return ref_conv_binds_directly (to, val, direct_init_p).is_false ();
>   }
>   
> +/* Return true if FROM can be converted to TO using implicit conversions,
> +   or both FROM and TO are possibly cv-qualified void.  NB: This doesn't
> +   implement the "Access checks are performed as if from a context unrelated
> +   to either type" restriction.  */
> +
> +bool
> +is_convertible (tree from, tree to)

You didn't want to add conversion to is*_xible?

> +{
> +  if (VOID_TYPE_P (from) && VOID_TYPE_P (to))
> +    return true;
> +  tree expr = build_stub_object (from);
> +  expr = perform_implicit_conversion (to, expr, tf_none);
> +  if (expr == error_mark_node)
> +    return false;
> +  return !!expr;
> +}
> +
> +/* Like is_convertible, but the conversion is also noexcept.  */
> +
> +bool
> +is_nothrow_convertible (tree from, tree to)
> +{
> +  if (VOID_TYPE_P (from) && VOID_TYPE_P (to))
> +    return true;
> +  tree expr = build_stub_object (from);
> +  expr = perform_implicit_conversion (to, expr, tf_none);
> +  if (expr == NULL_TREE || expr == error_mark_node)
> +    return false;
> +  return expr_noexcept_p (expr, tf_none);
> +}
> +
>   /* Categorize various special_function_kinds.  */
>   #define SFK_CTOR_P(sfk) \
>     ((sfk) >= sfk_constructor && (sfk) <= sfk_move_constructor)
> diff --git a/gcc/cp/parser.cc b/gcc/cp/parser.cc
> index 3cbe0d69de1..bb83d1c78f6 100644
> --- a/gcc/cp/parser.cc
> +++ b/gcc/cp/parser.cc
> @@ -5922,6 +5922,8 @@ cp_parser_primary_expression (cp_parser *parser,
>   	case RID_IS_CONSTRUCTIBLE:
>   	case RID_IS_NOTHROW_ASSIGNABLE:
>   	case RID_IS_NOTHROW_CONSTRUCTIBLE:
> +	case RID_IS_CONVERTIBLE:
> +	case RID_IS_NOTHROW_CONVERTIBLE:
>   	case RID_REF_CONSTRUCTS_FROM_TEMPORARY:
>   	case RID_REF_CONVERTS_FROM_TEMPORARY:
>   	  return cp_parser_trait_expr (parser, token->keyword);
> @@ -11008,6 +11010,14 @@ cp_parser_trait_expr (cp_parser* parser, enum rid keyword)
>         kind = CPTK_IS_NOTHROW_CONSTRUCTIBLE;
>         variadic = true;
>         break;
> +    case RID_IS_CONVERTIBLE:
> +      kind = CPTK_IS_CONVERTIBLE;
> +      binary = true;
> +      break;
> +    case RID_IS_NOTHROW_CONVERTIBLE:
> +      kind = CPTK_IS_NOTHROW_CONVERTIBLE;
> +      binary = true;
> +      break;
>       case RID_REF_CONSTRUCTS_FROM_TEMPORARY:
>         kind = CPTK_REF_CONSTRUCTS_FROM_TEMPORARY;
>         binary = true;
> diff --git a/gcc/cp/semantics.cc b/gcc/cp/semantics.cc
> index 86562071612..92fc795df40 100644
> --- a/gcc/cp/semantics.cc
> +++ b/gcc/cp/semantics.cc
> @@ -12044,6 +12044,12 @@ trait_expr_value (cp_trait_kind kind, tree type1, tree type2)
>       case CPTK_IS_NOTHROW_CONSTRUCTIBLE:
>         return is_nothrow_xible (INIT_EXPR, type1, type2);
>   
> +    case CPTK_IS_CONVERTIBLE:
> +      return is_convertible (type1, type2);
> +
> +    case CPTK_IS_NOTHROW_CONVERTIBLE:
> +      return is_nothrow_convertible (type1, type2);
> +
>       case CPTK_REF_CONSTRUCTS_FROM_TEMPORARY:
>         return ref_xes_from_temporary (type1, type2, /*direct_init=*/true);
>   
> @@ -12165,6 +12171,8 @@ finish_trait_expr (location_t loc, cp_trait_kind kind, tree type1, tree type2)
>       case CPTK_IS_TRIVIALLY_CONSTRUCTIBLE:
>       case CPTK_IS_NOTHROW_ASSIGNABLE:
>       case CPTK_IS_NOTHROW_CONSTRUCTIBLE:
> +    case CPTK_IS_CONVERTIBLE:
> +    case CPTK_IS_NOTHROW_CONVERTIBLE:
>       case CPTK_REF_CONSTRUCTS_FROM_TEMPORARY:
>       case CPTK_REF_CONVERTS_FROM_TEMPORARY:
>         if (!check_trait_type (type1)
> diff --git a/gcc/testsuite/g++.dg/ext/has-builtin-1.C b/gcc/testsuite/g++.dg/ext/has-builtin-1.C
> index fe25cb2f669..17dabf648cf 100644
> --- a/gcc/testsuite/g++.dg/ext/has-builtin-1.C
> +++ b/gcc/testsuite/g++.dg/ext/has-builtin-1.C
> @@ -131,3 +131,9 @@
>   #if !__has_builtin (__builtin_is_pointer_interconvertible_with_class)
>   # error "__has_builtin (__builtin_is_pointer_interconvertible_with_class) failed"
>   #endif
> +#if !__has_builtin (__is_convertible)
> +# error "__has_builtin (__is_convertible) failed"
> +#endif
> +#if !__has_builtin (__is_nothrow_convertible)
> +# error "__has_builtin (__is_nothrow_convertible) failed"
> +#endif
> diff --git a/gcc/testsuite/g++.dg/ext/is_convertible1.C b/gcc/testsuite/g++.dg/ext/is_convertible1.C
> new file mode 100644
> index 00000000000..2e72945bceb
> --- /dev/null
> +++ b/gcc/testsuite/g++.dg/ext/is_convertible1.C
> @@ -0,0 +1,269 @@
> +// PR c++/106784
> +// { dg-do compile { target c++11 } }
> +
> +#define SA(X) static_assert((X),#X)
> +
> +template<typename From, typename To>
> +struct is_convertible {
> +  static const bool value = __is_convertible(From, To);
> +};
> +
> +struct from_int {
> +  from_int(int);
> +};
> +
> +struct from_charp {
> +  from_charp(const char *);
> +};
> +
> +struct to_int {
> +  operator int();
> +};
> +
> +typedef int Fn(int);
> +typedef char Arr[10];
> +enum E { XYZZY };
> +
> +SA(!__is_convertible(int, void));
> +SA(__is_convertible(int, int));
> +SA(__is_convertible(int, from_int));
> +SA(__is_convertible(long, from_int));
> +SA(__is_convertible(double, from_int));
> +SA(__is_convertible(const int, from_int));
> +SA(__is_convertible(const int&, from_int));
> +SA(__is_convertible(to_int, int));
> +SA(__is_convertible(to_int, const int&));
> +SA(__is_convertible(to_int, long));
> +SA(!__is_convertible(to_int, int&));
> +SA(!__is_convertible(to_int, from_int));
> +SA(!__is_convertible(int, Fn));
> +SA(!__is_convertible(int, Fn*));
> +SA(!__is_convertible(int, Fn&));
> +SA(!__is_convertible(int, Arr));
> +SA(!__is_convertible(int, Arr&));
> +SA(!__is_convertible(int, int&));
> +SA(__is_convertible(int, const int&));
> +SA(!__is_convertible(const int, int&));
> +SA(__is_convertible(const int, const int&));
> +SA(!__is_convertible(int, int*));
> +
> +SA(!__is_convertible(int, E));
> +SA(__is_convertible(E, int));
> +
> +SA(__is_convertible(int&, int));
> +SA(__is_convertible(int&, int&));
> +SA(__is_convertible(int&, const int&));
> +SA(!__is_convertible(const int&, int&));
> +SA(__is_convertible(const int&, const int&));
> +SA(!__is_convertible(int&, int*));
> +SA(!__is_convertible(int&, void));
> +SA(!__is_convertible(int&, Fn));
> +SA(!__is_convertible(int&, Fn*));
> +SA(!__is_convertible(int&, Fn&));
> +SA(!__is_convertible(int&, Arr));
> +SA(!__is_convertible(int&, Arr&));
> +
> +SA(!__is_convertible(int*, int));
> +SA(!__is_convertible(int*, int&));
> +SA(!__is_convertible(int*, void));
> +SA(__is_convertible(int*, int*));
> +SA(__is_convertible(int*, const int*));
> +SA(!__is_convertible(const int*, int*));
> +SA(__is_convertible(const int*, const int*));
> +SA(!__is_convertible(int*, Fn));
> +SA(!__is_convertible(int*, Fn*));
> +SA(!__is_convertible(int*, Fn&));
> +SA(!__is_convertible(int*, Arr));
> +SA(!__is_convertible(int*, Arr&));
> +SA(!__is_convertible(int*, float*));
> +
> +SA(__is_convertible(void, void));
> +SA(!__is_convertible(void, char));
> +SA(!__is_convertible(void, char&));
> +SA(!__is_convertible(void, char*));
> +SA(!__is_convertible(char, void));
> +SA(__is_convertible(const void, void));
> +SA(__is_convertible(void, const void));
> +SA(__is_convertible(const void, const void));
> +SA(!__is_convertible(void, Fn));
> +SA(!__is_convertible(void, Fn&));
> +SA(!__is_convertible(void, Fn*));
> +SA(!__is_convertible(void, Arr));
> +SA(!__is_convertible(void, Arr&));
> +
> +SA(!__is_convertible(Fn, void));
> +SA(!__is_convertible(Fn, Fn));
> +SA(__is_convertible(Fn, Fn*));
> +SA(__is_convertible(Fn, Fn&));
> +SA(!__is_convertible(int(int), int(int)));
> +SA(__is_convertible(int(int), int(&)(int)));
> +SA(__is_convertible(int(int), int(&&)(int)));
> +SA(__is_convertible(int(int), int(*)(int)));
> +SA(__is_convertible(int(int), int(*const)(int)));
> +SA(!__is_convertible(int(int), char));
> +SA(!__is_convertible(int(int), char*));
> +SA(!__is_convertible(int(int), char&));
> +
> +SA(!__is_convertible(Fn&, void));
> +SA(!__is_convertible(Fn&, Fn));
> +SA(__is_convertible(Fn&, Fn&));
> +SA(__is_convertible(Fn&, Fn*));
> +SA(!__is_convertible(Fn&, Arr));
> +SA(!__is_convertible(Fn&, Arr&));
> +SA(!__is_convertible(Fn&, char));
> +SA(!__is_convertible(Fn&, char&));
> +SA(!__is_convertible(Fn&, char*));
> +
> +SA(!__is_convertible(Fn*, void));
> +SA(!__is_convertible(Fn*, Fn));
> +SA(!__is_convertible(Fn*, Fn&));
> +SA(__is_convertible(Fn*, Fn*));
> +SA(!__is_convertible(Fn*, Arr));
> +SA(!__is_convertible(Fn*, Arr&));
> +SA(!__is_convertible(Fn*, char));
> +SA(!__is_convertible(Fn*, char&));
> +SA(!__is_convertible(Fn*, char*));
> +
> +SA(!__is_convertible(Arr, void));
> +SA(!__is_convertible(Arr, Fn));
> +SA(!__is_convertible(Arr, Fn*));
> +SA(!__is_convertible(Arr, Fn&));
> +SA(!__is_convertible(Arr, Arr));
> +SA(!__is_convertible(Arr, Arr&));
> +SA(__is_convertible(Arr, const Arr&));
> +SA(!__is_convertible(Arr, volatile Arr&));
> +SA(!__is_convertible(Arr, const volatile Arr&));
> +SA(!__is_convertible(const Arr, Arr&));
> +SA(__is_convertible(const Arr, const Arr&));
> +SA(__is_convertible(Arr, Arr&&));
> +SA(__is_convertible(Arr, const Arr&&));
> +SA(__is_convertible(Arr, volatile Arr&&));
> +SA(__is_convertible(Arr, const volatile Arr&&));
> +SA(__is_convertible(const Arr, const Arr&&));
> +SA(!__is_convertible(Arr&, Arr&&));
> +SA(!__is_convertible(Arr&&, Arr&));
> +SA(!__is_convertible(Arr, char));
> +SA(__is_convertible(Arr, char*));
> +SA(__is_convertible(Arr, const char*));
> +SA(!__is_convertible(Arr, char&));
> +SA(!__is_convertible(const Arr, char*));
> +SA(__is_convertible(const Arr, const char*));
> +SA(!__is_convertible(int, int[1]));
> +SA(!__is_convertible(int[1], int[1]));
> +SA(!__is_convertible(int[1], int(&)[1]));
> +SA(__is_convertible(int(&)[1], int(&)[1]));
> +SA(__is_convertible(int(&)[1], const int(&)[1]));
> +SA(!__is_convertible(const int(&)[1], int(&)[1]));
> +SA(!__is_convertible(int[1][1], int*));
> +SA(!__is_convertible(int[][1], int*));
> +
> +SA(!__is_convertible(Arr&, void));
> +SA(!__is_convertible(Arr&, Fn));
> +SA(!__is_convertible(Arr&, Fn*));
> +SA(!__is_convertible(Arr&, Fn&));
> +SA(!__is_convertible(Arr&, Arr));
> +SA(__is_convertible(Arr&, Arr&));
> +SA(__is_convertible(Arr&, const Arr&));
> +SA(!__is_convertible(const Arr&, Arr&));
> +SA(__is_convertible(const Arr&, const Arr&));
> +SA(!__is_convertible(Arr&, char));
> +SA(__is_convertible(Arr&, char*));
> +SA(__is_convertible(Arr&, const char*));
> +SA(!__is_convertible(Arr&, char&));
> +SA(!__is_convertible(const Arr&, char*));
> +SA(__is_convertible(const Arr&, const char*));
> +SA(__is_convertible(Arr, from_charp));
> +SA(__is_convertible(Arr&, from_charp));
> +
> +struct B { };
> +struct D : B { };
> +
> +SA(__is_convertible(D, B));
> +SA(__is_convertible(D*, B*));
> +SA(__is_convertible(D&, B&));
> +SA(!__is_convertible(B, D));
> +SA(!__is_convertible(B*, D*));
> +SA(!__is_convertible(B&, D&));
> +
> +/* These are taken from LLVM's test/SemaCXX/type-traits.cpp.  */
> +
> +struct I {
> +  int i;
> +  I(int _i) : i(_i) { }
> +  operator int() const {
> +    return i;
> +  }
> +};
> +
> +struct F
> +{
> +  float f;
> +  F(float _f) : f(_f) {}
> +  F(const I& obj)
> +    : f(static_cast<float>(obj.i)) {}
> +  operator float() const {
> +    return f;
> +  }
> +  operator I() const {
> +    return I(static_cast<int>(f));
> +  }
> +};
> +
> +SA(__is_convertible(I, I));
> +SA(__is_convertible(I, const I));
> +SA(__is_convertible(I, int));
> +SA(__is_convertible(int, I));
> +SA(__is_convertible(I, F));
> +SA(__is_convertible(F, I));
> +SA(__is_convertible(F, float));
> +SA(__is_convertible(float, F));
> +
> +template<typename>
> +struct X {
> +  template<typename U> X(const X<U>&);
> +};
> +
> +SA(__is_convertible(X<int>, X<float>));
> +SA(__is_convertible(X<float>, X<int>));
> +
> +struct Abstract {
> +  virtual void f() = 0;
> +};
> +
> +SA(!__is_convertible(Abstract, Abstract));
> +
> +class hidden {
> +  hidden(const hidden&);
> +  friend void test ();
> +};
> +
> +SA(__is_convertible(hidden&, hidden&));
> +SA(__is_convertible(hidden&, const hidden&));
> +SA(__is_convertible(hidden&, volatile hidden&));
> +SA(__is_convertible(hidden&, const volatile hidden&));
> +SA(__is_convertible(const hidden&, const hidden&));
> +SA(__is_convertible(const hidden&, const volatile hidden&));
> +SA(__is_convertible(volatile hidden&, const volatile hidden&));
> +SA(__is_convertible(const volatile hidden&, const volatile hidden&));
> +SA(!__is_convertible(const hidden&, hidden&));
> +
> +void
> +test ()
> +{
> +  /* __is_convertible(hidden, hidden) should be false despite the
> +     friend declaration above, because "Access checks are performed
> +     as if from a context unrelated to either type", but we don't
> +     implement that for the built-in (std::is_convertible works as
> +     expected).  This is the case for __is_assignable as well.  */
> +  //SA(!__is_convertible(hidden, hidden));
> +}
> +
> +void
> +test2 ()
> +{
> +  struct X { };
> +  struct Y {
> +    explicit Y(X); // not viable for implicit conversions
> +  };
> +  SA(!__is_convertible(X, Y));
> +}
> diff --git a/gcc/testsuite/g++.dg/ext/is_convertible2.C b/gcc/testsuite/g++.dg/ext/is_convertible2.C
> new file mode 100644
> index 00000000000..9b46e264379
> --- /dev/null
> +++ b/gcc/testsuite/g++.dg/ext/is_convertible2.C
> @@ -0,0 +1,46 @@
> +// PR c++/106784
> +// { dg-do compile { target c++20 } }
> +// Adapted from <https://en.cppreference.com/w/cpp/types/is_convertible>.
> +
> +#include <string>
> +#include <string_view>
> +
> +#define SA(X) static_assert((X),#X)
> +
> +class E { public: template<class T> E(T&&) { } };
> +
> +int main()
> +{
> +    class A {};
> +    class B : public A {};
> +    class C {};
> +    class D { public: operator C() { return c; }  C c; };
> +
> +    SA(__is_convertible(B*, A*));
> +    SA(!__is_convertible(A*, B*));
> +    SA(__is_convertible(D, C));
> +    SA(!__is_convertible(B*, C*));
> +    SA(__is_convertible(A, E));
> +
> +    using std::operator "" s, std::operator "" sv;
> +
> +    auto stringify = []<typename T>(T x) {
> +        if constexpr (std::is_convertible_v<T, std::string> or
> +                      std::is_convertible_v<T, std::string_view>) {
> +            return x;
> +        } else {
> +            return std::to_string(x);
> +        }
> +    };
> +
> +    const char* three = "three";
> +
> +    SA(!__is_convertible(std::string_view, std::string));
> +    SA(__is_convertible(std::string, std::string_view));
> +
> +    auto s1 = stringify("one"s);
> +    auto s2 = stringify("two"sv);
> +    auto s3 = stringify(three);
> +    auto s4 = stringify(42);
> +    auto s5 = stringify(42.);
> +}
> diff --git a/gcc/testsuite/g++.dg/ext/is_nothrow_convertible1.C b/gcc/testsuite/g++.dg/ext/is_nothrow_convertible1.C
> new file mode 100644
> index 00000000000..bb7243e4611
> --- /dev/null
> +++ b/gcc/testsuite/g++.dg/ext/is_nothrow_convertible1.C
> @@ -0,0 +1,270 @@
> +// PR c++/106784
> +// { dg-do compile { target c++11 } }
> +// Like is_convertible1.C, but conversion functions are made noexcept.
> +
> +#define SA(X) static_assert((X),#X)
> +
> +template<typename From, typename To>
> +struct is_nothrow_convertible {
> +  static const bool value = __is_nothrow_convertible(From, To);
> +};
> +
> +struct from_int {
> +  from_int(int) noexcept;
> +};
> +
> +struct from_charp {
> +  from_charp(const char *) noexcept;
> +};
> +
> +struct to_int {
> +  operator int() noexcept;
> +};
> +
> +typedef int Fn(int);
> +typedef char Arr[10];
> +enum E { XYZZY };
> +
> +SA(!__is_nothrow_convertible(int, void));
> +SA(__is_nothrow_convertible(int, int));
> +SA(__is_nothrow_convertible(int, from_int));
> +SA(__is_nothrow_convertible(long, from_int));
> +SA(__is_nothrow_convertible(double, from_int));
> +SA(__is_nothrow_convertible(const int, from_int));
> +SA(__is_nothrow_convertible(const int&, from_int));
> +SA(__is_nothrow_convertible(to_int, int));
> +SA(__is_nothrow_convertible(to_int, const int&));
> +SA(__is_nothrow_convertible(to_int, long));
> +SA(!__is_nothrow_convertible(to_int, int&));
> +SA(!__is_nothrow_convertible(to_int, from_int));
> +SA(!__is_nothrow_convertible(int, Fn));
> +SA(!__is_nothrow_convertible(int, Fn*));
> +SA(!__is_nothrow_convertible(int, Fn&));
> +SA(!__is_nothrow_convertible(int, Arr));
> +SA(!__is_nothrow_convertible(int, Arr&));
> +SA(!__is_nothrow_convertible(int, int&));
> +SA(__is_nothrow_convertible(int, const int&));
> +SA(!__is_nothrow_convertible(const int, int&));
> +SA(__is_nothrow_convertible(const int, const int&));
> +SA(!__is_nothrow_convertible(int, int*));
> +
> +SA(!__is_nothrow_convertible(int, E));
> +SA(__is_nothrow_convertible(E, int));
> +
> +SA(__is_nothrow_convertible(int&, int));
> +SA(__is_nothrow_convertible(int&, int&));
> +SA(__is_nothrow_convertible(int&, const int&));
> +SA(!__is_nothrow_convertible(const int&, int&));
> +SA(__is_nothrow_convertible(const int&, const int&));
> +SA(!__is_nothrow_convertible(int&, int*));
> +SA(!__is_nothrow_convertible(int&, void));
> +SA(!__is_nothrow_convertible(int&, Fn));
> +SA(!__is_nothrow_convertible(int&, Fn*));
> +SA(!__is_nothrow_convertible(int&, Fn&));
> +SA(!__is_nothrow_convertible(int&, Arr));
> +SA(!__is_nothrow_convertible(int&, Arr&));
> +
> +SA(!__is_nothrow_convertible(int*, int));
> +SA(!__is_nothrow_convertible(int*, int&));
> +SA(!__is_nothrow_convertible(int*, void));
> +SA(__is_nothrow_convertible(int*, int*));
> +SA(__is_nothrow_convertible(int*, const int*));
> +SA(!__is_nothrow_convertible(const int*, int*));
> +SA(__is_nothrow_convertible(const int*, const int*));
> +SA(!__is_nothrow_convertible(int*, Fn));
> +SA(!__is_nothrow_convertible(int*, Fn*));
> +SA(!__is_nothrow_convertible(int*, Fn&));
> +SA(!__is_nothrow_convertible(int*, Arr));
> +SA(!__is_nothrow_convertible(int*, Arr&));
> +SA(!__is_nothrow_convertible(int*, float*));
> +
> +SA(__is_nothrow_convertible(void, void));
> +SA(!__is_nothrow_convertible(void, char));
> +SA(!__is_nothrow_convertible(void, char&));
> +SA(!__is_nothrow_convertible(void, char*));
> +SA(!__is_nothrow_convertible(char, void));
> +SA(__is_nothrow_convertible(const void, void));
> +SA(__is_nothrow_convertible(void, const void));
> +SA(__is_nothrow_convertible(const void, const void));
> +SA(!__is_nothrow_convertible(void, Fn));
> +SA(!__is_nothrow_convertible(void, Fn&));
> +SA(!__is_nothrow_convertible(void, Fn*));
> +SA(!__is_nothrow_convertible(void, Arr));
> +SA(!__is_nothrow_convertible(void, Arr&));
> +
> +SA(!__is_nothrow_convertible(Fn, void));
> +SA(!__is_nothrow_convertible(Fn, Fn));
> +SA(__is_nothrow_convertible(Fn, Fn*));
> +SA(__is_nothrow_convertible(Fn, Fn&));
> +SA(!__is_nothrow_convertible(int(int), int(int)));
> +SA(__is_nothrow_convertible(int(int), int(&)(int)));
> +SA(__is_nothrow_convertible(int(int), int(&&)(int)));
> +SA(__is_nothrow_convertible(int(int), int(*)(int)));
> +SA(__is_nothrow_convertible(int(int), int(*const)(int)));
> +SA(!__is_nothrow_convertible(int(int), char));
> +SA(!__is_nothrow_convertible(int(int), char*));
> +SA(!__is_nothrow_convertible(int(int), char&));
> +
> +SA(!__is_nothrow_convertible(Fn&, void));
> +SA(!__is_nothrow_convertible(Fn&, Fn));
> +SA(__is_nothrow_convertible(Fn&, Fn&));
> +SA(__is_nothrow_convertible(Fn&, Fn*));
> +SA(!__is_nothrow_convertible(Fn&, Arr));
> +SA(!__is_nothrow_convertible(Fn&, Arr&));
> +SA(!__is_nothrow_convertible(Fn&, char));
> +SA(!__is_nothrow_convertible(Fn&, char&));
> +SA(!__is_nothrow_convertible(Fn&, char*));
> +
> +SA(!__is_nothrow_convertible(Fn*, void));
> +SA(!__is_nothrow_convertible(Fn*, Fn));
> +SA(!__is_nothrow_convertible(Fn*, Fn&));
> +SA(__is_nothrow_convertible(Fn*, Fn*));
> +SA(!__is_nothrow_convertible(Fn*, Arr));
> +SA(!__is_nothrow_convertible(Fn*, Arr&));
> +SA(!__is_nothrow_convertible(Fn*, char));
> +SA(!__is_nothrow_convertible(Fn*, char&));
> +SA(!__is_nothrow_convertible(Fn*, char*));
> +
> +SA(!__is_nothrow_convertible(Arr, void));
> +SA(!__is_nothrow_convertible(Arr, Fn));
> +SA(!__is_nothrow_convertible(Arr, Fn*));
> +SA(!__is_nothrow_convertible(Arr, Fn&));
> +SA(!__is_nothrow_convertible(Arr, Arr));
> +SA(!__is_nothrow_convertible(Arr, Arr&));
> +SA(__is_nothrow_convertible(Arr, const Arr&));
> +SA(!__is_nothrow_convertible(Arr, volatile Arr&));
> +SA(!__is_nothrow_convertible(Arr, const volatile Arr&));
> +SA(!__is_nothrow_convertible(const Arr, Arr&));
> +SA(__is_nothrow_convertible(const Arr, const Arr&));
> +SA(__is_nothrow_convertible(Arr, Arr&&));
> +SA(__is_nothrow_convertible(Arr, const Arr&&));
> +SA(__is_nothrow_convertible(Arr, volatile Arr&&));
> +SA(__is_nothrow_convertible(Arr, const volatile Arr&&));
> +SA(__is_nothrow_convertible(const Arr, const Arr&&));
> +SA(!__is_nothrow_convertible(Arr&, Arr&&));
> +SA(!__is_nothrow_convertible(Arr&&, Arr&));
> +SA(!__is_nothrow_convertible(Arr, char));
> +SA(__is_nothrow_convertible(Arr, char*));
> +SA(__is_nothrow_convertible(Arr, const char*));
> +SA(!__is_nothrow_convertible(Arr, char&));
> +SA(!__is_nothrow_convertible(const Arr, char*));
> +SA(__is_nothrow_convertible(const Arr, const char*));
> +SA(!__is_nothrow_convertible(int, int[1]));
> +SA(!__is_nothrow_convertible(int[1], int[1]));
> +SA(!__is_nothrow_convertible(int[1], int(&)[1]));
> +SA(__is_nothrow_convertible(int(&)[1], int(&)[1]));
> +SA(__is_nothrow_convertible(int(&)[1], const int(&)[1]));
> +SA(!__is_nothrow_convertible(const int(&)[1], int(&)[1]));
> +SA(!__is_nothrow_convertible(int[1][1], int*));
> +SA(!__is_nothrow_convertible(int[][1], int*));
> +
> +SA(!__is_nothrow_convertible(Arr&, void));
> +SA(!__is_nothrow_convertible(Arr&, Fn));
> +SA(!__is_nothrow_convertible(Arr&, Fn*));
> +SA(!__is_nothrow_convertible(Arr&, Fn&));
> +SA(!__is_nothrow_convertible(Arr&, Arr));
> +SA(__is_nothrow_convertible(Arr&, Arr&));
> +SA(__is_nothrow_convertible(Arr&, const Arr&));
> +SA(!__is_nothrow_convertible(const Arr&, Arr&));
> +SA(__is_nothrow_convertible(const Arr&, const Arr&));
> +SA(!__is_nothrow_convertible(Arr&, char));
> +SA(__is_nothrow_convertible(Arr&, char*));
> +SA(__is_nothrow_convertible(Arr&, const char*));
> +SA(!__is_nothrow_convertible(Arr&, char&));
> +SA(!__is_nothrow_convertible(const Arr&, char*));
> +SA(__is_nothrow_convertible(const Arr&, const char*));
> +SA(__is_nothrow_convertible(Arr, from_charp));
> +SA(__is_nothrow_convertible(Arr&, from_charp));
> +
> +struct B { };
> +struct D : B { };
> +
> +SA(__is_nothrow_convertible(D, B));
> +SA(__is_nothrow_convertible(D*, B*));
> +SA(__is_nothrow_convertible(D&, B&));
> +SA(!__is_nothrow_convertible(B, D));
> +SA(!__is_nothrow_convertible(B*, D*));
> +SA(!__is_nothrow_convertible(B&, D&));
> +
> +/* These are taken from LLVM's test/SemaCXX/type-traits.cpp.  */
> +
> +struct I {
> +  int i;
> +  I(int _i) noexcept : i(_i)  { }
> +  operator int() const noexcept {
> +    return i;
> +  }
> +};
> +
> +struct F
> +{
> +  float f;
> +  F(float _f) noexcept : f(_f) {}
> +  F(const I& obj) noexcept
> +    : f(static_cast<float>(obj.i)) {}
> +  operator float() const noexcept {
> +    return f;
> +  }
> +  operator I() const noexcept {
> +    return I(static_cast<int>(f));
> +  }
> +};
> +
> +SA(__is_nothrow_convertible(I, I));
> +SA(__is_nothrow_convertible(I, const I));
> +SA(__is_nothrow_convertible(I, int));
> +SA(__is_nothrow_convertible(int, I));
> +SA(__is_nothrow_convertible(I, F));
> +SA(__is_nothrow_convertible(F, I));
> +SA(__is_nothrow_convertible(F, float));
> +SA(__is_nothrow_convertible(float, F));
> +
> +template<typename>
> +struct X {
> +  template<typename U> X(const X<U>&) noexcept;
> +};
> +
> +SA(__is_nothrow_convertible(X<int>, X<float>));
> +SA(__is_nothrow_convertible(X<float>, X<int>));
> +
> +struct Abstract {
> +  virtual void f() = 0;
> +};
> +
> +SA(!__is_nothrow_convertible(Abstract, Abstract));
> +
> +class hidden {
> +  hidden(const hidden&);
> +  friend void test ();
> +};
> +
> +SA(__is_nothrow_convertible(hidden&, hidden&));
> +SA(__is_nothrow_convertible(hidden&, const hidden&));
> +SA(__is_nothrow_convertible(hidden&, volatile hidden&));
> +SA(__is_nothrow_convertible(hidden&, const volatile hidden&));
> +SA(__is_nothrow_convertible(const hidden&, const hidden&));
> +SA(__is_nothrow_convertible(const hidden&, const volatile hidden&));
> +SA(__is_nothrow_convertible(volatile hidden&, const volatile hidden&));
> +SA(__is_nothrow_convertible(const volatile hidden&, const volatile hidden&));
> +SA(!__is_nothrow_convertible(const hidden&, hidden&));
> +
> +void
> +test ()
> +{
> +  /* __is_nothrow_convertible(hidden, hidden) should be false despite the
> +     friend declaration above, because "Access checks are performed
> +     as if from a context unrelated to either type", but we don't
> +     implement that for the built-in (std::is_convertible works as
> +     expected).  This is the case for __is_assignable as well.  */
> +  //SA(!__is_nothrow_convertible(hidden, hidden));
> +}
> +
> +void
> +test2 ()
> +{
> +  struct X { };
> +  struct Y {
> +    explicit Y(X); // not viable for implicit conversions
> +  };
> +  SA(!__is_nothrow_convertible(X, Y));
> +}
> diff --git a/gcc/testsuite/g++.dg/ext/is_nothrow_convertible2.C b/gcc/testsuite/g++.dg/ext/is_nothrow_convertible2.C
> new file mode 100644
> index 00000000000..aa089173b75
> --- /dev/null
> +++ b/gcc/testsuite/g++.dg/ext/is_nothrow_convertible2.C
> @@ -0,0 +1,19 @@
> +// PR c++/106784
> +// { dg-do compile { target c++11 } }
> +
> +#define SA(X) static_assert((X),#X)
> +
> +struct A { };
> +struct B { };
> +
> +struct M {
> +  operator A();
> +  operator B() noexcept;
> +  M(const A&);
> +  M(const B&) noexcept;
> +};
> +
> +SA(!__is_nothrow_convertible(A, M));
> +SA(!__is_nothrow_convertible(M, A));
> +SA(__is_nothrow_convertible(B, M));
> +SA(__is_nothrow_convertible(M, B));
> diff --git a/libstdc++-v3/include/std/type_traits b/libstdc++-v3/include/std/type_traits
> index 94e73eafd2f..1797b9e97f7 100644
> --- a/libstdc++-v3/include/std/type_traits
> +++ b/libstdc++-v3/include/std/type_traits
> @@ -1453,7 +1453,7 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
>   
>     // is_nothrow_convertible for C++11
>     template<typename _From, typename _To>
> -    struct __is_nothrow_convertible
> +    struct __is_nothrow_convertible_lib
>       : public __is_nt_convertible_helper<_From, _To>::type
>       { };
>   
> @@ -2999,7 +2999,7 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
>       struct __is_nt_invocable_impl<_Result, _Ret,
>   				  __void_t<typename _Result::type>>
>       : __or_<is_void<_Ret>,
> -	    __is_nothrow_convertible<typename _Result::type, _Ret>>::type
> +	    __is_nothrow_convertible_lib<typename _Result::type, _Ret>>::type
>       { };
>     /// @endcond
>   
> diff --git a/libstdc++-v3/testsuite/20_util/is_nothrow_convertible/value_ext.cc b/libstdc++-v3/testsuite/20_util/is_nothrow_convertible/value_ext.cc
> index 0f896428537..d736d2ca260 100644
> --- a/libstdc++-v3/testsuite/20_util/is_nothrow_convertible/value_ext.cc
> +++ b/libstdc++-v3/testsuite/20_util/is_nothrow_convertible/value_ext.cc
> @@ -19,10 +19,10 @@
>   
>   #include <type_traits>
>   
> -// Test the non-standard __is_nothrow_convertible trait
> +// Test the non-standard __is_nothrow_convertible_lib trait
>   
>   template<typename From, typename To>
> -  using is_nothrow_convertible = std::__is_nothrow_convertible<From, To>;
> +  using is_nothrow_convertible = std::__is_nothrow_convertible_lib<From, To>;
>   
>   #define IS_NT_CONVERTIBLE_DEFINED
>   #include "value.cc"
> 
> base-commit: 32d8123cd6ce87acb557aec230e8359051316f9f


  reply	other threads:[~2022-09-22 22:14 UTC|newest]

Thread overview: 10+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2022-09-22 13:39 Marek Polacek
2022-09-22 22:14 ` Jason Merrill [this message]
2022-09-23 14:34   ` Marek Polacek
2022-09-23 14:43     ` Jonathan Wakely
2022-09-23 16:34       ` Jonathan Wakely
2022-09-23 16:37         ` Marek Polacek
2022-09-23 15:54     ` Jason Merrill
2022-09-23 16:16       ` Marek Polacek
2022-09-23 14:40   ` Jonathan Wakely
2022-09-23 15:04     ` Marek Polacek

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=b8712969-ec22-31e1-769a-6f7d900b742d@redhat.com \
    --to=jason@redhat.com \
    --cc=gcc-patches@gcc.gnu.org \
    --cc=jwakely@redhat.com \
    --cc=libstdc++@gcc.gnu.org \
    --cc=polacek@redhat.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).